垃圾回收内容总结

155 阅读17分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情

1、垃圾

在进行垃圾回收之前,首先要明确,哪些对象是垃圾?

1、垃圾

在进行垃圾回收之前,首先要明确,哪些对象是垃圾?

没有任何引用指向的一个对象或多个对象(循环引用)

这里需要注意,一般情况下,我们讨论的垃圾回收是针对堆和方法区的。因为:

程序计数器,虚拟机站,本地方法栈随着线程而生,随着线程而灭。每一个栈帧中分配多少内存基本上再类结构确定下来时就是已知的了,因此这几个区域内存分配和回收具备确定性,在这个几个区域内不需要多考虑如何回收的问题,当方法结束或线程结束的时候,内存自然就跟着回收了。

而Java堆和方法区有着不确定性。这里面讲一下堆和方法区存储什么东西,可以发现,只有在运行期间,我们才知道程序究竟会创建哪些对象,创建多少个个对象,这部分内存的分配和回收是动态的

2、如何找到垃圾

2.1 引用计数法

在每个对象中添加一个引用计数器,用来统计指向该对象的引用个数。有一个地方引用它时,计数器的值加一;引用失效,就减一;引用计数器为0的时候说明该对象是垃圾了。

优点:原理简单,判定效率高

缺点:

1、需要额外的内存空间存储计数器

2、有频繁的更新操作

3、无法找到循环引用的垃圾对象

使用场景:微软COM技术,使用ActionScript3的FlashPlayer,python和游戏脚本领域得到很多应用的Squirrel都用它来管理内存

2.2可达性分析算法

将根对象(GC Roots)作为初始的存活对象集,从该集合触发,搜索所有能被该集合引用到的对象,并且把它们加进去集合中,而无法被搜索到的对象就是死亡的,会被GC。

其中根对象包括但不限于:1、Java方法栈帧中的局部变量。2、已加载类的静态变量。3、JNI指针。4、已启动但并没有停止的Java线程

优点:弥补了引用计数法的缺点,可以找到循环引用的对象

缺点:

1、多线程环境下,如果一个线程A中的对象已经被GC,但是线程B启动正好要调用a,那么JVM会崩溃

2、假设A有对象a,线程B一直在运行没有关闭,但已经调用完a,此时a不会被GC,只能在下一次被GC

应用场景:Java,C#

3、GC算法

不移动对象的停顿时间更短,甚至不需要停顿,但是从整个程序的吞吐量来看,移动对象更划算

1、标记清除法

分为两个阶段

标记阶段:遍历所有对象,将所有活动对象打上标记,一般用根可达算法

清除阶段:遍历堆,将没有标记的对象释放掉

优点:算法简单,适合存活对象较多的内存区域,比如老年区

缺点:

一、执行效率不稳定,它的执行效率会随着需要回收的垃圾而改变

二、内存碎片化问题

应用场景:只有小部分对象需要进行回收的老年代,因为年轻代垃圾比较多,执行效率会降低很多。

2、标记复制法

将内存一分为二,每次用一块内存,发生GC时,将A部分存活的对象复制到B中,将A全部清理。

优点:

一、没有碎片化内存,

二、相较于标记清除法只需要遍历一次,不需要遍历堆,所以针对存活对象少的内存执行效率高。

三、缓存时防止重复读取数据:由于现在对象和子对象放在一起,缓存的时候就可以一起读取,防止重复读取数据。

缺点:

一、浪费1/2堆空间

二、存活对象多的时候,复制和移动对象非常耗时

三、需要担保机制

应用场景:只有小部分对象存活的年轻代。

3、标记压缩法

先进行标记,找到存活的对象和垃圾,把存活的对象放到内存空间的一端,垃圾回收的时候直接清理掉边界以外的内存

优点:它融合了标记清楚和复制算法的优点

一、没有碎片化,

二、不需要额外内存空间

缺点:效率低。因为它标记阶段需要遍历所有对象,压缩阶段根据算法的不同,需要遍历2-3次堆,而创建对象分配内存的操作和复制算法一样

