最近在极客时间上的《深入拆解Java虚拟机》课堂上学习,所以记录下了学习的笔记,与及对其相关的内容进行思考和拓展。
用来辨别的计算方法有两种: 引用计数法与可达性分析。
实现方式: 每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。
缺点:
1.需要额外的空间来存储计数器,以及繁琐的更新操作
2.引用计数法还有一个重大的漏洞,那便是无法处理循环引用对象。
实现方式: 这个是主流的垃圾回收器。这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
GC Roots 包括(但不限于)如下几种:
Java 方法栈桢中的局部变量;
已加载类的静态变量;
JNI handles;
已启动且未停止的 Java 线程。
缺点: 比如说,在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会直接导致 Java 虚拟机崩溃。
Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作。而详细怎样实现就不一一说了。总结来说就是,在每个方法一个安全点,然后调用一个方法,或者完成一个方法,就会触发这个安全点检查,如果安全点检查发现有需要停止进行垃圾回收的,线程就会先阻塞。而检查的颗度为方法,为什么不是每一个字节码,主要还是开销的问题。
当标记完所有的存活对象时,我们便可以进行死亡对象的回收工作了。主流的基础回收方式可分为三种。
实现方式: 即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。
缺点:
1)会造成内存碎片。由于 Java 虚拟机的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。
2)分配效率较低。如果是一块连续的内存空间,那么我们可以通过指针加法(pointer bumping)来做分配。而对于空闲列表,Java 虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。
实现方式: 把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题
缺点: 压缩算法的性能开销大,需要将大量对象迁移,过程消耗资源多,耗时大。
实现方式: 把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。复制这种回收方式同样能够解决内存碎片化的问题。
缺点: 空间的使用效率极其低下。说明只能有一半是可以利用,一般是空着的。
Java 虚拟机可以给不同代使用不同的回收算法。对于新生代,我们猜测大部分的 Java 对象只存活一小段时间,那么便可以频繁地采用耗时较短的垃圾回收算法,让大部分的垃圾都能够在新生代被回收掉。对于老年代,我们猜测大部分的垃圾已经在新生代中被回收了,而在老年代中的对象有大概率会继续存活。当真正触发针对老年代的回收时,则代表这个假设出错了,或者堆的空间已经耗尽了。
Java 虚拟机将堆划分为新生代和老年代。其中,新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区。如下图:
默认情况下,Java 虚拟机采取的是一种动态分配的策略(对应 Java 虚拟机参数 -XX:+UsePSAdaptiveSurvivorSizePolicy),根据生成对象的速率,以及 Survivor 区的使用情况动态调整 Eden 区和 Survivor 区的比例。
-XX:SurvivorRatio 来固定这个比例。但是需要注意的是,其中一个 Survivor 区会一直为空,因此比例越低浪费的堆空间将越高。
Minor GC 触发时机,当Eden区满了,利用 标记 - 复制算法,做了什么事情呢?看下面流程图:
上面的每个判断,其实都是有参数是可以配置设置的,这里省略了。
Minor GC 好处:
1)Eden区的对象基本都是只用一次就清除了,所以只需要标记还存活的对象进行复制,其实清除,这样就很高效。
2) 不用对整个堆进行垃圾回收。只是对新生代进行清除,效率高。
带来的问题:
那就是老年代的对象可能引用新生代的对象。也就是说,在标记存活对象的时候,我们需要扫描老年代中的对象。如果该对象拥有对新生代对象的引用,那么这个引用也会被作为 GC Roots。因此引出了Full GC的概念。
Full 触发时机: 当有对象晋升为老年代时候,发现老年代的空间不足或,则会触发。者持久代空间不足。或者当在持久代创建对象,发现内存不足时候,但是现在通常的持久代是在系统的内存上了,所以通常比较少会出现内存不足。
实现流程: 不同的垃圾回收器用不同的算法,主要流程是在老年代创建对象,发现内存不足,然后就会进行对该区的对象不可达的对象回收,回收之后,如果发现还是内存不足,无法保存对象,这个时候就会报错内存泄露。
通常发送了full gc是对程序比较严重的预警,比较高度注意,从而阻止一些一起改full gc的行为,从而保证系统的正常运行。
常见的垃圾回收器:Serial GC、ParNew GC、Parrallel GC、CMS GC、G1 GC
它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。
它是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作。
它是 server 模式 JVM 的默认 GC 选择,也被称作是吞吐量优先的 GC。它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。
基于标记 - 清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,这一点对于 Web 等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC。但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。 CMS 已经在 JDK 9 中被标记为废弃(deprecated)
G1 GC 这是一种兼顾吞吐量和停顿时间的 GC 实现,是 Oracle JDK 9 以后的默认 GC 选项。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。所以很多情况选择G1 GC作为服务器的垃圾回收器是很好的选择。所以这里对C1 GC详细说说。
7.5.1 首先我们对GC1的基本了解
Region :G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。
如下图所示:
G1的内存结构不仅和传统的结构上不同,而且还可以看到比传统的多了一个结构模块,就是图上的H,其全称是Humongous,字面的意思其实就是巨大的,而这些Region表示用来存储巨大的对象的。就是当一个对象大小超过某个阈值,就会被标记为Humongous。按照我们传统的,对于这种大对象,会直接安排到老生代上。
RSet: 全称Remembered Sets, 用来记录外部指向本Region的所有引用,每个Region维护一个RSet。利用这样记录的方法的好处是在回收一个Region的时候不需要执行全堆扫描,只需要检查它的RS就可以找到外部引用,而这些引用就是initial mark的根之一。
Card: JVM将内存划分成了固定大小的Card。这里可以类比物理内存上page的概念。
Card Table: 如果一个线程修改了Region内部的引用,就必须要去通知RS,更改其中的记录。为了达到这种目的,G1回收器引入了一种新的结构,CT(Card Table)——卡表。每一张卡,都用一个Byte来记录是否修改过。卡表即这些byte的集合。
Region、Card、Card Table、Rset之间的关系如下图:
其中主要包括:Young GC、Mixed GC、Full GC 。
当Eden区内存不足时候,就会发生Young GC。而当老年代占整个堆大小的百分比到达阈值时候,就会发生Mixed GC。而当G1的垃圾回收过程是和应用程序并发执行的,当Mixed GC的速度赶不上应用程序申请内存的速度的时候,Mixed G1就会降级到Full GC,使用的是Serial GC。
这里详细说一下Mixed GC。
什么是 Mixed GC?
回收所有的年轻代的Region+部分老年代的Region,回收部分老年代是参数-XX:MaxGCPauseMillis ,用来指定一个G1收集过程目标停顿时间,默认值200ms,当然这只是一个期望值。G1的强大之处在于他有一个停顿预测模型(Pause Prediction Model),他会有选择的挑选部分Region,去尽量满足停顿时间。
Mixed GC触发的条件是什么?
Mixed GC的触发也是由一些参数控制。比如XX:InitiatingHeapOccupancyPercent表示老年代占整个堆大小的百分比,默认值是45%,达到该阈值就会触发一次Mixed GC。
Mixed GC的过程是怎样?
Mixed GC主要可以分为两个阶段:
全局并发标记
全局并发标记可以分为五个阶段如下:
初始标记:标记了从GC Root开始直接可达的对象。初始标记阶段借用young GC的暂停,因而没有额外的、单独的暂停阶段。
并发标记:这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
最终标记:标记那些在并发标记阶段发生变化的对象,将被回收。
清除垃圾:这个阶段如果发现完全没有活对象的region就会将其整体回收到可分配region列表中。 清除空Region。
拷贝存活对象
Evacuation阶段是全暂停的。它负责把一部分region里的活对象拷贝到空region里去(并行拷贝),然后回收原本的region的空间。Evacuation阶段可以自由选择任意多个region来独立收集构成收集集合(collection set,简称CSet),CSet集合中Region的选定依赖于上文中提到的停顿预测模型,该阶段并不evacuate所有有活对象的region,只选择收益高的少量region来evacuate,这种暂停的开销就可以(在一定范围内)可控。
Mixed GC带来的效果是什么?
Mixed GC主要可以对old区无用的对象进行提早清理回收,减少无用的对象一直积压,引起full GC,从而带来对象程序停顿 。
十分感谢学习文章:
1.垃圾回收有哪些?
2.Java Hotspot G1 GC的一些关键技术
3.G1从入门到放弃
4.G1垃圾回收详解
5.极客时间课堂-《深入拆解Java虚拟机》