爱上JVM(二): JVM垃圾回收笔记分享(附参考学习视频!)

文章目录

如何判断对象可回收

引用计数法

如果一个对象被其他变量所引用,则让该对象的引用计数+1,如果该对象被引用2次则其引用计数为2,依次类推。

某个变量不再引用该对象,则让该对象的引用计数-1,当该对象的引用计数变为0时,则表示该对象没用被其他变量所引用,这时候该对象就可以被作为垃圾进行回收。

引用计数法弊端:循环引用时,两个对象的引用计数都为1,导致两个对象都无法被释放回收。最终就会造成内存泄漏!

可达性分析算法

可达性分析算法:就是JVM中判断对象是否是垃圾的算法:该算法首先要确定GC Root(根对象,就是肯定不会被当成垃圾回收的对象)。

在垃圾回收之前,JVM会先对堆中的所有对象进行扫描,判断每一个对象是否能被GC Root直接或者间接的引用,如果能被根对象直接或间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收:

JVM中的垃圾回收器通过可达性分析来探索所有存活的对象。

扫描堆中的对象,看能否沿着GC Root为起点的引用链找到该对象,如果找不到,则表示可以回收,否则就可以回收。

可以作为GC Root的对象:

虚拟机栈(栈帧中的本地变量表)中引用的对象。

方法区中类静态属性引用的对象。

方法区中常量引用的对象

本地方法栈中JNI(即一般说的Native方法)引用的对象。

可以理解成一个葡萄,从根出发能到达各个葡萄的位置。

案例演示:

这里在list1有无值两方面抓取快照。

然后通过eclipse提供的MAT工具来 GCRoot,哪些可以作为GCRoot

这些都可以作为gcroot

  • 所以以后我们查看跟对象就可以通过MAT工具啦。

五种引用

强软弱

  • 强引用:对于A1对象,只有两个GC Root都不在引用它,才会释放。
  • 软引用和弱引用,当内存不够的时候,看你就会释放A2,A3对象,之后软引用和弱引用就会进入到引用队列,如果要释放二者,就需要通过引用队列遍历,然后释放两个引用(因为它们俩也是对象)

虚终

  • 虚引用和终引用必须配合引用队列使 用

虚引用

当引用的对于ByteBuffer被回收以后,直接内存还没有被回收,虚引用对象(Cleaner)就会被放入到引用队列中,

然后就会由线程ReferencHandler定时去引用队列寻找是否有Cleaner对象,如果有,就会调用Cleaner对象的clean方法,而clean方法就会根据前面记录的直接内存地址,调用Unsafe.freeMemory()方法,来释放直接内存。

总之虚引用的一个体现是释放直接内存所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存。

根据上面的图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用 队列中,然后调用它的clean方法来释放直接内存。

可以回顾一下之前学的直接内存,和Cleaner底层原理。

终引用

所有的类都继承自Object类,Object类有一个finalize()方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize()方法。调用以后,该对象就可以被垃圾回收了。

当一个对象重写了finalize方法,肯定是希望通过终结器应用释放。

例如上图,当A4对象没有强引用的时候,这时候会进行垃圾回收,虚拟机会对这个对象创建终结器引用,终结器应用会进入到引用队列,但这时候A4对象还没有被释放!!

只有当优先级很低的FinallizeHandler线程来应用队列寻找是否有终结器应用,然后调用A4对象的finallize()方法,在下一次垃圾回收的时候A4对象才会得到释放。

所以也就有一个弊端,因为FinallizeHandler线程优先级很低,而且第一次内存回收的时候并不能完全释放。所以有时候对象迟迟得不到释放,内存就会一直被占用,所以这个终结器应用在日常用的并不多。


软引用案例



让我们细看一下细节。打印gc详情

所以在处理一些不重要的对象的时候,就可以通过软引用来降低内存压力。

软引用_引用队列

弱引用对象

弱引用一般会发生在垃圾回收的时候,当内存紧张时,就会回收弱引用对象,同时也会回收弱引用自身。

