详解Java垃圾回收机制

详解Java垃圾回收机制

一 对象被判定为垃圾的标准

  • 没有被其他对象引用

二 判定对象是否为垃圾的算法

2.1 引入计数算法

判断对象的引用数量

  1. 通过判断对象的引用数量来决定对象是否可以被回收
  2. 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
  3. 任何引用计数为0的对象实例可以被当作垃圾回收

优点:执行效率高,程序执行受影响较小

缺点:无法检测出循环引用的情况,导致内存泄漏

2.2 可达性分析算法

通过判断对象的引用链是否可达来决定对象是否被回收

  • 可达性算***对内存中的整个路线图进行遍历(从GC Root开始),回收器会将所有遍历到的对象标记为存活;当标记阶段完成以后所有的对象都已经被标记完成了,如果此时有没有被标记的对象回收器就会认为这些对象为垃圾进行回收;

  • 如上图所示,object1~object4对GC Root都是可达的,说明不可被回收,object5和object6对GC Root节点不可达,说明其可以被回收。

2.3 什么对象可以作为GC Root?

  1. 虚拟机栈中引用的对象;
  2. 方法区中常量引用的对象;
  3. 类静态属性引用的对象;
  4. 本地方法栈中JNI引用的对象;
  5. 活跃线程的引用对象;

三 谈谈你了解的垃圾回收算法

3.1 标记-清除算法(Mark and Sweep)

  • 标记 : 从根集合进行扫描,对存活的对象进行标记;
  • 清除 : 对堆内存进行从头到尾的线性遍历,回收不可达对象内存;
  • 缺点 : 导致大量的空间不连续,内存碎片化严重,垃圾清理完成后,造成很多内存空间不连续。后续可能发生大对象不能找到可利用的问题。

3.2 复制算法(Copy)

为了解决标记清除算法内存碎片化的缺陷而提出的算法。按照内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清理掉。

  • 分为对象面和空闲面;
  • 对象在对象面上创建;
  • 存活的对象在空间不足的情况下会在复制到空闲面中;
  • 最后将对象面中的对象一次性全部清空;

优点

  • 解决碎片化问题
  • 顺序分配内存,简单高效
  • 适用于对象存活率低的场景

缺点

  • 这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可以用内存被压缩到了原本的一半。且存活对象增多的话,copying算法的效率也大大降低

3.3 标记—整理算法(Compacting)

结合以上两个算法,为了避免缺陷而提出。标记阶段和标记清除算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清楚端边界的对象.

  • 标记 : 从根集合进行扫描,对存活对象进行标记;
  • 整理 : 移动所有存活对象,且按照内存地址依次排序然后回收所有空闲内存;

