GC发展
Java不像C或C++那样,需要程序员在编程的过程中,时刻注意申请内存保存对象,在对象使用完成后,要在合适的时机将对象占用的内存释放掉(析构函数);Java得意与内部的三大机制,保证了程序开发方便:
解释执行+ 即时编译器JIT,对热点代码的即时编译执行为机器,在方法区中保存代码缓存;
执行引擎;
垃圾回收机制;
我们今天的重点就是要阐述:垃圾回收机制。
为什么要进行垃圾回收?
原因很简单:现在的计算机系统都是跑在程序存储结果的计算机上的,程序必须首先通过IO加载到内存中,才能开始创建程序进程,开始运行。而内存的大小是有上限的,无休止地在内存中创建对象,占用内存,而不去清理,必然是会造成内存不足,导致操作系统将应用进程杀死。
所以就需要java字节码,要么字节码中管理内存,申请内存,扩展内存,释放内存;要么JVM自己提供垃圾回收机制;
要进行垃圾回收,首先就要知道,对象保存在哪里?什么样的对象是垃圾?
对象保存在哪里?
对象在堆中保存,小的对象可能会保存在栈的TLAB中,JVM的内存区域分五个部分,其实在实际中,java所使用的内存包括有(后续补充)??
java内存 = PC寄存器 + java栈 + 本地栈 + 年轻代 +老年代 + 直接内存空间
堆的逻辑划分
分别介绍如下:
程序计数器PC:是一块较小的内存空间,作用可以看做是当前线程所执行的字节码的行号的指示器,线程私有。
JVM方法栈和本地方法栈:在sun的jdk中,JVM方法栈和本地方法栈是算在一起的,虚拟机栈为虚拟机执行java方法,本地方法栈为虚拟机执行Native方法,线程私有。
java heap:是虚拟机中内存区域最大的一块,细分可以分为新生代和旧生带,新生代可以划分为E、S0、S1,其中S1和S0又可以叫做from 或 to,线程共享。
方法区:存放了要加载的类的信息,类中的静态变量,定义为final的常量,类中的Field信息,方法信息等,全局共享,又叫做持久带,可以通过 -XX:PermSize和-XX:MaxPermSize设置最小值和最大值,线程共享。
各个区的作用
图示:
新生代:
大多数情况下,java中新建的对象都是在新生代上分配的,新生代由Eden和两块相同大小的S0和S1组成,其中S0和S1又称为From和To(这个划分没有先后顺序),可以通过-Xmn来设置新生代的大小,-XX:SurvivorRatio设置Eden和S区的比值,有些垃圾回收器会对S0或者S1进行动态的调整。
之所以说大多数情况下新建的对象在新生代上分配,是因为有两种情况下java新创建的对象会直接到旧生带,一种是大的数组对象,且对象中无外部引用的对象,另外一种是通过
启动参数上面进行设置-XX:PretenureSizeThreshold=1024(单位是字节),意思是对象超过此大小,就直接分配到老年代的堆内存中,此外,并行垃圾回收器可以在运行期决定那些对象可以直接创建在旧生带。
老年代:
多次回收之后仍然存活的对象,大小是-Xms减去-Xmn。
常见的参数设置:
-XX:+ 启用选项
-XX:- 不启用选项
-XX:= 给选项设置一个数字类型值,可跟单位,例如 32k, 1024m, 2g
-XX:= 给选项设置一个字符串值,例如-XX:HeapDumpPath=./dump.core
什么是垃圾对象
Java采用的是可达性分析法。
判断对象是否为垃圾的方法有两种:
引用计数法
可达性分析算法
可达性分析算法中,有个非非常常的概念:根集合,引用链:
根集合
(1)根集合,包含了一组对象,这些是对运行时数据区的对象快照的扫描,主要包含有:
java虚拟机栈(栈帧中的本地变量表)中的引用的对象
本地方法栈中JNI本地方法的引用对象。
方法区中的类静态属性引用的对象
方法区中的常量引用的对象。
常驻的异常对象,锁对象
其他代的堆内存中,对当前回收内存区域有引用关系的对象;比如:young gc时,出现的老年代中的对象,G1回收时Region中的remember set对象
引用链
从根集合出发,开始遍历堆内存中的对象,在引用链上的对象,就是正在使用的对象,不是垃圾;没有在引用链上的对象,就是垃圾,垃圾回收器就要对其进行回收处理。
STW的时机
为了确保堆内存中的对象间的引用关系是不变的,在进行确定根集合时,需要将应用程序暂时停顿,这也是后续垃圾回收器在努力奋斗进行攻克的一个重要参数,停顿时间,越小越好,吞吐量越大越好。
STW的时机:
安全点
哪些时刻作为安全点:
方法开始执行;
方法返回之前;
异常抛出
进入循环调用前
安全区域
哪些时间区域作为安全区域:
sleep()操作
线程阻塞时
垃圾回收算法
三种垃圾回收算法和java最后选择的回收算法
复制算法
所有的年轻代的收集算法
标记-清除算法
CMS的垃圾回收算法
标记-整理算法
Parallel old的收集算法
Java选择的垃圾回收算法:分代回收算法,将堆内存进行逻辑划分为:根据对象的年龄大小,划分为年轻代,年老代保存,在不同的代,使用不同的回收算法。
垃圾回收器
第一阶段 串性回收
用在客户端模式的java虚拟机上,
暂停应用线程,单线程地回收垃圾对象;
第二阶段 并行回收
暂停应用线程,启动多个GC线程地回收垃圾对象;
对比串性回收的速度高了,停顿的时间变短了,但是仍是在整个回收过程中,只有GC运行,应用线程是不工作的;
第三阶段 并发回收
在GC的过程中,根据GC过程的不同阶段,将部分阶段设置为GC线程和应用线程同时运行,并且最大限度地减少GC造成的暂停时间,这个过程包括有:
收集根对象;
初始标记
并发标记
最终标记
并发清除
举例:CMS垃圾回收器
第四阶段 标记和回收分离
标记和回收都是并发的,并且标记有标记的触发条件;
回收有回收的触发条件,两个条件相互影响。
举例:G1,ZGC
CMS三个很重要的参数
-XX:CMSInitiatingOccupancyFraction
设置CMS收集器在老年代空间被使用多少(百分比)后触发垃圾收集。默认设置-XX:CMSInitiatingOccupancyFraction=68
表示老年代空间使用比例达到68%时触发CMS垃圾收集。仅当老年代收集器设置为CMS时候这个参数才有效。
-XX:+UseCMSCompactAtFullCollection
设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅当老年代收集器设置为CMS时候这个参数才有效。
-XX:CMSFullGCsBeforeCompaction
设置CMS收集器在进行多少次垃圾收集后再进行一次内存碎片整理。如设置-XX:CMSFullGCsBeforeCompaction=2
表示CMS收集器进行了2次垃圾收集之后,进行一次内存碎片整理。仅当老年代收集器设置为CMS时候这个参数才有效。
与日志有关的三个参数:
- -XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-Xloggc
指定gc日志的存放位置。如
-Xloggc:/var/log/myapp-gc.log
表示将gc日志保存在磁盘/var/log/
目录,文件名为myapp-gc.log
到底Java GC是在什么时候,对什么东西,做了什么事情?
(1)时间:
JVM进程启动后,就会不断地在新生代中保存新创建的对象,当Eden区满了后触发minor gc,用的是复制算法,当Survivor中的一半以上的对象存活年龄大于平均年龄时,或者对象的年龄大于设置的参数:-XX:MaxTenuringThreshold时,就会触发对象晋升,从年轻代晋升到老年代保存,当晋升到老年代的对象大小总和大于老年代剩余空间full gc时,并且晋升担保机制设置为ture时,就会直接尝试晋升,老年代内存不足时,触发老年代full gc,或者小于时被HandlePromotionFailure参数强制full gc,gc与非gc的耗时比较后,超过了GCTimeRatio的限制引发OOM:
通过参数NewRatio控制年轻代和老年代的堆空间的比例;
通过参数SurvivorRatio控制年轻代中,伊甸园区和幸存者区的比例
通过参数:-XX:MaxTenuringThreshold设置从年轻代晋升到老年代的对象年龄,其中:CMS是默认:15,G1默认是:6;
GC关键技术
三色标记法
白色:还没标记,或者标记为了垃圾对象;
灰色:标记一半,属性没有标记完成
黑色:自己和自己的属性都已近全部标记完成。
记忆集
用来保存其他堆内存区,对当前GC线程作用的堆内存区的引用关系;
卡表
用来记录对其他代,或其他Region中的对象,有当前card的引用指向时,就将当前card标记为dirty,并且在垃圾回收时,将dirty保存到drity card queue中;
目的:在进行垃圾回收时,防止对全堆进行扫描,从而降低并发标记或最终标记的暂停时间;
全称是Remembered Set,是辅助GC过程的一种结构,典型的空间换时间工具,和Card Table有些类似。还有一种数据结构也是辅助GC的:Collection Set(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。
逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
下图表示了RSet、Card和Region的关系:
SATB
当G1开始进行回收时,为了确保堆内存中,对象之间的引用关系不变,而获取一个内存快照,进行并发标记的作用对象就是这个SATB内存快照;
CMS和G1算法都涉及对可达对象的并发标记。并发标记的主要问题是collector在标记对象的过程中mutator可能正在改变对象引用关系图,从而造成漏标和错标。错标不会影响程序的正确性,只是造成所谓的浮动垃圾。但漏标则会导致可达对象被当做垃圾收集掉,从而影响程序的正确性。
为解决漏标问题,GC Handbook一书首先将对象分为三类,即所谓的black对象,grey对象和white对象。white对象是那些还没有被collector标记到的对象;grey对象是那些自身已经被标记到,但其所有引用字段还没有处理的对象;而black对象则是自身已经被标记到,且其引用的所有对象也已经被标记的对象。
基于上述分类,一个white对象在并发标记阶段会被漏标的充分必要条件是:
1、mutator插入了一个从black对象到该white对象的新引用
2、mutator删除了所有从grey对象到该white对象的直接或者间接引用。
因此,要避免对象的漏标,只需要打破上述2个条件中的任何一个即可。
CMS:
Incremental update关注的是第一个条件的打破,即引用关系的插入。Incremental update利用write barrier将所有新插入的引用关系都记录下来,最后以这些引用关系的src为根STW地重新扫描一遍即避免了漏标问题。
G1:
SATB关注的是第二个条件的打破,即引用关系的删除。SATB利用pre write barrier将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根STW地重新扫描一遍即可避免漏标问题。
在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
1,在开始标记的时候生成一个快照图标记存活对象
2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)
3,可能存在游离的垃圾,将在下次被收集
G1的写前屏障
G1在并发标记过程中,当对象与对象之间的引用关系发生变化时,就会将引用断开的对象,保存起来,在并发阶段的后期,重新对这些对象进行扫描标记;
目的:防止对象消失,
CMS的增量更新
CMS在进行并发标记过程中,当对象间的引用关系发生变化时,就会引用发生变化的对象,利用增量更新的条件,使用写后屏障技术保存起来,在最终标记阶段,将会把根集合和并发标记阶段引用发生变化的对象为新的根集合,重新扫描一遍所有的对象;
目的:防止对象消失;
缺点:可能存在浮动垃圾;
染色指针
多个虚拟内存地址,根据地址中特殊位置的标记位,用来表示哪个地址段是有效的,从而实现多个虚拟内存地址,对应一个物理内存地址,可支持最大内存空间为:
16T;
G1的标记过程
概述
STAB全称Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:
白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉,即灰色节点的子节点。
灰:对象被标记了,但是它的field还没有被标记或标记完。
黑:对象被标记了,且它的所有field也被标记完了。
由于并发阶段的存在,那就有可能在并行运行期间之前的标记过的对象的引用关系可能被改变,就会出现白对象漏标的情况,这种情况发生的前提是:
把一个白对象的引用存到黑对象的字段里,如果这个情况发生,因为标记为黑色的对象认为是扫描完成的,不会再对它进行扫描。
某个白对象失去了所有能从灰对象到达它的引用路径。
对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?
如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误。
解决新创建对象产生的漏标问题
SATB算法机制中,会在GC开始时先创建一个对象快照,在并发标记时所有快照中当时的存活对象就认为是存活的,标记过程中新分配的对象也会被标记为存活对象,不会被回收。这种机制能够很好解决新创建对象漏标的情况。STAB核心的两个结构就是两个Bitmap。
Bitmap分别存储在每个Region中,并发标记过程里的两个重要的变量:preTAMS(pre-top-at-mark-start,代表着Region上一次完成标记的位置) 以及nextTAMS(next-top-at-mark-start,随着标记的进行会不断移动,一开始在top位置)。SATB通过控制两个变量的移动来进行标记,移动规则如下:
假设第n轮并发标记开始,将该Region当前的Top指针赋值给nextTAMS,在并发标记标记期间,分配的对象都在[ nextTAMS, Top ]之间,SATB能够确保这部分的对象都会被标记,默认都是存活的。
当并发标记结束时,将nextTAMS所在的地址赋值给previousTAMS,SATB给[ Bottom, previousTAMS ]之间的对象创建一个快照Bitmap,所有垃圾对象能通过快照被识别出来。
第n+1轮并发标记开始,过程和第n轮一样。
A阶段,初始标记阶段,需要STW,将扫描Region的Top值赋值给nextTAMS。
A-B阶段:并发标记阶段。
B阶段,并发标记结束阶段,此时并发标记阶段生成的新对象都会被分配在[nextTAMS,Top]之间,这些对象会被定义为“隐式对象”,同时
_next_mark_bitmap
也开始存储nextTAMS标记的对象的地址。C阶段,清除阶段,
_next_mark_bitmap
和_prev_mark_bitmap
会进行交换,同时清理[ Bottom, previousTAMS ]之间被标记的所有对象,对于“隐式对象”会在下次垃圾收集过程进行回收(如第F步),这也是SATB存在弊端,会一定程度产生未能在本次标记中识别的浮动垃圾。
解决对象引用被修改产生的漏标问题
SATB利用pre-write barrier,将所有即将被修改引用关系的白对象旧引用记录下来,最后以这些旧引用为根重新扫描一遍,以解决白对象引用被修改产生的漏标问题。
在引用修改时把原引用保存到satb_mark_queue中,每个线程都自带一个satb_mark_queue。在下一次的并发标记阶段,会依次处理satb_mark_queue中的对象,确保这部分对象在本轮GC中是存活的。
如果被修改引用的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是float garbage。因为SATB的做法精度比较低,所以造成的float garbage也会比较多。
总结
本文主要对GC发展做了详述,分布式高并发的环境,造就了GC不断发展,要在最短的时间内,给用户响应最多的返回数据,就需要后端服务有很强的响应能力;
就需要GC的暂停时间段,GC回收的效率越高越好。