回收算法

标记清除算法

首先有两步

  • 先标记哪些对象可以是垃圾
  • 然后清除垃圾回收对象(这里的腾出内存空间并不是将内存空间的字节清 0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存。)

缺点容易产生大量的内存碎片 ,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢。

标记整理

会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整理移动,需要消耗一定的时间,所以效率较低。

复制回收

  • 第一步,分两个区,From区和 To区
  • 第二步,将From区存活的对象复制到To区,完成碎片的整理
  • 第三步,交换From区和To区,使To区总是空闲的一块空间

复制算法:将内存分为等大小的两个区域,FROMTO (TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间

分代垃圾回收

长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),在内存不足的时候也可能回收,用完即可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快):

堆内存大致分为两块:

新生代、老年代;新生代又划分为伊甸园、幸存区from、幸存区to。

长时间使用的对象放在老年代中,用完了就丢弃的对象放在新生代中。根据对象生命周期的不同特点,采用不同的垃圾回收算法,老年代发生次数少,新生代比较频繁。不同区域采用不同算法,更有效的对垃圾回收进行管理

分代垃圾回收怎么工作:

java虚拟机本身就是一个小的操作系统吧?

  • 新创建的对象首先分配在 伊甸园 区;
  • 伊甸园 区空间不足时,触发 minor gc ,使用可达性分析算法沿着GC root引用链去找,进行标记的动作;
  • 采用复制算法将 伊甸园 区 和幸存区from 区存活的对象复制到幸存区to 中,存活的对象年龄加一,然后交换 from to(这里很细节,实际变的不是两块物理地址,而是指针引用);
  • minor gc 会引发 stop the world,因为垃圾回收的时候对象地址发生改变,如果其他线程也在运行,会造成混乱。暂停其他线程,等垃圾回收结束后,恢复用户线程运行;
  • 新生代触发的STW的时间较短
  • 每次minorGC后,eden和from对象都清除干净了
  • 每次都把不需要回收和幸存区from留下的移到To中,然后交换From和To的位置
  • 当幸存区to对象的寿命超过阈值时,会晋升到老年代,最大寿命是15(4bit)
  • 相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
  • 如果老年代空间不足时,会先尝试触发 minor gc,如果空间仍然不足,那么就触发 full fc ,STW停止的时间更长!
  • from和to每次都要留一个空的,空间不足就触发gc,还不足就会触发提前晋升老年代,老年代如果放不下先触发full gc 然后再尝试提前晋升,还不行就Java heap space outofmemoryerror异常

GC

相关参数

**含义 ** 参数

堆初始大小 -Xms

堆最大大小 -Xmx 或 -XX:MaxHeapSize=size

新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )

幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy

幸存区比例 -XX:SurvivorRatio=ratio

晋升阈值 -XX:MaxTenuringThreshold=threshold

晋升详情 -XX:+PrintTenuringDistribution

GC详情 -XX:+PrintGCDetails -verbose:gc

FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

分析

  • 程序刚加载时的堆内存情况:
  • 代码分析

通过如下代码,给 list 分配内存,来观察 新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况,什么时候 幸存区放不下,直接晋升老年代,使用前需要设置 jvm 参数。

java 复制代码
public class Code_10_GCTest {

    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
    }
}
  • 大对象处理策略:

当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

  • 线程内存溢出:

某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行

这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常。

垃圾回收器

相关概念:

在谈论垃圾收集器的上下文语境中, 它们可以理解为:

并行(Parallel) :

并行描述的是多条垃圾收集器线程之间的关系, 说明同一时间有多条这样的线程在协同工作, 通常默认此时用户线程是处于等待状态。

并发(Concurrent) :

并发描述的是垃圾收集器线程与用户线程之间的关系, 说明同一时间垃圾收集器线程与用户线程都在运行(不一定是并行的可能会交替执行)。 由于用户线程并未被冻结, 所以程序仍然能响应服务请求, 但由于垃圾收集器线程占用了一部分系统资源, 此时应用程序的处理的吞吐量将受到一定影响。

