Lisp是第一门开始使用内存动态分配和垃圾收集技术的语言,在胚胎时期时,其作者John McCarthy就思考过垃圾收集需要完成的三件事情:哪些内存需要回收?什么时候回收?
如何回收?
在java中,Java堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理。
教科书判断对象是否存活的算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。是通过
一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
(1)在虚拟机栈(栈帧中的本地变量表)中引用的对象
(2)在方法区中类静态属性引用的对象
(3)在方法区中常量引用的对象
(4)在本地方法栈中JNI(即通常所说的Native方法)引用的对象
(5)Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
(6)所有被同步锁(synchronized关键字)持有的对象
(7)反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
以及根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。
在JDK 1.2版之后,Java对引用的概念进行了扩充,引用分为:
(1)强引用是最传统的“引用”的定义:是指在程序代码之中普遍存在的引用赋值,类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
(2)软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
(3)弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
(4)虚引用称为“幽灵引用”或者“幻影引用”,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
(1)如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,将会第一次标记。
(2)筛选此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
如果有必要执行finalize()方法,会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。如果重新与引用链上的任何一个对象建立关联即可,否则会被回收。
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型;可通过Xnoclassgc参数进行控制是否要无用了就进行回收。
判定一个类型是否属于“不再被使用的类”的条件:
(1)该类所有的实例都已经被回收,Java堆中不存在该类及其任何派生子类的实例。
(2)加载该类的类加载器已经被回收
(3)该类对应的java.lang.Class对象没有在任何地方被引用
从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(Reference
Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类。
收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间。
“标记-清除”(Mark-Sweep)算法在1960年由Lisp之父John McCarthy所提出。算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
主要缺点有两个:
a.是执行效率不稳定
b.是内存空间的碎片化问题
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。其缺陷是将可用内存缩小为了原来的一半。
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。移动存活对象并更新所有引用这些对象的地方是一种极为负重的操作,必须全程暂停用户应用程序才能进行。
从整个程序的吞吐量来看,移动对象会更划算。
所有收集器在根节点枚举这一步骤时都必须暂停用户线程,还始终还是必须在一个能保障一致性的快照中才得以进行,
在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但可能导致引用关系变化,从而会需要大量的额外存储空间,空间成本过于高昂。
实际上HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint)。用户程序执行时必须执行到达安全点后才能够暂停,
如何在垃圾收集发生时让所有线程都跑到最近的安全点,然后停顿下来。这里有两种方案可供选择:
(1)抢先式中断(Preemptive Suspension):
不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。
(2)主动式中断(Voluntary Suspension):当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针,不需要了解跨代指针的全部细节。在实现记忆集时可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,如:
(1)字长精度:每个记录精确到一个机器字长,该字包含跨代指针。
(2)对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
(3)卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。目前最常用的一种记忆集实现形式,
有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。伪共享是处理并发底层细节时一种经常需要考虑的问题。
了当前主流编程语言的垃圾收集器基本上都是依靠可达性分析算法来判定对象是否存活的,可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析,
《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。
直到现在还没有最好的收集器出现,更加不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。
最基础、历史最悠久,是一个单线程工作的收集器。在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束【“Stop The World”】。
它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,最大的优点是:是简单而高效。
实质上是Serial收集器的多线程并行版本,,除了同时使用多条线程进行垃圾收集之
外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。
在JDK 5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS收集器,是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,但是无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以自JDK 9开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。
【也经常被称作“吞吐量优先收集器”】
是一款新生代收集器,同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。高吞吐量可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
d是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到JDK6时才开始提供的。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,器是基于标记-清除算法实现的。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。它最主要的优点:并发收集、低停顿。
CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,缺点:应用程序变慢,降低总吞吐量、收集结束时会有大量空间碎片产生。
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。JDK 8 Update 40以后的G1收集器才被Oracle官方称为“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。
它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。G1开创的基于Region的堆内存布局是它能够实现这个目标的关键,收集器能够对扮演不同角色的
Region采用不同的策略去处理。
在G1中,新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默
认值是200毫秒),优先处理回收价值收益最大的那些Region。
G1收集器的运作过程大致可划分为:
(1)·初始标记:标记一下GC Roots能直接关联到的对象,并且修改TAMS
指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
(2)并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆
里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
(3)最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留
下来的最后那少量的SATB记录。
(4)筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
除了并发标记,其余阶段要完全暂停用户线程。官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望。
可由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。
衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),一款优秀的收集器通常最多可以同时达成其中的两项。延迟是最被重视的性能指标。
第一款不由Oracle(包括以前的Sun)公司的虚拟机团队所领导开发的HotSpot垃圾收集器。
是一款只有OpenJDK才会包含,而OracleJDK里反而不存在的收集器,“免费开源版”比“收费商业版”功能更多。它与G1有着相似的堆内存布局,在初始标记、并发标记等许多阶段的处理思路上
都高度一致,甚至还直接共享了一部分实现代码。但是与G1至少有三个明显的不同之处:
(1)支持并发的整理算法
(2)默认不使用分代收集
(3)使用为“连接矩阵”的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题。
收集器的名字叫做Z Garbage Collector,是一款在JDK 11中新加入的具有实验性质[1]的低延迟垃圾收集器,是由Oracle公司研发的。
ZGC和Shenandoah的目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
采用的染色指针技术,它直接把标记信息记在引用对象的指针上。其三大优势:
(1)·染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。
(2)染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量
(3)染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。
遵循“必须因地制宜,按需选用”的准则。
是一款以不能够进行垃圾收集为“卖点”的垃圾收集器,垃圾收集器的职责是“自动内存管理子系统”,除了垃圾收集这个本职工作之外,它还要负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责。只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出,那显然运行负载极小、没有任何回收行为的Epsilon便是很恰当的选择。
如何选择一款适合自己应用的收集器,主要受以下三个因素影响:
(1)·应用程序的主要关注点是什么?
(2)·运行应用的基础设施如何?
(3)·使用JDK的发行商是什么?
阅读分析虚拟机和垃圾收集器的日志是处理Java虚拟机内存问题必备的基础技能。没有任何的“业界标准”可言,换句话说,每个收集器的日志格式都可能不一样
见书中参数表 P180
Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以
及自动回收分配给对象的内存。
1 对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
2 大对象直接进入老年代
大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组.
3 长期存活的对象将进入老年代
4 动态对象年龄判定
5 空间分配担保
垃圾收集器在许多场景中都是影响系统停顿时间和吞吐能力的重要因素之一,虚拟机之所以提供
多种不同的收集器以及大量的调节参数,就是因为只有根据实际应用需求、实现方式选择最优的收集
方式才能获取最好的性能。