JVM学习——(三)垃圾回收那些事

一、如何确定一个对象是垃圾?

想要进行垃圾回收,得先知道什么样的对象是垃圾。

1.1 引用计数法

对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾。

如果一个对象没有任何指针对其引用,它就是垃圾。

弊端:如果AB相互持有引用,就会导致用于不能回收。

1.2 可达性分析

通过一系列名为“GC Roots”的对象作为起始点,从“GC Roots”对象开始向下搜索,如果一个对象到“GC Roots”没有任何引用链相连,说明此对象可以被回收。

能作为GC Root:类加载器、Thread、虚拟机的本地变量表、static成员、常量引用、本地方法栈的变量等。

二、垃圾收集算法

已经能确定一个对象为垃圾之后接下来要考虑的就是如何回收。

2.1 标记-清除(Mark-Sweep)

标记清除是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

  • 标记

找出内存中需要回收的对象,并且把它们标记出来。

此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时。

  • 清除

清除掉被标记需要回收的对象,释放出对应的内存空间。

缺点:
1)标记和清除两个过程都比较耗时,效率不高

2)标记清除之后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,

无法找到足够的连续内存,导致不得不提前触发另一次垃圾收集动作。

2.2 复制(Copying)

复制的算法,将内存划分为两块相等的区域,每次只使用其中一块,

当这一块的内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。

优点:不会产生内存碎片

缺点:这种算法的代价将内存缩小为原来的一半,空间利用率降低。
          对象存活率较高时就要执行更多的复制操作,效率将会变低。
 

2.3 标记-整理(Mark-Compact)

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,

而是让所有存活对象都向一端移动,然后直接清理掉边界外的内存。

 
 

2.4 分代收集

对于之前说的三种垃圾收集算法,堆内存到底应该采用哪一种?

Young区:复制算法。每次GC都有大批对象死去,只有少量存活,因此采用复制算法效率高
Old区:标记清除或标记整理。Old区对象存活时间较长,如果用复制则复制量更多,没有必要,不如做个标记再清理。
 

三、垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现了。

 

3.1 Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯一选择。
 
Serial收集器是一种单线程收集器,不仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,
更重要的是它在进行垃圾收集时需要暂停其他线程。
 
优点:简单高效,有很高的单线程收集效率。
缺点:收集过程中需要暂停其他线程。
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器
 
 

3.2 ParNew收集器

可以把这个收集器理解为Serial收集器的多线程版本。
 
优点:CPU多时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
使用范围:新生代
应用运行在Server模式下的虚拟机中首选的新生代收集器
 

3.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器。又是并行的多线程收集器,
看上去和ParNew一样,但是Parallel Scanvenge更关注 系统的吞吐量。
 
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集的时间)
比如虚拟机一共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=99/100=99%
吞吐量越大,意味着垃圾收集的时间越短,用户代码利用CPU资源的效率更高。
 
-XX:MaxGCPauseMillis   设制最大的垃圾收集停顿时间。
-XX:GCTimeRatio    直接设置吞吐量的大小。
 

3.4 Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用“标记-整理”算法,运行过程和Serial收集器一样。
 

3.5 Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法进行垃圾回收。
吞吐量优先。
 

3.6 CMS收集器

CMS收集器是一种以实现最短回收停顿时间为目标的收集器。
采用的是“标记-清除算法”,过程中分为4步:
(1)初始标记  标记GC Roots能关联到的对象  Stop The World-->速度很快
(2)并发标记  进行GC Roots Tracing ,不会Stop The World
(3)重新标记 修改并发标记因用户程序变动的内容 Stop The World
(4)并发清除 CMS concurrent sweep
由于整个过程中,并发标记和并发清除,收集器线程可以和用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是和用户线程一起并发地执行的。
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
 

3.7 G1收集器

G1收集器的特点:
并行与并发
分代收集(仍然保留了分代的概念)
空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
可预测的停顿(比CMS更先进的地方在于,能让使用者明确指定一个长度为M毫秒的时间片段,消耗在垃圾收集上的时间不得超过N毫秒)
 
使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。
 
G1的工作过程可以分为以下几步:
初始标记     标记GC Roots能够关联的对象。(需要暂停用户线程)
并发标记     从GC Roots进行可达性分析,找出存活的对象。(与用户线程并发执行)
最终标记     修正在并发标记阶段,因为用户程序的并发执行导致变动的数据。(需要暂停用户线程)
筛选回收     对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
 

3.8 垃圾收集器的分类和适用

串行收集器-->Serial和Serial Old
只能有一个垃圾回收线程执行,用户线程暂停。(适用于内存比较小的嵌入式设备)。
 
并行收集器[吞吐量优先]-->Parallel Scanvenge、Parallel Old
多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。(适用于科学计算、后台处理等弱交互场景)
 
并发收集器[停顿时间]-->CMS、G1
用户线程和垃圾收集线程同时执行(但并不是一定并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。(适用于对停顿时间有要求的场景,比如web)。
 

3.9 吞吐量与停顿时间

停顿时间->>垃圾收集器进行垃圾回收导致用户线程停顿的时间
吞吐量->>运行用户代码的时间/(运行用户代码+垃圾收集时间)
 
停顿时间越短就越适合需要和用户交互的程序,更快的响应速度提升用户体验;
高吞吐量则可以高效的利用CPU,尽快完成程序的运算任务,适合在后台运算但不需要太多交互的任务。
 
其实调优也是这两个指标更加符合应用的需要。


3.10 如何选择合适的垃圾收集器

优先调整堆的大小,让服务器自己来选择
如果内存小于100M,使用串行收集器
如果是单核,而且没有停顿时间的要求,使用串行或JVM自己选择
如果运行停顿时间超过有1秒,选择并行或JVM自己选
如果想要时间最重要,而且不能超过1秒,使用并发收集器
 
注意:这里并行指的是GC线程同时执行,用户线程会停顿。
          并发指的是用户线程和GC线程同时执行,用户线程不停顿。
 

3.11 再看G1

G1收集器从JDK7开始使用,JDK8非常成熟,JDK9默认的垃圾收集器,适用于新老生代。
什么情况需要使用G1收集器?
1)50%以上的堆被存活对象占用
2)对象分配和晋升老年代的速度辩护非常大
3)垃圾回收时间比较长
 

3.12 开启需要的垃圾收集器的参数

1)串行
-XX:+UseSerialGC
-XX:+UseSerialOldGC
2)并行(吞吐量优先):
-XX:+UseParallelGC
-XX:+UseParallelOldGC
3)并发收集器(响应时间优先)
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
 
发布了25 篇原创文章 · 获赞 4 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_41570691/article/details/105377633