深入理解Java虚拟机篇:GC垃圾回收机制总结

深入理解Java虚拟机篇:GC垃圾回收机制总结_第1张图片

文章目录

  • 一、 对象在内存中的状态
    • 1. 三种状态
    • 2. 可达性判断
  • 二、对象存活判断
    • 1. 引用计数
    • 2. 可达性分析
  • 三、Java引用的四种机制
  • 四、垃圾回收类型
  • 五、垃圾回收算法
    • 1. 标记-清除算法(Mark-Sweep)
    • 2. 复制算法
    • 3. 标记-压缩算法
    • 4. 分代收集算法(Generational Collection)★
  • 六、 垃圾回收器
    • 1. 串行(Serial)收集器
    • 2. parNew收集器
    • 3. Parallel Scanvenge收集器
    • 4. CMS收集器❤️
    • 5. G1收集器❤️
  • 七、内存分配与回收策略

一、 对象在内存中的状态

深入理解Java虚拟机篇:GC垃圾回收机制总结_第2张图片

1. 三种状态

1. 可达的
Java对象被创建后,如果被一个或者多个变量引用,就是可达的。
2. 可恢复的
Java对象不再被任何变量引用就会进入可恢复状态。
回收对象前调用finalize()方法让变量引用该对象,则可达,否则不可达。
ps:finalize()方法只能恢复一次。

注意⚠️:永远不要主动调用finalize方法,应该留给垃圾回收机制调用,而且具有不确定性,finalize可以使一个对象重新可达,此方法出现异常时,垃圾回收机制不会报告异常,程序继续执行。方法来自Object,protected void finalize()throw Throwable
参考资料:https://blog.csdn.net/qq_21927765/article/details/50809291

3. 不可达的
Java对象不被任何变量引用,且调用finalize()方法后仍然没有变成可达状态。
当对象处于不可达时,系统才会真正回收该对象占有的资源。

2. 可达性判断

  1. 单条引用路径及可达性判断:最弱的一个引用决定对象的可达性。
  2. 多条引用路径及可达性判断:几条路径中,最强的一条引用决定对象的可达性。

二、对象存活判断

深入理解Java虚拟机篇:GC垃圾回收机制总结_第3张图片

1. 引用计数

  • 概念:判断对象的引用数量。每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。
  • 优点:算法实现简单,判定效率高。
  • 缺点:无法解决对象相互循环引用的问题;引用和去引用需要计数记忆,影响性能。
    深入理解Java虚拟机篇:GC垃圾回收机制总结_第4张图片
public class ReferenceCountingGC {
      public Object instance = null;
       public static void testGC(){
       ReferenceCountingGC objA = new ReferenceCountingGC ();
       ReferenceCountingGC objB = new ReferenceCountingGC ();
            // 对象之间相互循环引用,对象objA和objB之间的引用计数永远不可能为0
            objB.instance = objA;
            objA.instance = objB;
            objA = null;
            objB = null;
            System.gc();
    }
}

解析:上述代码最后面两句将objA和objB赋值为null,也就是说objA和objB指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为 0,那么垃圾收集器就永远不会回收它们。
ps:所以,主流的java虚拟机中没有选用引用计数法来管理内存,主要原因就是它很难解决对象之间互相循环引用的问题

2. 可达性分析

可达性分析:判断对象的引用链是否可达。可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为 “GC Roots” 的对象作为起始点,从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可达的。

Java采取了“可达性分析”来判断对象是否存活,同样采用此算法的还有C#。
在Java语言中,GC Roots包括:

  1. 虚拟机栈(栈帧中的局部变量表)中引用的对象
  2. 方法区中类静态属性实体引用的对象
  3. 方法区中常量引用的对象(全局变量)
  4. 本地方法栈中JNI(Native方法)引用的对象。

三、Java引用的四种机制

