学堂 学堂 学堂公众号手机端

两篇文章带你搞懂GC垃圾回收之基础篇

lewis 1年前 (2024-03-28) 阅读数 4 #技术


文章目录​​1.JVM GC回收哪个区域的垃圾?​​​​2.判断对象可以回收的方法​​​​2.1 引用计数法​​​​2.2 可达分析算法​​​​2.3 什么对象可以当作GC Roots?​​​​虚拟机栈中的引用对象​​​​全局的静态的对象​​​​常量引用​​​​本地方法栈中JNI引用的对象​​​​3.垃圾回收算法​​​​3.1 标记清除算法​​​​3.2 复制算法​​​​3.3 标记压缩算法​​​​3.4 分代回收算法​​​​4.垃圾回收器​​​​理解什么是STW?​​​​4.1 Serial 和 Serial Old 回收器​​​​4.2 ParNew 回收器​​​​4.3 Parallel Scavenge 回收器​​​​4.4 Parallel Old 回收器​​​​4.5 CMS 回收器(重点)​​​​4.5.1 并发标记出现的问题:漏标和错标​​​​4.5.2 并发标记问题的解决:三色标记算法​​​​4.5.3 CMS巨大bug​​​​4.6 G1 回收器​​
1.JVM GC回收哪个区域的垃圾?

JVM GC只回收​​堆区和方法区内的对象​​。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。

2.判断对象可以回收的方法2.1 引用计数法

给对象中添加一个​​引用计数器​​,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。


它的优点是简单、高效,但是缺点也是异常明显:这个方法无法解决​​对象循环引用​​的问题

// 对象循环引用示例
Object objectA = new Object();
Object objectB = new Object();

objectA.instance = objectB;
objectB.instance = objectA;

objectA = null;
objectB = null;

假设我们有上面的代码。程序启动后,​​objectA​​​和​​objectB​​​两个对象被创建并在​​堆中分配内存​​​,它们都相互持有对方的引用,但是除了它们相互持有的引用之外,再无别的引用。而实际上,引用已经被置空,这两个对象不可能再被访问了,但是因为它们​​相互引用着对方​​​,​​导致它们的引用计数都不为0​​​,因此​​引用计数算法无法通知GC回收它们​​​,造成了​​内存的浪费​​。如下图:对象之间的引用形成一个有环图。

2.2 可达分析算法

基于引用计数法无法回收循环应用,我们就有了一种新的方法。

​可达分析算法​​​,或叫​​根搜索算法​​​,在主流的JVM中,都是使用的这种方法来判断对象是否存活的。这个算法的思路很简单,它把内存中的每一个对象都看作一个​​结点​​​,然后定义了一些可以作为​​根结点​​​的对象,我们称之为​​GC Roots​​。如果一个对象中有另一个对象的引用,那么就认这个对象有一条指向另一个对象的边。

像上面这张图,JVM会起一个线程从所有的​​GC Roots​​​开始往下遍历,当遍历完之后如果发现有一些对象不可到达,那么就认为这些对象已经没有用了,需要被​​回收​​。

上图红色为无用的节点,可以被回收。

需要注意的是:基本所有的​​GC算法​​​都引用​​根搜索算法​​这种概念。

2.3 什么对象可以当作GC Roots?

共有四种对象可以作为GC Roots

虚拟机栈中的引用对象

我们在程序中正常创建一个对象时,对象会在​​堆上开辟一块内存空间​​​,同时会将这块空间的地址作为​​引用​​​保存到虚拟机​​栈​​中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种对象可以作为GC Roots。

全局的静态的对象

也就是使用了​​static​​关键字定义了的对象,这种对象的引用保存在共有的方法区中,因为虚拟机栈是线程私有的,如果保存在栈里,就不叫全局了,很显然,这种对象是要作为GC Roots的。

常量引用

就是使用了​​static final​​关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也作为GC Roots。

本地方法栈中JNI引用的对象

有时候单纯的java代码不能满足我们的需求,就可能需要调用C或C++代码(java本身就是用C和C++写的嘛),因此会使用​​native方法​​​,JVM内存中专门有一块​​本地方法栈​​,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。

3.垃圾回收算法3.1 标记清除算法