优点

  • 避免内存的不连续(标记清除算法
  • 不用设置两块内存互换(复制算法
  • 适用于存活率高的场景(MinorGC使用该算法

3.4 分代收集算法(Generationl Collector)

  • 是一套垃圾回收算法的组合;
  • 按照对象的声明周期划分成不同的区域,然后采用不同的算法进行回收;
  • 目的:提高JVM的回收效率

1 GC的分类

  1. Minor GC(轻GC)复制算法 + 空间交换;

    年轻代(也叫新生代),主要是用来存放新生的对象。新生代又细分为 Eden区、SurvivorFrom区、SurvivorTo区。

    • Minor GC操作后,Eden区如果仍然存活(判断的标准是被引用了,通过GC root进行可达性判断)的对象,将会被移到Survivor To区。而From区中,对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,可以通过**-XX:MaxTenuringThreshold来设置**,默认是15)的对象会被移动到年老代中,否则对象会被复制到“To”区。经过这次GC后,Eden区和From区已经被清空。
    • From”区和“To”区互换角色,原Survivor To成为下一次GC时的Survivor From区, 总之,GC后,都会保证Survivor To区是空的。

    对象如何晋升到老年代?

    • 经历一定Minor(轻GC)次数依然存活的对象
    • Survivor区中存放不下的对象
    • 新生成的大对象(可通过-XX"+PretenuerSizeThreshold控制大对象的大小)

    常用的调优参数

    • -XX:SurvivorRatio:Eden和Survivor的比值,默认8:1
    • -XX:NewRatio:老年代和年轻代内存大小的比例
  2. Full GC(重GC)标记-清理算法+标记-整理算法

    老年代

    • Full GC( 是清理整个堆空间—包括年轻代和老年代)和Major GC(老年代GC)
    • Full GC比Minor GC慢,但执行频率低

    触发Full GC的条件

    • 老年代空间不足
    • jdk7以下永久代空间不足;
    • Minor GC晋升到老年代的平均大小大于老年代的剩余空间
    • 调用System.gc()

2 常见概念

  1. Stop-the-World(STW)
    • JVM由于要执行GC而停止了应用程序的执行
    • 任何一种GC算法都会发生
    • 多数GC优化通过减少STW发生的时间来提高程序性能
  2. Safepoint
    • 分析过程中对象引用关系不好发生变化的点
    • 产生Safepoint的地方:方法调用;循环跳转;异常跳转等;
    • 安全点数量适中

四 常见的垃圾收集器(Garbage Collector)

4.1 JVM的运行模式

  • Server模式:针对桌面应用,加载速度比server模式快10%,而运行速度为server模式的10分之一。client下默认的堆容量 -Xms1M -Xmx64M

  • Client模式:针对服务器应用。server下默认的堆容量 -Xms128M -Xmx1024M

  • 区别:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升.原因是:****

    当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,服务起来之后,性能更高.

通过 java -version 查看JVM的默认运行模式。

4.2 垃圾收集器之间的联系

  • 新生代收集器

    Serial
    ParNew
    Parallel Scavenge

  • 老年代收集器

    Serial Old
    CMS
    Parallel Old

  • 堆内存垃圾收集器:G1

    每种垃圾收集器之间有连线,表示他们可以搭配使用。

4.3 年轻代常见的垃圾回收器

  • Serial收集器(-XX:UserSerialGC,复制算法)

    Serial 是一款用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial 进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World)。

    就比如妈妈在家打扫卫生的时候,肯定不会边打扫边让儿子往地上乱扔纸屑,否则一边制造垃圾,一遍清理垃圾,这活啥时候也干不完。

    • 单线程收集,进行垃圾收集时,必须暂停所有工作线程
    • 简单高效Client模式下默认的年轻代收集器

    适用场景:Client 模式(桌面应用);单核服务器。

    可以用 -XX:+UserSerialGC 来选择 Serial 作为新生代收集器。

  • ParNew收集器(-XX:+UserParNewGC,复制算法)

    ParNew 就是一个 Serial 的多线程版本,其它与Serial并无区别。ParNew 在单核 CPU 环境并不会比 Serial 收集器达到更好的效果,它默认开启的收集线程数和 CPU 数量一致,可以通过 -XX:ParallelGCThreads 来设置垃圾收集的线程数。

    • 多线程收集,其余的行为、特点和Serial收集器一样
    • 单核执行效率不如Serial,在多核下执行才有优势

    适用场景:多核服务器;与 CMS 收集器搭配使用。

    新生代收集器默认就是 ParNew,也可以用 -XX:+UseParNewGC 来指定使用 ParNew 作为新生代收集器。

  • Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)

    Parallel Scavenge 也是一款用于新生代的多线程收集器,与 ParNew 的不同之处是ParNew 的目标是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge 的目标是达到一个可控制的吞吐量

    吞吐量就是 CPU 执行用户线程的的时间与 CPU 执行总时间的比值【吞吐量 = 运行用户代代码时间/(运行用户代码时间+垃圾收集时间)】,比如虚拟机一共运行了 100 分钟,其中垃圾收集花费了 1 分钟,那吞吐量就是 99% 。比如下面两个场景,垃圾收集器每 100 秒收集一次,每次停顿 10 秒,和垃圾收集器每 50 秒收集一次,每次停顿时间 7 秒,虽然后者每次停顿时间变短了,但是总体吞吐量变低了,CPU 总体利用率变低了。

    • 比起关注用户线程停顿时间,更关注系统的吞吐量
    • 多核下执行才有优势,Server模式下默认的年轻代收集器

    适用场景:注重吞吐量,高效利用 CPU,需要高效运算且不需要太多交互。

    可以使用 -XX:+UseParallelGC 来选择 Parallel Scavenge 作为新生代收集器,jdk7、jdk8 默认使用 Parallel Scavenge 作为新生代收集器。

4.4 老牛代常见的垃圾收集器

  • Serial Old收集器(-XX:+UserSerialOldGC,标记-整理算法)

    Serial Old 收集器是 Serial 的老年代版本,同样是一个单线程收集器,采用标记-整理算法。

    • 单线程手机,进行垃圾收集时,必须暂停所有工作线程
    • 简单高效,Client模式下默认的老年代收集器

    适用场景:Client 模式(桌面应用);单核服务器;与 Parallel Scavenge 收集器搭配;作为 CMS 收集器的后备预案。

  • Parallel Old 收集器(-XX:UseParallelOldGC,标记-整理算法)

    Parallel Old 收集器是 Parallel Scavenge 的老年代版本,是一个多线程收集器,采用标记-整理算法。可以与 Parallel Scavenge 收集器搭配,可以充分利用多核 CPU 的计算能力。

    • 多线程,吞吐量优先

    适用场景:与Parallel Scavenge 收集器搭配使用;注重吞吐量。jdk7、jdk8 默认使用该收集器作为老年代收集器,使用 -XX:+UseParallelOldGC 来指定使用 Paralle Old 收集器。

  • CMS(Concurrent Mark Sweep) 收集器(-XX:+UserConcMarkSweepGC,标记-清除算法)

    初始标记:标记一下 GC Roots 能直接关联到的对象,速度较快。(STW

    并发标记:进行 GC Roots Tracing,标记出全部的垃圾对象,耗时较长

    重新标记:修正并发标记阶段引用户程序继续运行而导致变化的对象的标记记录,耗时较短。(STW)

    并发清除:用标记-清除算法清除垃圾对象,耗时较长

    CMS 收集器也存在一些缺点

    1. 对 CPU 资源敏感:默认分配的垃圾收集线程数为(CPU 数+3)/4,随着 CPU 数量下降,占用 CPU 资源越多,吞吐量越小
    2. 无法处理浮动垃圾:在并发清理阶段,由于用户线程还在运行,还会不断产生新的垃圾,CMS 收集器无法在当次收集中清除这部分垃圾。同时由于在垃圾收集阶段用户线程也在并发执行,CMS 收集器不能像其他收集器那样等老年代被填满时再进行收集,需要预留一部分空间提供用户线程运行使用。当 CMS 运行时,预留的内存空间无法满足用户线程的需要,就会出现 “ Concurrent Mode Failure ”的错误,这时将会启动后备预案,临时用 Serial Old 来重新进行老年代的垃圾收集。
    3. 因为 CMS 是基于标记-清除算法,所以垃圾回收后会产生空间碎片,可以通过 -XX:UserCMSCompactAtFullCollection 开启碎片整理(默认开启),在 CMS 进行 Full GC 之前,会进行内存碎片的整理。还可以用 -XX:CMSFullGCsBeforeCompaction 设置执行多少次不压缩(不进行碎片整理)的 Full GC 之后,跟着来一次带压缩(碎片整理)的 Full GC。

    适用场景:重视服务器响应速度,要求系统停顿时间最短。可以使用 -XX:+UserConMarkSweepGC 来选择 CMS 作为老年代收集器.

  • Garbage First(G1收集器)(-XX:UserG1GC,复制+标记+整理算法)

    G1 收集器是 jdk1.7 才正式引用的商用收集器,现在已经成为 jdk9 默认的收集器。前面几款收集器收集的范围都是新生代或者老年代,G1 进行垃圾收集的范围是整个堆内存,它采用 “ 化整为零 ” 的思路,把整个堆内存划分为多个大小相等的独立区域(Region),在 G1 收集器中还保留着新生代和老年代的概念,它们分别都是一部分 Region,如下图:

    Garbage First收集器

    • 将整个Java堆内存划分为多个大小相等的独立区域(Region)
    • 年轻代和老年代不再物理隔离

    特点:

    • 并行和并发
    • 分代收集
    • 空间整合
    • 可预测的停顿
  • JVM垃圾收集器总结

    本文主要介绍了JVM中的垃圾回收器,主要包括串行回收器、并行回收器以及CMS回收器、G1回收器。他们各自都有优缺点,通常来说你需要根据你的业务,进行基于垃圾回收器的性能测试,然后再做选择。下面给出配置回收器时,经常使用的参数:

    -XX:+UseSerialGC:在新生代和老年代使用串行收集器

    -XX:+UseParNewGC:在新生代使用并行收集器

    -XX:+UseParallelGC :新生代使用并行回收收集器,更加关注吞吐量

    -XX:+UseParallelOldGC:老年代使用并行回收收集器

    -XX:ParallelGCThreads:设置用于垃圾回收的线程数

    -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器

    -XX:ParallelCMSThreads:设定CMS的线程数量

    -XX:+UseG1GC:启用G1垃圾回收器

五 常见问题解析

  • Object的finalize()方法的作用是否与C++的析构函数相同?

    1. 不相同,析构函数调用确定,finalize()不确定;
    2. 将未被引用的对象放置于F-Queue队列中;
    3. finalize()方法执行随时可能会被终止;
    4. 给与对象最后一次重生的机会;
  • Java中的强引用 弱引用 软引用 虚引用有什么用?

    • 强引用(Strong Reference)

      1. 是最普遍的引用 : 例如 Object obj = new Object();
      2. 在OOM的时候,强引用的对象也不会被回收;
      3. 通过将对象设置为null来弱化引用,使其被回收;
    • 软引用(Soft Reference)

      1. 表示对象处在有用但非必须的状态;
      2. 只有当内存空间不足时,GC会回收该引用的对象的内存;
      3. 可以用来实现高速缓存;
      String str = new String("abc");//强引用
      SoftReference<String> softRef = new SoftReference<>(str);//软引用
      
    • 弱引用(Weak Reference)

      1. 非必须的对象,比软引用更弱一些;
      2. GC时会被回收;
      3. 被回收的概率也不大,因为GC线程的优先级比较低;
      4. 适用于引用偶尔被使用且不影响垃圾回收的对象;
      String str = new String("abc");//强引用
      WeakReference<String> weakRef = new WeakReference<>(str);//弱引用
      
    • 虚引用(Phantom Reference)

      1. 不会决定对象的声明周期;
        2. 任何时候都可能被垃圾收集器回收;
        3. 跟踪对象被垃圾收集器回收的活动,起哨兵作用;
        4. 必须和引用队列ReferenceQueue联合使用;

      String str = new String("abc");//强引用
      ReferenceQueue<Object> queue = new ReferenceQueue<>();
      PhantomReference<String> phantomRef = new PhantomReference<>(str, queue);//虚引用
      
  • 引用总结:强引用 > 软引用 > 弱引用 > 虚引用

  • 引用队列是什么?

    1. 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达;
    2. 存储关联的且被GC的软引用,弱引用以及虚引用;

    效果:引用队列可以配合软引用、弱引用及幽灵引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列中。

    应用:通过引用队列可以了解JVM垃圾回收情况

    ReferenceQueue<String> rq = new ReferenceQueue<String>();
    
    SoftReference<String> sr = new SoftReference<String>(new String("Soft"), rq);// 软引用
    
    WeakReference<String> wr = new WeakReference<String>(new String("Weak"), rq);// 弱引用
    
    PhantomReference<String> pr = new PhantomReference<String>(new String("Phantom"), rq);// 幽灵引用
    
    Reference<?> reference = rq.poll();// 从引用队列中弹出一个对象引用