应用场景:内存吃紧,又要避免空间碎片的场景,老年代想要避免空间碎片问题的话通常会使用标记整理法。

为什么新生代使用标记标记复制算法而非其他算法?

首先新生代的特征是对象生命周期短,有大量垃圾,所以需要进行频繁的YGC,这时候GC算法需要的是更快的执行效率。面对大量的垃圾,标记清除算法的执行效率就会变低,GC时间边长,而标记复制算法使用时间换空间的策略,尽管需要复制,但是相对于标记清除算法效率更高。而标记整理算法相对于标记复制算法多了三次遍历堆操作,所以效率慢很多。

4、内存分代模型

在内存中,有一些对象,生的快死的快,有一些对象能活很久,就是因为这两种对象的特征不同,所以考虑把内存分为两块,分别构造出不同的GC算法,这样效率会高很多(分而治之)。

把存活时间比较久的对象,分到一块儿,也就是老年代,因为这里存活的对象很多,垃圾很少,所以把垃圾标记出来直接清理就行。也就是标记清除法。但是标记清除法存在内存碎片化的问题,如果内存碎片化已经大到影响对象分配了,那么就使用标记整理法收集一次,以获得较为完整的内存。比如CMS就是这样一种机制。以上就涉及到响应时间和吞吐量之间的权衡问题。

把照生夕死的对象,分到一块儿,为新生代,这里能一直存活的对象很少,垃圾很多,如果用标记清除法的话需要清除的对象很多,执行效率很低,所以用标记复制算法。那既然要用标记复制法,所以把新生代内存分为Eden和survivor区两部分,为了尽可能不浪费空间,我们希望不是1:1,我们希望survivor区尽可能小一些,,可是这又存在一个问题了,因为Survivor区域小,所以每当在Survivor区的时候,内存相对Eden来说满得快一些,所以进行YGC的次数会增加,所以想到了在survivor区切开来分成两部分,然后比例是8:1:1,这样任何时候的内存占用比例都是90%,而且频繁YGC的问题也能解决。

对象从出生到消亡的过程

一个对象优先分配到堆中的伊甸区,当伊甸区空间耗尽的时候触发一次MGC,而存活下来的对象,被送到S区(from指向),空间再次耗尽的时候,对From指向的S区和伊甸区进行MGC,并将存活的对象放入to指向的S区,然后交换from和to指针,以确保下次MGC的时候to指向的survivor区是空的。当经过多次MGC时,将年龄大的对象放入老年区,默认PS中5次,CMS6次,G115次,GC的age是4位,最大15,所以不能调,最大15,通过XX:MaxTenuringThreshold指定次数

5、垃圾回收器

在进行垃圾回收之前,首先要明确,哪些对象是垃圾?

没有任何引用指向的一个对象或多个对象(循环引用)

这里需要注意,一般情况下,我们讨论的垃圾回收是针对堆和方法区的。因为:

程序计数器,虚拟机站,本地方法栈随着线程而生,随着线程而灭。每一个栈帧中分配多少内存基本上再类结构确定下来时就是已知的了,因此这几个区域内存分配和回收具备确定性,在这个几个区域内不需要多考虑如何回收的问题,当方法结束或线程结束的时候,内存自然就跟着回收了。

而Java堆和方法区有着不确定性。这里面讲一下堆和方法区存储什么东西,可以发现,只有在运行期间,我们才知道程序究竟会创建哪些对象,创建多少个个对象,这部分内存的分配和回收是动态的

2、如何找到垃圾

2.1 引用计数法

在每个对象中添加一个引用计数器,用来统计指向该对象的引用个数。有一个地方引用它时,计数器的值加一;引用失效,就减一;引用计数器为0的时候说明该对象是垃圾了。

优点:原理简单,判定效率高

缺点:

1、需要额外的内存空间存储计数器

2、有频繁的更新操作

3、无法找到循环引用的垃圾对象

使用场景:微软COM技术,使用ActionScript3的FlashPlayer,python和游戏脚本领域得到很多应用的Squirrel都用它来管理内存

2.2可达性分析算法