吞吐量:

即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。

1)串行

单线程

适用于堆内存较小,如个人电脑(CPU核数较少也可以,因为单线程)

java 复制代码
-XX:+UseSerialGC=serial + serialOld

安全点:

让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

Serial 收集器:

Serial 收集器是最基本的、发展历史最悠久的收集器

特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!

ParNew 收集器:

  • ParNew 收集器其实就是 Serial 收集器的多线程版本
  • 除了Serial收集器外, 目前只有它能与CMS收集器配合工作。
  • 特点:多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题

Serial Old 收集器:

Serial Old 是 Serial 收集器的老年代版本

特点:同样是单线程收集器,采用标记-整理算法

在服务端模式下,它也可能有两种用途: 一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用, 另外一种就是作为CMS收集器发生失败时的后备预案, 在并发收集发生Concurrent Mode Failure时使用

吞吐量优先

parallel:并行的 Pause:停顿

  • 多线程
  • 适合堆内存较大,需要多核 cpu支持
  • 让单位时间内,STW(stop the world,停掉其他所有工作线程)时间最短
  • JDK1.8默认使用的垃圾回收器
java 复制代码
//第一个是新生代的垃圾回收器,复制算法;第二个是老年代的垃圾回收器,标记整理算法
//都是多线程的,只要开启一个,另外一个就会开启
//工作开启的回收线程数目,与cpu核数有关。回收时,cpu占有率100%
-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC  
-XX:+UseAdaptiveSizePolicy   //GC自适应调节策略,伊甸园和幸存区占比
-XX:GCTimeRatio=ratio  // 垃圾回收和总时间占比 1/(1+radio)
-XX:MaxGCPauseMillis=ms  //  垃圾回收暂停200ms
-XX:ParallelGCThreads=n   //回收线程数 控制

Parallel Scavenge 收集器:

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同, CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间, 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput) 。 所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值 。

与吞吐量关系密切,故也称为吞吐量优先收集器

特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)

GC自适应调节策略:

Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。

当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、

晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。

Parallel Scavenge 收集器使用两个参数控制吞吐量:

XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms):
回收器一般会让堆变小,减少每次停顿的时间

XX:GCTimeRatio=radio 垃圾收集时间占总时间的比率,相当于吞吐量的倒数
回收器一般会让堆变大,去减少垃圾回收次数,从而减少时间

Parallel Old 收集器

是 Parallel Scavenge 收集器的老年代版本

特点:多线程,采用标记-整理算法

响应时间优先

  • CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。
  • CMS作为老年代的收集器,新生代只能选择ParNew或者Serial收集器中的一个来配合工作。
  • 多线程
  • 适合堆内存较大,需要多核 cpu支持
  • 面向并发设计的程序都对处理器资源比较敏感。 在并发阶段, 它虽然不会导致用户线程停顿, 但却会因为占用了一部分线程(或者说处理器的计算能力) 而导致应用程序变慢, 降低总吞吐量。

参数设置:

java 复制代码
//第一个CMS是并发标记清除算法,用户线程与垃圾回收线程并发进行,老年代浮动垃圾过多,退化为 SerialOld
//第二个是parnew新生代多线程回收器
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

//第一个是并行的垃圾回收线程数,一般跟cpu核数一样;第二个是并发的垃圾回收线程数,一般设置为并行线程数的四分之一
//比如:核数为4,那就占用一个cpu核进行垃圾回收。其他用于用户线程
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads

//同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
//设置得太高将会很容易导致大量的并发失败产生,性能反而降低;
//设置太低将导致内存回收频率增加,性能降低
-XX:CMSInitiatingOccupancyFraction=percent

//重新标记前对新生代 先做一次垃圾回收(UseParNewGC),新生代存活对象少了,减轻重新标记的压力
-XX:+CMSScavengeBeforeRemark

