写在前面
在上一部分的学习中,我们对 JVM 的基础概念、JVM运行时内存、类加载机制有了基本的了解。
下面我们开始对于 JVM 的重点----GC,来进行学习。
6.5 GC中,如何判断对象存活、对象引用
什么是垃圾
很好理解,当一个对象没有引用指向他的时候,他就是垃圾。
判断对象是否存活的算法
为了判断一个对象是否存活,有多种算法。
引用计数算法
顾名思义,对一个对象,我们统计其引用的个数,进行计数。当引用次数为0的时候,即可以判断他变成垃圾了。
引用计数器算法很直观,也很简单。但是并没有在 Java 中使用,因为其不能解决 对象间循环引用 的问题。
// 如果使用引用计数算法,GC无法回收循环引用的情况
ObjA.instance = ObjB
ObjB.instance = ObjA
可达性分析算法(根搜索算法)
既然引用搜索算法无法满足要求,那么 java 就采用了另外一种算法----可达性分析算法。
可达性分析算法中,从根节点GC ROOT 出发,向下搜索引用链,对象如果不可被搜索到(不可达),就是垃圾。
那么什么样的对象可以看做是 GC ROOT 节点呢?
- 栈中引用的对象(局部对象)
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈引用的对象
总结一下,栈和方法区引用的对象。
两个栈(JVM栈和本地方法栈)引用的对象,方法区中 类静态属性、常量 引用的对象
再谈对象引用
这里判断对象是否存活,都是跟 “引用” 息息相关的。而传统意义上的引用,就只有被引用,和没有被引用两种状态。
而很多时候我们需要描述一些,内存空间足够则保留,内存不够则抛弃的对象,则需要多种的引用形式。
Java中同样给出了多种引用形式,下面列出来的引用依次变弱。
-
强引用
就是传统意义上理解的引用。 Object obj = new Object() 这种引用。只要强引用存在,就一定不会回收 -
软引用
软引用用来描述 有用,非必须的元素
GC第一次收集之后,软引用对象存在。但是如果内存还是不够(要内存溢出),则会触发GC第二次回收,此时,软引用对象就会被回收。
这里给一个应用场景来理解一下:
我们要实现一个缓存的功能,当内存足够的时候,通过一个软引用对象来取值,而不是直接从繁忙的真实数据来源来取值。内存是在不够了,这个软引用对象被回收,再从数据来源来直接取值。
-
弱引用
弱引用描述 非必须的对象。
弱引用的对象,在下一次垃圾GC时候被回收。弱引用主要用来监控对象是否已经被GC标记为即将回收(下一次GC)的垃圾。弱引用的 isEnQueued() 方法就可以返回该对象是否要被下次回收。 -
虚引用
唯一目的,就是在对象被GC回收时候,收到一个系统通知。
后面三者都有自己对应的实现类。 SoftReference/ WeakReference/ PhantomReference 类。
6.6 分代垃圾回收
上面一小节我们了解了对于一个对象,JVM是如何判断其是否已经变成垃圾了。那么变成垃圾之后呢?当然是要 GC 了。
目前的垃圾回收,核心都是分代垃圾回收机制。要理解分代回收机制。首先就要明白分代对于内存的划分。
这一部分在下面一小节对于 分代收集算法中,有详细的解释。
6.7 典型的垃圾收集算法
在上面一小节“分代垃圾回收”的基本思想指导下,我们来学习一下目前典型的垃圾收集算法。
mark-sweep标记清除算法
mark-sweep算法是最简单直接的垃圾收集算法,分为两个阶段:标记 + 清除。即标记出需要被回收的对象,再清除对象所占用的空间。
- 优点:实现简单。
- 缺点:容易产生内存碎片。
可能会导致大对象分配内存空间时,触发新一轮的GC。
Copying复制算法
将内存一分为二,只用一半。当这一半用完之后,将存活的对象放到另外一半,把这一半清除掉。
这么理解吧,有点类似于缓存的思想。
- 优点:没有内存碎片
- 缺点:内存只有一半。如果存活对象多的话,效率很低
Mark-Compact 标记整理算法
跟 mark-sweep 很类似。先标记,再把存活对象移动到一端,清理掉端边界以外的内存。
Generational Collection 分代收集算法
分代收集算法,比上面的三种GC算法复杂一点。
核心思想是:根据对象存活的生命周期,把内存进行划分。将heap堆分为老年代(Tenured Generation)和新生代(Young Generation)。老年代的特点是,每次GC时候,只有少量的对象需要被回收,新生代则是大量的对象。
新生代采用 Copying 算法,因为要回收的对象很多,存活的对象不多。新生代 = 1较大Eden + 2较小Survivor。 一般只使用一个Eden和Survivor。回收时候,将 Eden 和 Survivor存活的对象,复制到另外一个 Survivor,再清空前者。
那么基于对内存的划分,就可以在不同的内存区域,采用合适的GC算法。
对于老年代,采用 Mark-Compact 算法。
这里需要注意以下几个点:
MinorGC: 清理年轻代。 Major GC:清理老年代。FullGC:清理整个堆空间。
- 对象优先在 Eden 分配:
对象优先在 Eden 区域分配。当 Eden 没有足够空间,虚拟机进行一次 Minor GC - 大对象直接进入老年代
为了避免 Eden 区和两个 Survivor 区的频繁内存复制,大对象直接进入老年代。 - 长期存活的对象进入老年代
对象再 Eden 中出生,经过一次 Minor GC 后仍存活,被 Survivor接纳,其年龄被设为1,在 Survivor区每经历一个 Minor GC,年龄++,年龄增长到一定值,会被放到老年代中。 - 动态对象年龄判定
并不是一定要满足3中年龄达到阈值的情况,对象才可以进入老年代。
同年龄对象 总和 超过 survivor 空间内存的一半,这些对象就会进入老年代。 - 空间分配担保
首先,这里是有一个设置HandlePromotionFailure,来设定是否允许老年代来进行担保。
其次,这里的担保,是指 在 MinorGC 时候,可能遇到一个 Survivor 空间不够用(年轻代中存活对象很多,极端就是都存活)。这时候就需要老年代借用空间给年轻代,这就是担保。
而担保的前提是,老年代的空间也要够,但是实际上老年代并不知道这一次 MinorGC会存活多少对象,需要多少内存,所以就取一个之前每次均值作为比较对象,如果老年代现存对象比这个均值还要小,那么就需要执行 MajorGC 为年轻代腾出空间,做担保。
跟内存空间联系起来:堆空间 heap 被分为年轻代和老年代。年轻代又分为 eden + 2 survivor
上面说了新生代和老年代,都是在 heap 中的。实际上,在heap 之外还有一个永久代 Permanent Generation(就是方法区non-heap,在 HotSpot虚拟机中叫做永久代),存储 class 类、常量、方法描述等。对永久代的回收主要是回收 废弃常量、无用的类
可以看到,永久代回收的东西已经不是对象了,而是类和常量。那自然也就不在 heap 中了。
6.8 垃圾收集器
前面一个小节我们介绍了GC算法,那么这一个小节会聚焦于GC的具体实现----垃圾收集器。
首先是一张整体的图
垃圾收集器全家桶
Serial/
- 单线程。
- 新生代,Copying
- 简单高效;但是会给用户带来停顿。
因为没有线程间交互的开销,所以可以简单高效的进行垃圾收集,对于 Client 模式下的虚拟机是首选。一般是使用 Serial(新生代垃圾收集) + Serial Old(老年代垃圾收集)
ParNew
- Serial 的多线程版本。(二者的实现绝大部分都相同)
对于 Server 模式下的虚拟机是首选。因为除了 Serial之外,只有 ParNew 可以配合 CMS 收集器,实现并发收集(一边收集一边产生垃圾)。
Parallel Scavenge (平行捡破烂)
- 新生代,Copying。
- 多线程(并行收集)
目的:达到可控的吞吐量 ,高效利用CPU时间,尽快完成程序运算任务。
吞吐量:
其他的收集器,基本都是关注于,缩短垃圾回收带来的停顿时间,而 Parallel Scavenge 则关注于吞吐量。
停顿时间给交互带来了阻碍,而吞吐量则直接影响计算性能(CPU利用率)。所以 Parallel Scavenge 适合于用在后台运行,交互不多的任务
Serial Old 收集器
Serial 的老年代版本
- 单线程
- 老年代,Mark-Compact 标记整理算法。
也是主要用于 Client 模式下的虚拟机。跟前面说的一样。
Parallel Old
- Parallel Scavenge 老年代版本,Mark-Compact 标记整理算法
在注意吞吐量和CPU利用率时,Parallel Scavenge + Parallel Old。
CMS(Concurrent Mark Sweep)
目的:最短的垃圾收集停顿时间。所以一般用在网站服务器上,因为一般用户要求服务器获得最快的响应。
- 并发
- 老年代,标记清除(所有老年代中,唯一一个使用标记清除的)
下面主要了解一下,其垃圾回收的过程。分为4步:初始标记、并发标记、重新标记、并发清除
- 初始标记(initial mark)
标记GC ROOT能直接关联到的对象,速度很快。会停顿 - 并发标记(concurrent mark)
GC ROOT Tracing 的过程,是最耗时的。(是并发执行的,用户线程不停止,所以叫并发标记) - 重新标记(remark)
标记步骤2期间,程序继续执行产生变动的对象的记录。会停顿较长时间 - 并发清除(concurrent sweep)
并发标记、并发清除的 并发 是指垃圾处理进程和用户进程并发进行。初始标记、重新标记这里的并发,是指多个垃圾回收进程同时进行,而此时用户进程是暂停的。
优点:并发收集、低停顿
缺点:
- CMS收集器 对 CPU资源非常敏感。其实只要涉及并发设计的程序,都会对CPU资源敏感。在CPU数量较少的时候,整体性能被拖慢很多。
- CMS收集器 无法处理浮动垃圾。在并发清除时候,用户线程还是在运行,这时候产生的垃圾不会在本次垃圾回收被处理,称为浮动垃圾。
- CMS 空间碎片。由于采用 标记清除算法,会产生内存碎片,引发 新的垃圾收集。
- Concurrent Mode Failure 采用 SerialOld 作为老年代备用。
G1(Garbage First)
面向 服务端 应用的。标记整理算法。
G1 收集器将堆内存划分为一个个Region。同时维护了各个 Region垃圾堆积价值列表,优先回收价值大,代价小的 Region。
同时,为了避免全堆扫描,每一个 Region 都有一个对应的 Remembered Set。这个 Remembered Set 记录引用对象的信息,从而只要通过 Remembered Set 就可以找到相关对象,不用全堆扫描也不会遗漏。
- 并行与并发:利用多cpu缩短 stop-the-world停顿时间
- 分代收集:G1收集器同时运行于 新生代和老年代
- 空间整合:没有内存碎片
- 可预测的停顿:由于优先回收机制的存在,可以预测停顿时间。
垃圾回收的过程总结为:
- 初始标记
跟 CMS一样,先标记 GC ROOT 直接相关的对象。 - 并发标记
GC ROOT Tracing过程,并发执行。 - 最终标记
跟 CMS 一样 - 筛选回收
根据维护的回收价值列表,来优先回收。
可以看出,跟CMS来比,除了最后一步,都一样。
总结一下:
- 串行、并行、并发:
- 串行:Serial & Serial Old
- 并行:ParNew & Parallel Scavenge & Para Old
- 并发:CMS & G1
- 吞吐量优先和停顿时间优先
- 吞吐量优先:Para Scavenge & Para Old
- 停顿时间优先:CMS
client 模式: Serial + SerialOld
server 模式:ParNew + CMS
低交互模式:Para Scavenge + ParaOld
网友评论