【重要】第三章:垃圾收集器与内存分配策略

2020-01-18  本文已影响0人  linyk3

Java 和 C++ 之间有一堵由内存动态分配垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。

3.1 概述

垃圾收集(Garbage Collection,GC)的历史比Java久远。第一门真正使用内存动态分配和垃圾收集技术的语言是1960年诞生于MIT的Lisp。

需要回收的内存区域:Java堆和方法区。
程序计数器,虚拟机栈和本地方法栈是线程私有的,内存分配和回收都具备确定性,不需要过多考虑回收问题。

3.2 对象已死吗

Java堆里面几乎所有的对象实例。垃圾回收前需要确认对象是否存活。

3.2.1 引用计数算法

给对象添加一个引用计数器,每当一个地方引用它时,计数器的值+1,当引用失效时,计 数器值-1.任何时刻计时器为0的对象就是不可能再被使用的。
缺点: 无法解决对象之间相互循环引用的问题。

3.2.2 可达性分析算法

主流的商用程序语言都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。
思路:通过一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

Java中的GC Roots对象:

3.2.3 再谈引用

3.2.4 生存还是死亡

可达性分析算法中不可达对象,不是直接回收,而是要经历两次标记过程:


image.png

备注:任何一个对象的finalize()方法只会被系统自动调用一次。finalize() 方法运行代价高,不确定性大,无法保证各个对象的调用顺序,强烈建议不使用。可以用try-finally或其他方法实现。

3.2.5 回收方法区

方法区(或者HotSpot虚拟机中的永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类。

在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP 以及 OSGi 这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

3.3 垃圾收集算法

3.3.1 标记-清除算法

最基础的收集算法是:标记-清除算法,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点

3.3.2 复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块内存,然后再把已使用的内存空间一次清理掉。
优点:实现简单,运行高效。
缺点:内存利用率只有一半。


image.png

现代的商业虚拟机都是采用复制算法来回收新生代,因为新生代中的对象98%都是短暂存在的,
新生代: Eden :Survivor1 : Survivor2 = 8 :1 :1
老年代:为新生代进行分配担保。新生代:老年代 = 1 :2

3.3.3 标记-整理算法

标记-整理算法:首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,最后直接清理掉边界以外的内存。


image.png

3.4 HotSpot的算法实现

3.4.1 枚举根节点

GC Roots的节点:

一致性:分析过程中对象引用关系保持不变,所以必须停顿所有的Java线程(Stop The World)。
即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

目前的主流Java虚拟机使用的都是准确式GC,所以有办法直接得知哪些地方存放着对象的引用,而不需要一个不漏的检查完所有执行上下文和全局的引用位置。
在HotSpot的实现 ,是使用一组称为OopMap的数据结构来达到这个目的的。

3.4.2 安全点 Safe Point

在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots 枚举。
程序执行不是在所有的地方都能停顿下来开始GC,只有在到达安全点时才能暂停下来开始GC。
安全点的标准: 是否具有让程序长时间执行的特征。

3.4.3 安全区域 Safe Region

安全区域是指一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。
在线程要离开Safe Region时,他要检查是否已经完成了根节点枚举(或者整个GC过程):

3.5 垃圾收集器

下面讨论的收集器是JDK1.7之后的HotSpot虚拟机:


HotSpot虚拟机中的垃圾收集器

上面一共有7种作用于不同分代的垃圾收集器。两个收集器之间的连线表明他们可以搭配使用。
直到目前为止还没有最好的收集器,更加没有万能的收集器。只有对具体应用最适合的收集器。

Serial收集器 --> Parallel收集器 --> CMS --> G1
从JDK1.3 --> JDK1.7 用户线程停顿时间不断缩短,但仍然无法完全消除

参考文章

3.5.1 Serial 收集器

3.5.2 ParNew 收集器

并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能是交替执行),用户线程继续工作,而垃圾收集程序运行在另一个CPU上。

3.5.3 Parallel Scavenge 收集器 (吞吐量优先收集器)

3.5.4 Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本。也同样是一个单线程收集器。

3.5.5 Parallel Old 收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法进行垃圾回收。其通常与Parallel Scavenge收集器配合使用,“吞吐量优先”是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。

Parallel Scavenge / Parallel Old 收集器运行示意图

3.5.6 CMS 收集器

CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器.
运作过程相对复杂,可以分为4个步骤:

Concurrent Mark Sweep 收集器运行示意图