深入理解Java虚拟机篇:GC垃圾回收机制总结_第5张图片
在JDK1.2之后,Java对引用的概念做了扩充,将引用分为:强引用(Strong Reference)、软引用(Weak Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。

  • 强引用
    强引用指在代码中普遍存在的,类似于“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾回收器就用永远不会回收掉被引用的对象实例。(GC Roots指向的引用都属于强引用
  • 软引用
    用来描述一些有用但不是必须的对象。(例如:缓存)
    对于仅被软引用指向的对象,在系统将要发生内存溢出之前,会将所有软引用对象进行垃圾回收。若内存够用,这些对象仍然保留
    在JDK1.2之后,提供理论SoftReference 来实现软引用。
  • 弱引用
    弱引用也是用来表示非必需对象的。但是它的强度要弱于软引用,被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾回收器开始工作时,无论当前内存是否够用,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供理论WeakReference 来实现弱引用。
  • 虚引用
    虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。
    一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时,收到一个系统通知。在JDK1.2之后,提供理论PhantomReference 来实现弱引用。

四、垃圾回收类型

深入理解Java虚拟机篇:GC垃圾回收机制总结_第6张图片

  • Minor GC:对新生代区进行回收。新生代java对象死亡频繁,所以minor GC也非常频繁,所以使用速度快、效率高的算法可以使得垃圾回收尽快完成。

  • Full GC:也叫Major GC,对整个堆进行回收,包括新生代、老年代。Full GC需要对整个堆回收,所以比Minor GC慢(因为需要stop-the-world,吞吐量下降,造成性能差),因此要尽量减少Full GC次数。触发GC的例子:老年代满了,System.gc()被显示调用。。。

五、垃圾回收算法

深入理解Java虚拟机篇:GC垃圾回收机制总结_第7张图片

1. 标记-清除算法(Mark-Sweep)

概念:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
过程:该算法首先从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收,如下图:
深入理解Java虚拟机篇:GC垃圾回收机制总结_第8张图片深入理解Java虚拟机篇:GC垃圾回收机制总结_第9张图片
缺点:标记和清除过程效率不高;空间问题,标记清除后会产生大量不连续碎片,可能会导致程序需要分配的较大对象找不到连续足够的内存而提前触发另一次垃圾收集动作。

2. 复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完,就将还存活着的对象复制到另一块上,然后再把已经使用的内存空间清理掉。
深入理解Java虚拟机篇:GC垃圾回收机制总结_第10张图片
优点:不用考虑内存碎片问题;实现简单,运行高效。适合新生代。
缺点:内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。不适合老年代。

目前商用的虚拟机都采用这种算法来回收新生代。研究发现,新生代中的对象每次回收都基本上只有10%左右的对象存活,所以需要复制的对象很少,效率还不错。新生代中Eden、survivor0、survivor1比例8:1:1,当回收时,将Eden和Survovor0中还存活的对象一次地复制到另外一块survivor空间上,最后清理掉Eden和Survivor0空间,索引只有10%的内存被浪费。
深入理解Java虚拟机篇:GC垃圾回收机制总结_第11张图片
新生代、老年代概念
1)新生代
新生代的目标就是尽可能快速的收集那些生命周期短的对象。其内存按8:1:1比例分为eden(伊甸区)、survivor0(幸存者0区)、survivor1(幸存者1区)。
1. 大部分对象在eden区生成,进行垃圾回收时,先将eden区存活对象复制到survivor0区,然后清空eden区。
2. 当survivor0也快满了,就将eden区和survivor0区对象复制到survovr1区,然后清空eden和survivor0区。
3. 这时survivor0为空,然后交换survivor0,survivor1角色(下次垃圾回收时会扫描eden、survivor1区),进行反复。
4. 如果出现survivor1放不下存活对象时,就将存活对象直接放老年代。
5. 如果老年代也满了就会触发一次FullGC(新生代、老年代都回收,新生代发生的GC也叫MinorGC、MinorGC发生频率比较高,不一定等eden区满了才触发)
2)老年代
老年代中存放的都是生命周期比较长的对象。老年代和新生代占内存比例约(2:1),当老年代满时会触发Major GC(Full GC),老年代对象存活时间比较长,因此FullGC发生的概率比较低。

3. 标记-压缩算法

首先标记出所有需要回收的对象,然后让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。适合老年代。

深入理解Java虚拟机篇:GC垃圾回收机制总结_第12张图片

4. 分代收集算法(Generational Collection)★

GC分代假设:大部分对象的生命周期非常短暂,存活时间短。
把java堆分为新生代和老年代。根据各个年代的特点采用合适的收集算法。

  • 新生代中,对象存活率低,则选用复制算法,只需付出少量存活对象的复制成本就可以完成收集。
  • 老年代中,对象存活率高,则选择“标记-清除”或者“标记-整理”算法来进行回收。