​标记-清除算法​​​采用从根集合进行扫描,对​​存活​​​的对象进行​​标记​​,标记完毕后,再扫描整个空间中未被标记的对象进行直接回收,如上图。​​标记-清除算法​​​不需要进行对象的​​移动​​​,并且仅对不存活的对象进行处理,在存活的对象比较多的情况下极为​​高效​​​,但由于标记-清除算法直接回收不存活的对象,并没有对还存活的对象进行整理,因此会导致​​内存碎片​​。3.2 复制算法

复制算法将内存分为​​两个空间​​​,使用此算法时,所有动态分配的对象都只能分配在其中一个区间(活动区间),而另外一个区间(空闲区间)则是​​空闲​​的。复制算法采用​​从根集合扫描​​​,将存活的对象复制到空闲区间,当扫描完毕活动区间后,会将​​活动区间​​​一次性​​全部回收​​​。此时原本的​​空闲区间​​​变成了​​活动区间​​。下次GC时候又会重复刚才的操作,以此循环。复制算法在存活对象比较少的时候,极为高效,但是带来的成本是牺牲一半的​​内存空间​​用于进行对象的移动。所以复制算法的使用场景,必须是对象的存活率非常低才行,而且最重要的是,我们需要克服50%内存的浪费。3.3 标记压缩算法

​标记-压缩​​​算法采用和 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。​​标记-清除​​​ 算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此​​成本更高​​​,但却​​解决了内存碎片​​的问题。3.4 分代回收算法

根据存活对象划分几块内存区域,一般是分为​​新生代​​​和​​老年代​​。然后根据各个年代的特点制定相应的回收算法。

年轻代复制算法的具体应用:

生成空间好比就是​​eden​​​区,​​survivor​​分别是From幸存区、To幸存区,eden区会标记一些存活的对象拷贝到From区,然后清空eden区。

如果​​From区​​​和​​eden​​​区都有垃圾,就把​​eden区​​​和​​From区​​​都存活的对象全部拷贝到​​To区​​​,然后​​清空eden区和From区​

如果​​To区​​​和​​eden区​​​都有垃圾,就把​​eden区​​​和​​To区​​​都存活的对象全部拷贝到​​From区​​​,然后清空​​eden区​​​和​​To区​

反复循环,​​默认​​​来回​​循环15次​​​,如果活动的对象还是没有被垃圾回收器回收了,就存放到​​老年代​

新生代(new / Young)每次垃圾回收都有大量的对象死去,只有少量对象存活,选用​​复制算法​​比较合理。新生代回收可以称为:YGC老年代(old)老年代中对象的存活率较高,没有额外的空间分配对它进行担保,所以必须使用​​标记-清除​​​或者​​标记-压缩​​算法进行垃圾回收。老年代回收可以称为:MGC整体回收可以称为:FGC4.垃圾回收器理解什么是STW?在垃圾回收时,都会产生应用程序的停顿,停顿产生时,整个应用程序会被卡死,没有任何响应。java中Stop-The-World机制简称STW,是在​​执行垃圾收集算法时​​,Java应用程序的其他业务线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,​​全局停顿​​​,​​所有Java代码停止​​​,native代码可以执行,但不能与JVM交互;这些现象多半是由于​​GC​​引起。没有一种垃圾回收器不会产生​​STW​​​,就连​​CMS​​​这种多线程并发执行的回收器也会产生​​STW​​CMS产生​​STW​​​的阶段刚好就在​​初始标记​​​和​​重新标记​​阶段

说明:如果两个垃圾回收器之间存在连线说明他们之间是可以搭配使用的

4.1 Serial 和 Serial Old 回收器Serial分为Serial、Serial Old,其中​​Serial工作在年轻代​​​,​​Serial Old工作在老年代​​Serial是​​单线程​​的垃圾回收器,当垃圾回收线程开始的时候,业务线程必须暂停,直到垃圾回收线程结束​​Serial​​​使用的是​​复制算法​​​,而​​Serial Old​​​使用的是​​标记-压缩​​算法

4.2 ParNew 回收器ParNew可以认为是Serial的​​多线程​​版本ParNew是​​多线程并行​​​的,也就是说当​​多条垃圾回收线程​​​并行工作时,此时的​​业务线程​​​处于​​等待状态​