优点: 并发收集, 低停顿
缺点:

3.5.7 G1收集器

G1(Garbage-First) 收集器是当今收集器技术发展最前沿成果之一.
G1 是一款面向服务端应用的垃圾收集器.HotSpot 开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器. 与其他收集器相比,G1具备以下特点:

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代. 而使用G1收集器时, Java堆的内存布局就与其他的收集器有很大的差别.
G1将整个Java堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的,他们都是一部分不需要连续的Region的集合.

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集. G1 跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值), 在后台维护一个优先列表, 每次根据允许的收集时间, 优先回收价值最大的Region(这个也是 Garbage-First名称的由来). 这种使用Region 划分内存空间以及有优先级的区域回收方式, 保证了G1收集器在有限的时间内可以获取尽可能高的收集效率.

G1按照Region划分内存的思路是好的,但实现起来却是很难的,因为Region不可能是孤立的. 一个对象分配在某个Region中, 它并非只能被本Region中的其他对象引用, 而是可以和整个Java堆中的任意对象发生引用关系.

在G1收集器中, Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,虚拟机都是使用Remembered Set 来避免全堆扫描的. G1中的每个Region都会有一个与之对应的Remembered Set.

如果不计算维护 Remmenbered Set 的操作, G1收集器的运作大致可以划分为以下几个步骤:

G1 收集器运行示意图

3.5.8 理解GC日志

阅读GC日志是处理Java虚拟机内存问题的基础技能, 它只是一些认为确定的规则, 没有太多的技术含量.

33.125: [GC (Allocation Failure) --[PSYoungGen: 5591K->5591K(9216K)] 9687K->9751K(15360K), 0.0015296 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

100.667: [Full GC (Ergonomics) [PSYoungGen: 5591K->0K(9216K)] [ParOldGen: 4160K->5133K(6144K)] 9751K->5133K(15360K), [Metaspace: 2632K->2632K(1056768K)], 0.0045365 secs] [Times: user=0.06 sys=0.00, real=0.00 secs]

3.6 内存分配与回收策略

Java 技术体系中所提倡的自动内存管理最终可以归结为自动化的解决两个问题:

对象的内存分配, 往大方向讲,就是在堆上分配.对象主要分配在新生代的Eden区上, 如果启动了本地线程分配缓冲, 将按线程优先在TLAB上分配. 少数情况下也可能直接分配在老年代中. 分配的规则并不是百分百固定的, 其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置.

3.6.1 对象优先在Eden分配

大多数情况下, 对象在新生代Eden区中分配. 当Eden区中没有足够空间进行分配时,虚拟机将发起一次 Minor GC.

3.6.2 大对象直接进入老年代

所谓的大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组. 大对象对虚拟机的内存分配来说就是一个坏消息(比遇到一个大对象更加坏的消息就是遇到一群朝生夕灭的短命大对象).经常出现大对象容易导致内存还有不少空间时就提前出发垃圾收集以获取足够的连续空间来安置它们.

虚拟机提供了一个 -XX:PertenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配. 以避免在Eden以及两个Survivor区之间发生大量的内存复制.

3.6.3 长期存活的对象将进入老年代

虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中. 为了做到这一点, 虚拟机给每个对象定义了一个对象年龄计数器.

3.6.4 动态对象年龄判定

为了更好的适应不同程序的内存状况, 虚拟机并不是永远的要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代, 如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就直接进入老年代.

3.6.5 空间分配担保

在发生Minor GC前, 虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间. 如果这个条件成立,那么Minor GC可以确保是安全的.

image.png

JDK1.6之后, HandlePromotionFailure 参数将会失效. 只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC, 否则将进行Full GC.

image.png

3.7 本章小结

本章介绍了垃圾收集的算法, 还有几款JDK1.7中提供的垃圾收集器特点以及运作原理. Java虚拟机中自动内存分配及回收的主要规则.

内存回收与垃圾收集器在很多时候都是影响系统性能,并发能力的主要因素之一. 虚拟机之所以提供多种不同的收集器以及提供大量的调节参数, 是因为只有根据实际应用需求,实现方式选择最优的收集方式才能获取最高的性能.没有固定收集器, 参数组合, 也没有最优的调优方法,虚拟机也就没有什么必然的内存回收行为.因此,在学习虚拟机内存知识,如果要到实践调优阶段,那么必须了解每个具体收集器的行为, 优势和劣势,以及调节参数.

上一篇 下一篇

猜你喜欢

热点阅读