CMS 收集器

Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器

特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务

CMS 收集器的运行过程分为下列4步:

初始标记:

标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。

并发标记:

并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行 。

重新标记:

为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题,这个阶段的停顿时间通常会比初始标记阶段稍长一些, 但也远比并发标记阶段的时间短。

并发清除:

并发清除阶段, 清理删除掉标记阶段判断的已经死亡的对象, 由于不需要移动存活对象, 所以这个阶段也是可以与用户线程同时并发的

浮动垃圾:

在CMS的并发标记和并发清理阶段, 用户线程是还在继续运行的, 程序在运行自然就还会伴随有新的垃圾对象不断产生, 但这一部分垃圾对象是出现在标记过程结束以后, CMS无法在当次收集中处理掉它们, 只好留待下一次垃圾收集时再清理掉。 这一部分垃圾就称为"浮动垃圾"。

并发失败:

浮动垃圾过多:

如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,很耗费时间,本来是响应优先的垃圾回收器,响应时间变得更长了,这也是CMS存在的问题。

内存碎片过多:

CMS是一款基于"标记-清除"算法实现的收集器, 这意味着收集结束时会有大量空间碎片产生。 空间碎片过多时, 将会给大对象分配带来很大麻烦, 往往会出现老年代还有很多剩余空间, 但就是无法找到足够大的连续空间来分配当前对象, 而不得不提前触发一次Full GC的情况。 退化为SerialOld,进行标记整理,很耗费时间,本来是响应优先的垃圾回收器,响应时间变得更长了,这也是CMS存在的问题。

由于在整个过程中耗时最长的并发标记和并发清除阶段中, 垃圾收集器线程都可以与用户线程一起工作, 所以从总体上来说, CMS收集器的内存回收过程是与用户线程一起并发执行的。

G1


相关参数:JDK8 并不是默认开启的,需要参数开启:

java 复制代码
// G1开关
-XX:+UseG1GC
// 所划分的每个堆内存大小:
-XX:G1HeapRegionSize=size
// 垃圾回收最大停顿时间
-XX:MaxGCPauseMillis=time

回收阶段


新生代伊甸园垃圾回收 ----->内存不足,新生代回收+并发标记 ----->回收新生代伊甸园、幸存区、老年代内存 ------>新生代伊甸园垃圾回收(重新开始)。

Young Collection + CM

学习视频:【黑马程序员JVM完整教程,Java虚拟机快速入门,全程干货不拖沓】 https://www.bilibili.com/video/BV1yE411Z7AP/?share_source=copy_web\&vd_source=fcae3ca58a4c2446a58b5aaacbaa4bbe

这个老师的JUC也很赞!!

相关推荐
Mephisto.java16 分钟前
【大数据学习 | HBASE高级】region split机制和策略
数据库·学习·hbase
Xiao Fei Xiangζั͡ޓއއ30 分钟前
一觉睡醒,全世界计算机水平下降100倍,而我却精通C语言——scanf函数
c语言·开发语言·笔记·程序人生·面试·蓝桥杯·学习方法
Bio Coder42 分钟前
学习用 Javascript、HTML、CSS 以及 Node.js 开发一个 uTools 插件,学习计划及其周期
javascript·学习·html·开发·utools
baijin_cha1 小时前
机器学习基础04_朴素贝叶斯分类&决策树分类
笔记·决策树·机器学习·分类
Allen zhu1 小时前
【PowerHarmony】电鸿蒙学习记录-准备工作
学习·华为·harmonyos
华清远见成都中心1 小时前
物联网学习路线来啦!
物联网·学习
hgy89691 小时前
Ekman理论回归
学习
波克比QWQ1 小时前
rust逆向初探
笔记·rust逆向
LuckyLay1 小时前
Spring学习笔记_36——@RequestMapping
java·spring boot·笔记·spring·mapping
坚硬果壳_1 小时前
《硬件架构的艺术》笔记(一):亚稳态
笔记·学习