`目前分代收集算法使用较多

六、 垃圾回收器

深入理解Java虚拟机篇:GC垃圾回收机制总结_第13张图片

1. 串行(Serial)收集器

深入理解Java虚拟机篇:GC垃圾回收机制总结_第14张图片
最古老,稳定,效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法,老年代标记-压缩算法。垃圾收集过程中会服务暂停(stop-the-world)。
参数控制:-XX:+UseSerialGC 串行收集器

Stop the world:虚拟机在进行GC时,暂停其他所有的工作线程。

深入理解Java虚拟机篇:GC垃圾回收机制总结_第15张图片

2. parNew收集器

深入理解Java虚拟机篇:GC垃圾回收机制总结_第16张图片
Serial收集器的多线程版本。新生代并行,老年代串行。新生代复制算法、老年代标记-压缩算法。
参数控制:-XX:+UseParNewGC ParNew收集器;-XX:ParallelGCThreads限制线程数量

深入理解Java虚拟机篇:GC垃圾回收机制总结_第17张图片

并发和并行的区别

  • 并行(parallel):指多条垃圾手机线程并行工作,但是此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但是不一定并行,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另一个CPU上.

3. Parallel Scanvenge收集器

在这里插入图片描述
Parallel Scavenge类似parNew收集器,回收新生代对象,但是更关注系统的吞吐量。可通过参数来打开自适应调节策略,虚拟机会根据情况动态调整参数以提供最合适的停顿时间或者最大吞吐量。
参数控制:-XX:+UseParallelGC 使用Parallel收集器+老年代串行/老年代并行

Parallel old收集器,是parallel收集器的老年代版本,使用多线程“标记-整理”算法。JDK1.6开始提供。

4. CMS收集器❤️

深入理解Java虚拟机篇:GC垃圾回收机制总结_第18张图片
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法实现,也是唯一一个不会在GC时出现stop-the-world的收集器(标记过程不算)。

过程

  • 初始标记:仅标记GCRoot能够直接关联的对象,速度快且stop-the-world
  • 并发标记:标记所有可达对象,并且和用户线程一起并行执行
  • 重新标记:对于并发标记阶段用户线程产生的垃圾对象进行重新标记修正,这时stop-the-world
  • 并发清除:根据前面标记的结果清理垃圾对象,和用户线程一起并行执行
    深入理解Java虚拟机篇:GC垃圾回收机制总结_第19张图片
    优点:并发收集、低停顿
    缺点:
    1)对CPU资源非常敏感。虽然在并发阶段不会导致用户线程停顿,但是会因为占用一部分线程让应用程序变慢。
    2)无法处理浮动垃圾。 在最后一步的并发清除过程中,用户线程也会产生垃圾,这部分垃圾(浮动垃圾)需要等到下次gc才可以被清除。
    3)CMS收集器基于“标记-清除”法实现,会产生大量空间碎片(可通过参数设置解决内存碎片的合并整理,但这个过程会导致停顿时间变长),碎片过多会导致大对象没有合适连续空间分配,不得不提前触发一次FullGC。

CMS 出现FullGC的可能原因:
1、年轻代晋升到老年代没有足够的连续空间,很有可能是内存碎片导致的。
2、在并发过程中JVM觉得在并发过程结束之前堆就会满,需要提前触发FullGC。

5. G1收集器❤️

在这里插入图片描述 概念Garbage-First是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
步骤

  • 初始标记标记GC Root能够直接关联的对象,并修改TAMS(Next Top Mark Start)值,让下一阶段用户程序并发运行时可以在正确的可用Region中创建新对象,这个过程需要stop-the-world,但是停顿时间较短。
  • 并发标记:从GC Root开始对堆进行可达性分析,找出所有存活对象,这个过程耗时较长,可与用户线程并发执行。
  • 最终标记:修正并重新标记并发标记阶段用户线程产生的垃圾对象,JVM将这部分标记记录在线程Remembered Set中,这个阶段需要停顿线程,但是可并行执行。
  • 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户期待的GC停顿时间来制定回收计划,这个阶段也可以与用户线程并发执行,但是由于只回收一部分的Region,时间是可控的,而且停顿用户线程将大幅度提高收集效率。
    深入理解Java虚拟机篇:GC垃圾回收机制总结_第20张图片

G1的特点
1)并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行
2)分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。
3)空间整合✨:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片
4)可预测停顿✨:j降低停顿时间是G1、CMS的共同关注点,但是G1除此之外还可以建立可预测停顿时间模型,能让使用者明确指定一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1收集器将Java堆划分为多个大小相等的独立区域(Regin),虽然还保留了新生代和来年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合。
G1新生代收集跟ParNew类似,当新生代占用达到一定比例,开始出发收集。G1老年代收集和CMS类似,回收时对象会有短暂停顿(stop-the-world)。
深入理解Java虚拟机篇:GC垃圾回收机制总结_第21张图片

下面是一张GC收集器总结表:
深入理解Java虚拟机篇:GC垃圾回收机制总结_第22张图片

七、内存分配与回收策略

深入理解Java虚拟机篇:GC垃圾回收机制总结_第23张图片

  1. 对象优先分配在Eden区,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
  2. 大对象直接分配到老年区。比如长字符串、数组。
  3. 长期存活的对象将进入老年区。对象在新生代中经历一定次数(默认15)的Minor GC后,就会被晋升到老年代中。
  4. 动态对象年龄判断。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

参考文章:
https://blog.csdn.net/justloveyou_/article/details/71216049
https://www.jianshu.com/p/cc8395631acd
https://www.cnblogs.com/rgever/p/9534857.html

你可能感兴趣的:(JVM,java)