将根对象(GC Roots)作为初始的存活对象集,从该集合触发,搜索所有能被该集合引用到的对象,并且把它们加进去集合中,而无法被搜索到的对象就是死亡的,会被GC。

其中根对象包括但不限于:1、Java方法栈帧中的局部变量。2、已加载类的静态变量。3、JNI指针。4、已启动但并没有停止的Java线程

优点:弥补了引用计数法的缺点,可以找到循环引用的对象

缺点:

1、多线程环境下,如果一个线程A中的对象已经被GC,但是线程B启动正好要调用a,那么JVM会崩溃

2、假设A有对象a,线程B一直在运行没有关闭,但已经调用完a,此时a不会被GC,只能在下一次被GC

应用场景:Java,C#

3、GC算法

不移动对象的停顿时间更短,甚至不需要停顿,但是从整个程序的吞吐量来看,移动对象更划算

1、标记清除法

分为两个阶段

标记阶段:遍历所有对象,将所有活动对象打上标记,一般用根可达算法

清除阶段:遍历堆,将没有标记的对象释放掉

优点:算法简单,适合存活对象较多的内存区域,比如老年区

缺点:

一、执行效率不稳定,它的执行效率会随着需要回收的垃圾而改变

二、内存碎片化问题

应用场景:只有小部分对象需要进行回收的老年代,因为年轻代垃圾比较多,执行效率会降低很多。

2、标记复制法

将内存一分为二,每次用一块内存,发生GC时,将A部分存活的对象复制到B中,将A全部清理。

优点:

一、没有碎片化内存,

二、相较于标记清除法只需要遍历一次,不需要遍历堆,所以针对存活对象少的内存执行效率高。

三、缓存时防止重复读取数据:由于现在对象和子对象放在一起,缓存的时候就可以一起读取,防止重复读取数据。

缺点:

一、浪费1/2堆空间

二、存活对象多的时候,复制和移动对象非常耗时

三、需要担保机制

应用场景:只有小部分对象存活的年轻代。

3、标记压缩法

先进行标记,找到存活的对象和垃圾,把存活的对象放到内存空间的一端,垃圾回收的时候直接清理掉边界以外的内存

优点:它融合了标记清楚和复制算法的优点

一、没有碎片化,

二、不需要额外内存空间

缺点:效率低。因为它标记阶段需要遍历所有对象,压缩阶段根据算法的不同,需要遍历2-3次堆,而创建对象分配内存的操作和复制算法一样

应用场景:内存吃紧,又要避免空间碎片的场景,老年代想要避免空间碎片问题的话通常会使用标记整理法。

为什么新生代使用标记标记复制算法而非其他算法?

首先新生代的特征是对象生命周期短,有大量垃圾,所以需要进行频繁的YGC,这时候GC算法需要的是更快的执行效率。面对大量的垃圾,标记清除算法的执行效率就会变低,GC时间边长,而标记复制算法使用时间换空间的策略,尽管需要复制,但是相对于标记清除算法效率更高。而标记整理算法相对于标记复制算法多了三次遍历堆操作,所以效率慢很多。

4、内存分代模型

在内存中,有一些对象,生的快死的快,有一些对象能活很久,就是因为这两种对象的特征不同,所以考虑把内存分为两块,分别构造出不同的GC算法,这样效率会高很多(分而治之)。

把存活时间比较久的对象,分到一块儿,也就是老年代,因为这里存活的对象很多,垃圾很少,所以把垃圾标记出来直接清理就行。也就是标记清除法。但是标记清除法存在内存碎片化的问题,如果内存碎片化已经大到影响对象分配了,那么就使用标记整理法收集一次,以获得较为完整的内存。比如CMS就是这样一种机制。以上就涉及到响应时间和吞吐量之间的权衡问题。