4.3 Parallel Scavenge 回收器Parallel Scavenge 是一个​​年轻代​​​的垃圾回收器,也就是说​​Paralle​​​l工作在​​年轻代​​Parallel Scavenge是​​多线程并行​​​的,也就是说当​​多条垃圾回收线程​​​并行工作时,此时的​​业务线程​​​处于​​等待状态​​采用​​复制算法​​实现​​JDK1.8默认​​采用的垃圾回收器:Parallel Scavenge、Parallel Old

4.4 Parallel Old 回收器Parallel Old 是 Parallel Scavenge 的老年代版本,也就是说​​Parallel Old​​​工作在​​老年代​​Parallel Old是​​多线程并行​​​的,也就是说当​​多条垃圾回收线程​​​并行工作时,此时的​​业务线程​​​处于​​等待状态​​采用​​标记-压缩​​算法

4.5 CMS 回收器(重点)CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。CMS是​​多线程并发​​​的,也就是说​​垃圾线程​​​和​​业务进程​​​是可以​​一起执行​​的采用​​标记-清除​​ 算法实现。运行步骤:初始标记(CMS initial mark):标记​​GC Roots​​ 能直接关联到的对象并发标记(CMS concurrent mark):进行 GC Roots Tracing重新标记(CMS remark):修正并发标记期间的变动部分这里要注意:重新标记就不能业务线程和垃圾线程一起执行了,也就是​​不能并发​​执行了并发清除(CMS concurrent sweep)

4.5.1 并发标记出现的问题:漏标和错标并发标记虽然说垃圾线程和业务线程一起执行,但是这种情况也会产生各种问题​​漏标​​:一个对象被GC标记为不是垃圾,但是随着业务的进行,该对象的引用消失了,就变成了垃圾,而这时垃圾回收并没有标记该对象为垃圾,这时候就会产生漏标的情况这就是著名的"浮动垃圾(floating garbage)"解决办法很简单,下次垃圾线程再循环的时候,该对象会被​​重新标记​​为垃圾,进行清理​​错标​​:一个对象被GC标记为垃圾,但是随着业务的进行,该对象被其他对象引用了,又变成不是垃圾了,而这时垃圾回收已经标记为该对象是垃圾了,这时候就会产生错标的情况

4.5.2 并发标记问题的解决:三色标记算法

漏标问题

某个状态下,黑色->灰色->白色

如果一切顺利,不发生任何引用变化,GC线程顺着灰色的引用向下扫描,最后都变成黑色,都是存活对象

但是如果出现了这样一个状况,在扫描到灰色的时候,还没有扫描到这个白色对象,此时,黑色对象引用了这个白色对象,而灰色对象指向了别人,或者干脆指向了null,也就是取消了对白色对象的引用

那么我们会发现一个问题,根据三色标记规则,GC会认为,黑色对象是本身已经被扫描过,并且它所有指向的引用都已经被扫描过,所以不会再去扫描它有哪些引用指向了哪些对象
然后,灰色对象因为取消了对白色对象的引用,所以后面GC开始扫描所有灰色对象的引用时候,也不会再扫描到白色对象

最后结果就是,白色对象直到本次标记扫描结束,也是白色,根据三色标记规则,认为它是垃圾,被清理掉

但是实际情况,它明显是被引用的对象,是绝对不能当做垃圾来清除的,因为漏标,最后被当作垃圾清理掉了

/*
漏标的两个充要条件:
1.有至少一个黑色对象在自己被标记之后指向了这个白色对象
2.所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用
这两个条件,必须全满足,才会造成漏标问题.
*/
CMS的解决方案就是​​重新标记​​​:将A变成​​灰色​​,问题解决

4.5.3 CMS巨大bug

记住一句话:没有任何一个jdk版本的默认垃圾回收器是cms


4.6 G1 回收器G1是面向服务端的垃圾回收器。优点:并行与并发、分代收集、空间整合、可预测停顿。运作步骤:初始标记(Initial Marking)并发标记(Concurrent Marking)最终标记(Final Marking)筛选回收(Live Data Counting and Evacuation)


版权声明

本文仅代表作者观点,不代表博信信息网立场。

热门