把照生夕死的对象,分到一块儿,为新生代,这里能一直存活的对象很少,垃圾很多,如果用标记清除法的话需要清除的对象很多,执行效率很低,所以用标记复制算法。那既然要用标记复制法,所以把新生代内存分为Eden和survivor区两部分,为了尽可能不浪费空间,我们希望不是1:1,我们希望survivor区尽可能小一些,,可是这又存在一个问题了,因为Survivor区域小,所以每当在Survivor区的时候,内存相对Eden来说满得快一些,所以进行YGC的次数会增加,所以想到了在survivor区切开来分成两部分,然后比例是8:1:1,这样任何时候的内存占用比例都是90%,而且频繁YGC的问题也能解决。

对象从出生到消亡的过程

一个对象优先分配到堆中的伊甸区,当伊甸区空间耗尽的时候触发一次MGC,而存活下来的对象,被送到S区(from指向),空间再次耗尽的时候,对From指向的S区和伊甸区进行MGC,并将存活的对象放入to指向的S区,然后交换from和to指针,以确保下次MGC的时候to指向的survivor区是空的。当经过多次MGC时,将年龄大的对象放入老年区,默认PS中5次,CMS6次,G115次,GC的age是4位,最大15,所以不能调,最大15,通过XX:MaxTenuringThreshold指定次数

5、垃圾回收器

垃圾回收器的使用和内存息息相关。刚开始内存只有几十兆的时候,单线程的Serial+Serial Old已经能够应付,STW时间可以接受。但是随着内存的增大,增大到上百兆到几个G的时候,相当于一个扫地机器人本来只需要清理一个宿舍就行,现在要清理重庆大学,Serial+Serial Old的组合因为STW时间太长不适合使用。这时候就开始使用Parallel Scavenge+Parallel Old组合多线程清理垃圾,PS和serial唯一的区别就是一个单线程清理垃圾,一个多线程。但是内存增加到几十G的时候,现在要清理沙坪坝区了,这时候即使多线程,STW时间依然很长,这时候就想能否边回收垃圾边运行内存?所以诞生了CMS。但是CMS它使用的是标记清除法,所以不可避免地会产生内存碎片化问题,并且由于本身的原理,它会存在浮动垃圾,而内存还在不断扩大,这个缺点会逐渐被放大,一旦老年代内存分配不下后,就会默认启动SO,STW时间直线上升。这时候G1出现,它吸取了CMS实现方法的教训,因为原本就是因为CMS导致内存碎片化问题,Serial Old处理太大的内存,导致STW时间延长,那么把内存分为一块儿一块儿region,将垃圾比较多的内存采用复制算法,这样效率极大地提升了。\image-20210821213406474.png)

垃圾回收器的使用和内存息息相关。刚开始内存只有几十兆的时候,单线程的Serial+Serial Old已经能够应付,STW时间可以接受。但是随着内存的增大,增大到上百兆到几个G的时候,相当于一个扫地机器人本来只需要清理一个宿舍就行,现在要清理重庆大学,Serial+Serial Old的组合因为STW时间太长不适合使用。这时候就开始使用Parallel Scavenge+Parallel Old组合多线程清理垃圾,PS和serial唯一的区别就是一个单线程清理垃圾,一个多线程。但是内存增加到几十G的时候,现在要清理沙坪坝区了,这时候即使多线程,STW时间依然很长,这时候就想能否边回收垃圾边运行内存?所以诞生了CMS。但是CMS它使用的是标记清除法,所以不可避免地会产生内存碎片化问题,并且由于本身的原理,它会存在浮动垃圾,而内存还在不断扩大,这个缺点会逐渐被放大,一旦老年代内存分配不下后,就会默认启动SO,STW时间直线上升。这时候G1出现,它吸取了CMS实现方法的教训,因为原本就是因为CMS导致内存碎片化问题,Serial Old处理太大的内存,导致STW时间延长,那么把内存分为一块儿一块儿region,将垃圾比较多的内存采用复制算法,这样效率极大地提升了。

Serial

单线程,年轻代,使用拷贝算法。

先停止线程运行,然后垃圾回收,结束后程序继续运行。线程停止到运行的时间也就是STW的时间就是停顿时间

Serial Old

单线程,老年代,整理算法。

Parallel Scavenge

和Serial相比,只是使用多线程清理垃圾

Parallel Old

搭配PS的