JVM(二).垃圾回收算法/策略&内存分配
1.概述
内存释放动态化:那些内存需要回收;什么时候回收;如何回收;
2.回收那些对象
垃圾回收的是不需要被使用的对象,就是可以被回收的,如何确定?
2.1 引用计数
在对象中,添加引用计数,一个地方引用+1,引用失效-1,如何时刻计数器是0 的时候就是可以回收对象了;
明显的缺点:循环引用;(a 引用b ,b 引用a,但是a,b 都没有其他引用了,这个时候是应该被回收了)
2.2 可达性分析
主流语言都在使用的,通过一系列称为 GC Roots
的节点作为其实节点集,来搜索引用链 Reference Chain,如果一个对象没有连接到引用链,或者叫这个对象不可达,这个对象可以被回收了;
在Java体系里面 固定的 GC Roots
有以下几种
- 虚拟机栈中引用的对象
- 方法区中,静态属性引用的对象
- 常量池的引用
- Native 引用的对象
- 虚拟机内部引用 Class 对象
- 同步锁synchronized 引用的对象
- JMX 本地代码缓存等
2.3 引用的级别
级别的作用就是:当内存够时,我们可以都保留,内存比较紧张的时候,就可以根据引用的级别回收固定级别的这些对象;
- 强引用:Strongly Reference, 普遍的引用复制;
eg: Object a = new Object
;只要这种关系存在,就不会回收这种对象; - 软引用:Soft Reference,描述一些有用但是非必须,系统即将内存溢出的时候回收,如果还是内存不够,就报错内存溢出;
SoftReference
类来提供; - 若引用:比软引用还低一点,也是描述非必要的对象,发生
GC
就会被回收;WeakReference
类来提供弱引用 - 虚引用:幽灵引用/幻影引用,引用关系最弱的一种,一个对象持有关联了一个虚引用,不能通过这个引用来获取对象实例
PhantomReference
类来提供弱引用
2.4 回收/继续存活
被可达性分析算法判定后为不可达对象后,也不是马上必须去回收,至少会有两次标记的过程:
- 引用链不可达
- 是否需要执行
finalize()
,如果没有重写这个方法或者这个方法已经被调用(一个对象只会调用一次),就不会再执行这个方法;如果要执行,放入F-Queue对列,该虚拟机低优先级线程执行对列对象的finalize()
,如果在方法里面重写建立连接,就可以避免回收,但是也有可能等不到执行就会被GC
了
不建议使用!!!!
2.5 回收方法区
虚拟机规范中可以不要求实现方法区的内存回收,也有不能回收方法区的垃圾回收器(JDK11
的ZGC
),回收性价比低,回收条件复杂;
回收具体有两大类:废物的常量和不在使用的类型;
第二个实现比较复杂 需要做一下判断 :
- 该类已经该类的子类对象实例都被回收;
- 该类的加载器被回收,比较难,除非是设计过类加载器的比如
JSP
OSGi
的加载器,重加载之后的类型信息回收; - 类对应 Class 对象没有被引用,无法通过反射获取该类方法;
参数控制: 是否方法区 GC
-Xnoclassgc
,加载和卸载信息 -verbose:class
,-XX:+traceClassLoading
;-XX:+traceClassUnLoading
在使用反射,动态代理,CGLib
一些字节码框架,动态生成JSP
,OGSi
自定义类加载器 需要虚拟机具备类型卸载功能,来保证方法区可用;
3.垃圾回收算法
3.1 分代收集理论
1.弱分代假说:大部分对象是短暂的
2.强分代假说:多次垃圾回收后还存在就越难以消亡;
垃圾回收器回收对象的时候根据对象的年龄划分到不同的区域;也就是将上面说的短暂对象放在一起和多次GC
还存在的对象放在一起;提示回收效率;针对不同的区域,又有好几个回收类型 Minor GC
, Major GC
, Full GC
,在进而对不同类型的 GC
有了不同的算法 标记复制
标记清楚
标记整理
等回收算法;
3.跨代引用假说:占比较少,垃圾回收的年代不是孤立的
年轻代的回收需要判断是否被老年代引用, 需要把老年代加入GC Roots
但是开销巨大 ,而且就算是扫描了 年轻代的引用也会由于老年代的引用而进入老年代,为每一个对象存储跨代引用也比较浪费;所以在新生代创建一个 Remember Set
数据结构用于划分老年代内存块,记录年轻代关联老年代的区域,在 Minor GC
的时候,把块内对象加入GC Roots
就可以了;
- 部分回收
Partial GC
不是整个Java堆的垃圾回收- 新生代收集
Minor GC
/Young GC
新生代收集 Major GC
/Old GC
Mixed GC
整个新生代 部分老年代,只有G1
会
- 新生代收集
- 整堆收集
Full GC
: 整个Java堆和方法区的内存回收;
3.2 标记-清除算法
Mark-Sweep 算法:两个阶段,标记要回收的对象,统一回收标记的算法;也可以反着来,标记或者的对象,清除没有被标记的对象;
比较基础的垃圾回收算法,后面的回收算法大部分是以这个为基础;该算法的主要缺点有两个:执行效率不稳定,两个阶段的执行效率随着对象数量变多而降低;第二个就是执行后内存空间碎片化的问题,无法获取连续的内存空间;
3.3 标记-复制算法
内存等量划分两块,每次只用一块,一块使用完了,或者的对象复制到另一块上面去,已使用的那块全部清除;
缺点,浪费了空间
现在的虚拟机实现 HotSpot
的 Serial
,ParNew
等新生代收集器就是这种内存布局;把新生代划分成Eden区域
和两块 Survivor
;比例是8:1:1
;
Minor GC
的时候把Eden 和 Survivor
存活的对象复制到一块保留的Survivor
上面,清除Eden 和 Survivo
r;如果保留的Survivor
内存不够保存存活的区域,就需要老年代担保Handle Promotion
3.4 标记-整理算法
Mark-Compact :标记复制对于存活率较高的区域清除效率相对就比较低下,而且还需要浪费空间和空间担保;如果是内存全部存活的老年代那种回收 对象100%存活。
算法实现:标记过程 和 标记-清除 一样,但是后面是让存活对象向内存一边移动,然后直接清除以外的内存;
如何移动对象,特别是在老年代都是在使用中的对象,对象在内存区域内的移动是比较重的操作,虚拟机会停止用户程序的执行,成为 STD stop the world
为什么要移动对象,是为了保证内存空间的连续性,提示虚拟机吞吐量;要是移动对象,停顿时间会更短,甚至不停顿;
关注吞吐量的实现 Parallel Scavenge
关注低延迟的实现 使用标记-清楚实现 CMS
4.HotSpot
的算法实现细节
4.1根节点枚举
收集器的根节点枚举是需要暂停用户线程的,不能在节点枚举过程中出现因为用户线程发送变动,,保证分析结果的可靠性,会有STD stop the world
,但是可达性分析算法可以和用户线程一起执行;
在 HotSpot
的实现方案里面,是有OopMap
数据结构来存储执行上下文和全局引用;
4.2 安全点
有了OopMap
可以快速完成 GC Roots
根节点枚举,但是很多代码指令会影响到OopMap
的关系,如果每一个指令都去关联一下 OopMap
,难免加大运行成本和存储成本;只是在特定的时候记录到OopMap
,这些位置被称为安全点 SafePoint
,每个指令执行都很短暂,一般选择‘长时间指令’ 比如方法调用,循环跳转,异常跳转,这些才会有安全点;
如何保证程序运行到代码附加的安全点来停顿,有两种实现方案:
- 抢先式中断,
Preemptive Suspension
,不需要线程的执行代码主动配合,GC
时,全部停止,然后把还没有到安全点的线程恢复执行,知道最近的安全点,现在很少使用了 - 主动式中断,
Voluntary Suspension
,垃圾回收需要停止线程的时候,不需要对线程操作,设置一个标志位,用户线程在执行的时候(轮询的时间点:安全点+ 创建对象+ 堆内存分配内存)去轮询这个标志位,发现中断标志位就在最近的安全点上面主动挂起; 轮训指定 test
4.3 安全区域
Safe Region
安全点事正在执行的线程响应线程中断指令,但是用户线程处于Sleep /Blocked 状态 这个时候就无法主动响应中断了,这个时候就引入了安全区域;线程在执行到安全区域时候,垃圾回收器不会管理这些进入了安全区域的线程,等这些线程恢复的时候,如果还没有完成根节点枚举,就保持运行,一直在安全区域运行,直到枚举完成,可以离开安全区域;
4.4 Remember Set
前面说了 Remember Set
是记录跨代引的对象;避免全量扫描老年代;本小节主要是介绍Remember Set
的实现方式;若不考虑性能的话,最简单的实现就是非收集区域中所有含跨代引用对象的数组,这样也是新能比较低下的;
有三种记录方式
- 字长精度 定位到机器字长
- 对象精度 对象里面有字段含跨代引用
- 卡精度 内存区域块内部又跨代引用
Card Table
方式去实现
Card Table
最简单的实现就是字节数组(不用bit 是因为现在计算机硬件指令最小都是byte),而HotSpot
也是这样实现的
CARD_TABLE[this addredd >> 9 ] = 0
数组内部一个对象标识者内存区域一块内存(卡页 Card Page
,大小一般是2^n 幂的字节数 上面可以看到是 9 次方 512 字节),
比如其实地址是 0x0000
下面就是映射关系 , 如果 Card Page
记录的是1 就是标志着对应的内存区域是Dirty
是有跨代引用的,加入 GC Roots
4.5 写屏障
卡表元素如何维护: 何时变脏:其他区域引用了本区域,时间点就是引用对象赋值的那一刻;如何变脏:如何在赋值的时候去维护卡表;
在HotSpot
的实现里面是通过写屏障 Write Barrier 实现的,其处理模型可以看做是对引用对象赋值时候的AOP Around
切面;一个是Pre-Write Barrier
一个是Post-Write Barrier
。一般是在对象赋值后使用写后屏障更新卡表;
对于高并发下面的并发修改,JDK
后新增一个参数 -XX:+UseCondCardMark
来判断卡表是否变成脏了,如果没有变脏才更新卡表;
4.6 并发的可达性分析
主流编程语言基本都是通过可达性分析来判断对象是否存活,算法理论上需要全过程都基于一致性的快照中去分析,意味着要停止所有用户线程;上面介绍过 OopMap
来记录的根节点开始便利查找对象图,这个步骤的时间是和堆的内存大小成正比的,堆越大,就需要更多时间去标记;
如何保障一致性快照。三色标记来辅助理解
- 白色 垃圾回收器还没有访问,如果回收结束后,对象还是白色的,代表对象不可达;
- 黑色 表示已经被垃圾回收器访问过,是安全的
- 灰色 表示对象已经访问,但是还有一个对象没有被扫描过;
如果在回收期间一切用户线程停止,扫描就不会有任何的问题,但是如果有用户线程参与的话,一种是把要回收的变成不回收的,这个没什么问题,最多下次再回收,如果把本来存活的对象标记为要回收,就会出现问题了;下面描述此过程;
正常情况:用户线程不干预
1.引用是有方向的,只有被黑色引用才能存活;(只有根节点里面引用的对象才能活,对象引用更节点对象,不代表是可以存活的对象)
2.扫描过程,灰色是正在扫描
3.正常完成,黑色存活,白色回收 ,非黑即白
非正常情况:用户线程干预
1.扫描时,用户线程干预,切断引用(红色),本来灰色的引用指向了已经扫描过的黑色,黑色不会二次扫描了;
2.扫描时,用户线程干预,切断的引用是一段引用链:导致两个/多个对象丢失(红色)
问题的来源:原本是黑色的由于被并发干预变成了白色;下面两个条件同时满足的时候会导致这个问题
- 插入了一个或者黑色对象到白色对象的引用
- 删除了灰色对象到白色对象的引用
要解决这个问题,破坏其中一个条件就可以=> 两种解决方案:
增量更新 Incremental Update
,当黑色插入时,记录黑色节点,扫描结束后,重写扫描这些黑色节点;可以简单的理解为,一旦黑色插入白色,就把黑色转变成黑色;(CMS
使用该方案)
原始快照 Snapshot At the Begining
SATB
: 灰色节点要删除的时候,保存灰色节点到删除节点记录,扫描结束之后,灰色节点作为根节点,重写扫描;(G1
,Shenandoah
使用该方案)
5.经典垃圾回收器
下图7种作用于不同内存区域位置的垃圾回收器,连线表示可以搭配使用;
5.1 Serial
JDK1.3
之前的新生代唯一的垃圾回收器,单线程,运行时停止所有用户线程 ; 会发生STD
体验比较差;但是也有优点,适合内存资源有限,内存消耗较小,单线程情况下没有线程交互开销;
使用场景:客户端模式;
5.2 ParNew
ParNew
回收器是 Serial 回收器的多线程并行版本,代码也复用很多
JDK9
之前官方推荐使用的是 ParNew
+ CMS
,ParNew
搭配很单一 ,JDK9
之后还取消了 ParNew
+ Serial Old
,ParNew
+ CMS
;ParNew
直接并入CMS
单线程环境下效果比Serial
还要差,只有在核心比较多的情况下,才能体现其效率;默认开启的线程是CPU 核心数
;参数-XX:ParallerGCThreads
限制线程个数
5.3 Parallel Scavenge
新生代,标记-复制算法,并行 垃圾回收器;
目标:达到一个可控制的吞吐量;表面特性类似于ParNew
吞吐量 = 用户代码时间/(用户代码时间+GC 时间)
-XX:MaxGCPauseMillis
最大垃圾回收时间;设置大于0 的毫秒值,但是也不能设置的过于小,过小会频繁的GC
,减低了吞吐量;
-XX:GCTimeRatio
直接设置吞吐量大小 0~100
,如果设置19 就是 1/(1+19)= 5% ;系统默认时99 ,就是1/(1+99) = 1%,允许1% GC
时间
吞吐量有限的垃圾回收器,有一个参数 -XX:+UseAdaptiveSizePolicy
可以让虚拟机自适应的来分配新生代各个区域的内存大小比例(Eden,Survivor),此时只需要指定虚拟机的最大最小内存就可以了;
5.4 Serial Old
5.5 Parallel Old
Parallel Old
是 Parallel Scavenge
的老年代版本,JDK6
以后才有;
下图是 Parallel Old
+ Parallel Scavenge
5.6 CMS
CMS
Current mark Sweep
;
目标:获取最短 回收停顿时间,关注服务响应速度,尽可能的降低系统停顿时间;基于标记-清除算法
四个步骤:
1.初始标记 CMS initial mark
STD
标记 GC Roots
的对象 速度很快
2.并发标记 CMS current mark
开始根据标记的对象开始遍历对象图,但是是和用户线程并发运行的;
3.重新标记 CMS remark
STD
重写并发标记期间的用户线程变动导致的那一部分对象,时间比1长,但是远比2 短;
4.并发清除 CMS current sweep
和用户线程一起运行,清除标记可以回收的垃圾;
时间最长的2.和4 步骤 是和用户线程并发执行的,所以总体看来,垃圾回收是和用户线程并发执行的;
优点:并发低停顿回收器;
缺点:1.资源敏感,并发占用CPU 资源,减低用户线程的吞吐量。默认启动的线程数量是 (CPU+3)/4,CPU核心数在4以上是,占用的资源少于25%;
2.无法处理浮动垃圾,重新标记阶段可以处理并发标记阶段修改的引用,但是无法处理新增的垃圾对象,并发清理更是,处理不了新增的垃圾对象;导致在本次垃圾回收阶段直到结束后,都无法处理;只能下一次处理;由于无法处理浮动垃圾,有可能出现 Current Mode Failure 失败导致一次Full GC
;
3.标记清理,会导致大量内存碎片;会触发Full GC
参数:-XX:CMSInitiatingOccupancyFraction
设置内存使用率 超过多少后开始GC
,JDK5
默认68% JDK6
默认92% ;生产环境根据情况权衡设置比例;
5.7G1
Garbage First 垃圾回收器技术发展史上里程碑式的成果,面向局部回收思路和基于Region 的内存布局形式;
面向服务器端的垃圾回收器;用来替换CMS
JDK9
发布:G1
取代Parallel Scacenge
+Parallel Old
CMS
被声明为不推荐使用
新思想:在G1
之前回收的目标,要么是老年代 Major GC
,要么是新生代 Minor GC
,要么是整个Java堆 Full GC
,而G1
是面向整个堆内存的回收集,Collection Set,那块内存最多垃圾数量,回收收益最大 才进行回收;这就是G1 的Mixed GC
模式;
G1
: 基于Region 的内存布局,也遵循分代收集理论,但是堆内存的布局和其他收集器有很大区别:不在坚持固定大小和固定比例的分代内存区域划分;每一个Region根据需求,可以是 Eden,Survivor ,也可以是老年代空间;与此同时G1
对不同角色的Region采用不同的策略去处理;还有一类特殊区域:Humongous
,用于存放大对象。每个Region可以通过 -XX:G1HeapRegionSize
设定内存大小,范围是1M~32M
且是2的幂次;超过一半Region 的对象就是大对象,超过一个Region会被存放在多个连续的Humongous Region
,G1
会把这多个连续的Humongous Region
作为老年区看待;
G1
的老年代和新生代是动态的内存集合了;每次收集就是部分Region,避免整个Java堆垃圾回收。建立可预测的内存模型:跟踪各个Region区域的回收垃圾的价值大小,维护优先列表,根据用户允许的停顿时间优先处理回收价值大的Region,保证了有限的时间内获取最大的效率;
实现细节:
1.跨Region 对象的引用问题:和remember Set 的实现方案差不多,但是比它复杂;实现的数据结构是一个哈希表,key 是别的Region的其实地址,value 是一个Set,存储的是卡表的索引号。因此堆内存的10%~20% 用于维持收集器工作;
2.GC/用户线程
并发问题:G1
使用SATB
,回收期间,会在Region中划出一部分空间用于分配新的对象,G1
每个Region创建两个TAMS
(指针可以动态调节这个区域的大小),新对象的分配空间都在这两个指针上面,而这个区域也不会进行垃圾回收;但是这个区域的内存回收的速度赶不上分配的速率。就会被迫停止用户线程,进行Full GC
3.建立可靠停顿预测模型: -XX:MaxGCPauseMIlls
来设置 ,但是G1
如何保证;为每一个Region计算出一个衰减均值 Decaying Average
,在垃圾回收过程中,会评估记录每个Region的回收耗时,Remember Set
里面的脏卡数量;衰减均值不同于均值,最近的数据影响也是比较大;
GC步骤
1.初始标记:initail Marking
,标记GC roots
关联的对象,设置TAMS
,STD
时间非常短暂
2.并发标记:concurrent Marking
从GC roots
开始对堆中对象可达性分析,时间较长,但是可以和用户线程并发执行;分析完成后,还需要重新处理SATB
并发修改的对象引用;
3.最终标记:Final Marking
对用户线程做一个短暂的暂停,处理少量的SATB
4.筛选回收:Live Date Counting and Evacuation
;Region 记录统计计算,回收价值排序,根据用户设置的参数,来选择多个Region区域回收;把回收区域的存活对象复制到空的Region中去,其中涉及对象引用的修改,必须停止用户线程;
用户根据设置停顿时间来在不同的应用场景中达到吞吐量和低延迟之间的平衡,但是期望停顿时间也是不能设置的太短,一般是一百多到三百多是比较适当的参数;不然回收失败还是会导致Full GC
G1
的目标是取代CMS
; G1
优点:G1
总体是标记-整理;但是在两个Region之间又是标记-复制,带来的好处就是完整的内存区域,不产生内存碎片;缺点:负载高,维护的卡表内存可能会占据内存的20%;维护卡表的操作G1
更加复杂;
G1
更适合大内存的应用;CMS
适合小内存;两者性能的分界点在6~8G
之间;
6.低延迟垃圾回收器
回顾垃圾回收器发展史:衡量一个垃圾回收器的好坏有下面三个重要的指标
- 内存占用
- 吞吐量
- 延迟
现在的硬件发展,内存越来越大,对延迟的要求越来越凸显
6.1 Shenandoah
RehHat
公司发展;Oracle官方没有支持;
目标:任何堆内存大小,垃圾回收停顿时间在10毫秒以内;
相比于Oracle 官方的ZGC
,更像是G1
的下一代继承者,相似的内存布局,和处理步骤,甚至共用一部分代码;Shenandoah 的改进有如下:
G1
最后并发回收阶段是STD,不能和用户线程并发;- Shenandoah 对于Region 的分代的回收优先级优先级减低;
G1
消耗计算资源和内存的Remember Set 替代为全局数据结构 连接矩阵 Connection Matrix 来记录跨Region 引用;
回收步骤:
- 初始标记:Initial Marking ,
G1一样
STD 停顿时间和堆大小无关,和GC Roots
数量有关; - 并发标记:Concurrent Marking
G1一样
b标记可达对象 - 最终标记:Final Marking
G1一样
处理SATB
统计回收结果构成Collection Set - 并发清理:Concurrent Cleanup 清理一个存活对象也没有的Region
- 并发回收:Concurrent Evacuation ,Collection Set 中存活的对象复制到未被使用的Region ,如何保证并发复制:读屏障,转发指针 Brooks Pointers
- 初始引用更新:Initial Update Reference ;回收完成后,原来的旧地址引用需要更新成复制后的引用,本步骤建立一个集合点,保证上一步并发线程都完成了对象的转移;
- 并发引用更新:Concurrent Update Reference 正在开始修改引用地址操作,和用户线程并发
- 最终引用更新:Final Update Reference 处理所有的引用更新后,修正
GC Roots
中的引用 - 并发清理:Concurrent Cleanup 并发回收和清理之后,Collection Set 中所有存活对象都被转移,在调用一次并发清除;
转发指针:在转移对象和用户线程并发的一种解决方案;在移动前的内存上设置保护陷阱;一旦访问到旧的内存地址就会自陷中断,进入异常处理,再有代码逻辑修改访问到新的地址。但是这样,需要操作系统支持,并且需要频繁切换到核心态,代价较大;
新方案:优点:不用设置保护陷阱;而是在原有的内存布局结构前面加上新的引用字段,正常不处于并发移动的情况下,引用指向自己;
比如多个对象引用一个对象A,原有的方式就是,只要是对象A移动的位置,所有引用到A 的对象指针地址都需要修改成新的地址;那么新的数据结构就是 对象指针地址是两部分组成,一个是Brooks Pointer ,一个是原有的 ;在并发移动后,只需要把,只需要修改一处,就可以了;(类似于使用句柄定位对象,只是转发指针是分散在对象上面)
缺点是:每次访问对象都有一次指针转发,尽管这个开销已经被优化到一行汇编指令;
并发情况的处理,2 如果在1,3 之间的话就会出现问题,Shenandoah 通过 CAS
操作保证其中之一执行,另一个必须等待;
1.GC
线程复制对象副本
2.用户线程更新对象某个字段
3.GC
线程更新转发指针的引用值为新副本地址;
Shenandoah 写屏障维护记忆矩阵,读屏障指针的转发处理;尤其是读屏障的使用,因为读的频率很高,所以必须谨慎使用,后续在JDK13
是考虑为引用访问屏障,而不去管原生数据类型的访问
6.2 ZGC
Oracle 公司研发; **目标:**和上面的Shenandoah 一样;
在X64
平台下Region (Zpage
) 有大中小三种容量;
- 小型 Region (Small Region) : 固定为
2MB
,用于存放小于256KB
的小对象; - 中型 Region (Medium Region ): 固定为
32MB
,用于存放小于256KB~4MB
的对象; - 大型 Region (Large Region):容量不固定,可以动态变化,必须时
2MB
的整数倍,用于4MB
以上的对象,只会存放一个对象,实际容量还可能小于中型容量;
ZGC
的思路和 Shenandoah 完全不同,设计更加复杂精巧;标志性的设计是其染色指针技术 Colored Pointer ;以往我们要在对象上存储额外的信息,比如分代年龄,哈希码,锁等都是存储在对象头上面。正常情况下是可以流程访问,但是在并发移动情况下就会有额外的负担,又或者是不去访问对象数据就可以直接获取到对象的一些信息呢?比如是否被移动;就比如之前垃圾回收的三色标记,像Serial 是直接在对象头上面标记的,像G1
和 Shenandoah 就把这个数据记录在 BitMap
上,这里,ZGC
是最直接的,最纯粹的; 原来是遍历对象图,现在就是遍历对象图的引用(染色指针)
染色指针:把少量信息存储在指针上的技术;64位系统,理论一次寻址可达2^64 = 16EB
的数据,但是处于需求,性能,成本AMD64
架构只支持了54位4PB
的地址总线,和 48位 256TB
的虚拟地址空间;此外操作系统还做了限制 Linux
:47位 128TB
进程虚拟地址 和 46位 64TB
物理地址空间, Windows 就44位 16TB
的物理地址空间;
Linux 平台下46 位指针 前四位用于存储信息 ,剩余用于寻址 ,早就压缩了ZGC
的内存大小最大为4TB
染色指针优势:
- Region 中对象移动走后,Region 就可以直接清理,理论上说,只要有一个Region 空闲,就可以完成回收;但是Shenandoah 需要引用更新结束后才可以清除Region ,极端情况下,Region 所有对象都存活,需要
1:1
的内存来准备复制的空间; - 大幅减少内存屏障的使用,对象引用关系的变动都存在于指针中,不需要专门的记录;不使用写屏障(不支持分代,也就没有跨代引用)
- 可以继续开发前18位来记录对象的状态;日后继续提高性能的依据;
问题:随意定义指针,操作系统是否支持,CPU 是否支持?程序代码最后都是转换成机器指令去执行,处理器不会理会指针里面多少位多少位是什么内容,只会当做是内存地址去处理;这个问题在 Solaris / SPARC
上可以直接设置忽略前几位;但是在X86-64
的平台上没有类类似的功能,这里就要说到补救措施:虚拟内存映射技术 :ZGC
为了能高效、灵活地管理内存,实现了两级内存管理:虚拟内存和物理内存,并且实现了物理内存和虚拟内存的映射关系。
GC
步骤
- 并发标记:Concurrent Mark , 比较短暂,和``G1,Shenandoah
差不多,只是标记不在对象上,而是在对象的指针上更新
Marked 0 ,Marked 1`; - 并发预备重分配: Concurrent Prepare for Relocate :统计计算 Region 计算出本次要回收的 Relocate Set;不会像
G1
一样优先回收,而是全部扫描,有存活对象就复制到其他Region ,清理的Region 回收; - 并发重分配:Concurrent Relocate ;为什么上面说复制完成后就可以清楚,不需要等待引用修改完成;是在复制完成后建立了一个转发表(
Forword Table
):记录的是旧地址到新地址的关系;当对象并发访问到时,可以根据指针支持,可以立刻知道是需要转发的,被内存读屏障拦截,这个时候读取新的地址,并更新新的引用;ZGC
称之为 指针自愈Self-Healing。好处是只有第一次会转发,相比于ZGC
的每次转发负载要更低; - 并发重映射:Concurrent Remap 修正堆中旧对象的所有引用;但是
ZGC
这个步骤并不是很迫切,因为上一步的自愈能力;并且这个过程可以过度到下一个垃圾回收的并发标记 过程 因为都是遍历所有对象,就省略一次遍历对象;所有对象都被修正后,转发表就可以释放了
优点:不需要Remember Set ,写屏障,卡表;这些计算资源和空间资源都省略了;
缺点:回收周期长(不代表停顿时间 STD ) 对象的分配速度不能太快,大量新对象是浮动垃圾,只能下次回收;只能增加堆空间,获取更多喘息的时间来下次回收;
优化方案:还是需要引入分代收集,新生代专门区域,针对这个区域更频繁和更快的收集;
7.为应用选择何时的垃圾回收器
为我们应用选择适合的垃圾回收器;要考虑jvm
运行的操作系统如Win Linux ,像ZGC
在Win 上就用不了;如果机器的内存和JDK 版本
较低,CMS
会比较好,内存再大一点的话,就是G1
也不错;
7.1 垃圾回收日志
日志级别:Trace
,Debug
,Info
,Warning
, Error
,Off
默认Info 级别
日志行携带信息: 默认是: uptime,level,tags
time
:当前时间uptime
: 虚拟机运行的时间 秒为单位timemillis
:当前的毫秒值timenanos
:当前的纳秒值pid
:进程IDtid
:线程IDlevel
:日志级别tags
:日志输出的标签集
以JDK 9
为分界线 参数-Xlog: selector :output :decorators :output-option
统一处理系统所有日志
selector 的种类有很多,其中就包括gc
,打印GC 日志
还有其他的比如add,thread,cpu.jni,ihop
等等
1.默认日志打印基本信息
-Xlog:gc
JDK9 之前
:-XX:+PrintGC
//jdk 9 之后
[0.004s][warning][gc] -XX:+PrintGC is deprecated. Will use -Xlog:gc instead.
[0.015s][info ][gc] Using G1
Connected to the target VM, address: '127.0.0.1:64410', transport: 'socket'
2.日志打印详细信息
-X-log:gc*
JDK9 之前
:-XX:+PrintGCDetails
//jdk 9 之后
[0.005s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.
[0.015s][info ][gc,heap] Heap region size: 1M
[0.017s][info ][gc ] Using G1
[0.017s][info ][gc,heap,coops] Heap address: 0x00000000fec00000, size: 20 MB, Compressed Oops mode: 32-bit
Connected to the target VM, address: '127.0.0.1:64611', transport: 'socket'
[20.740s][info ][gc,start ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
[20.740s][info ][gc,task ] GC(0) Using 2 workers of 10 for evacuation
[20.741s][info ][gc,phases ] GC(0) Pre Evacuate Collection Set: 0.0ms
[20.741s][info ][gc,phases ] GC(0) Evacuate Collection Set: 1.4ms
[20.741s][info ][gc,phases ] GC(0) Post Evacuate Collection Set: 0.2ms
其实还有很多参数,自行百度搜索。。。。。
7.2 垃圾回收器参数总结
每个垃圾回收和其对应的垃圾回收参数
参数 | 描述 |
---|---|
UseSerialGC | Client 模式 Serial+Serial Old 回收 |
UseParNewGC | 9后不支持 ParNew +Serial Old |
UseConcMarkSweepGC | ParNew+CMS+Serial Old ;CMS失败 后Serial Old |
UseParallelGC | 9之前默认 Parallel Scavenge + Serial Old |
UseParallelOldGC | Parallel Scavenge + Parallel Old |
SurvivorRatio | Eden :survivor 默认是8 代表8:1 |
PretenureSizeThreshold | byte 超过直接老年代 |
MaxTenuringThreshold | 每次 MinorGC 年龄+1 ,超过这个直接老年代 |
UseAdaptiveSizePolicy | 动态堆大小和动态进入老年代的年龄 |
HandlePromotionFailure | 允许老年代担保失败 |
ParallelGCThreads | 并行GC线程 数量 |
GCTimeRatio | 默认99 ,就是1/(1+99) = 1%,允许1% GC 时间;(Parallel Scavenge) |
MaxGCPauseMillis | 最大回收时间 |
CMSInitiatingOccupancyFraction | 内存使用率超过多少开始GC |
UseCMSCompactAtFullCollection | CMS 后是否需要整理内存(9后废除) |
CMSFullGCsBeforeCompaction | CMS 多少次后整理内存( 9后废除) |
UseG1GC | 9后默认垃圾回收 |
G1HeapRegionSize | byte Region Size |
MaxGCPauseMillis | G1 目标时间,默认200ms |
G1NewSizePrecent | 新生代最小值,默认 5% |
G1MaxNewSizePrecent | 新生代最大值,默认 60% |
ParallelGCThreads | 并行GC线程 数量 |
ConcGCThreads | 和用户线程一起的时候GC线程 数量 |
InitiatingHeapOccupancyPrecent | 默认 45% 表示non_young_capacity_bytes 堆占比 |
UseShenandoahGC | ShenandoahGC openJDK 才能用 |
ShenandoahGCHeuristics | 何时启动GC daptive(**默认**)、static、compact、passive(**diagnostic用**)、aggressive(**diagnostic用**) |
UseZGC | 使用ZGC |
UseNUMA | 启动NUMA 内存分配支持 |
8.内存分配和回收策略
Java的内存管理:自动给对象分配内存和回收对象内存;
JVM
初始分配的内存由-Xms
指定,默认是物理内存的1/64; 最大分配的内存由-Xmx
指定,默认是物理内存的1/4。默认空余堆内存小于40%时,就会增大堆直到-Xmx
的最大限制;空余堆内存大于70%时,减少堆直到-Xms
的最小限制。服务器一般设置-Xms、-Xmx
相等以避免GC
后调整堆的大小。
JVM使用-XX:PermSize
设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize
设置最大非堆内存的大小,默认是物理内存的1/4。
8.1 对象优先在Eden
大部分情况是,新的对象在Eden 区中分配。当Eden 没有空间,就发起Minor GC
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* young 10 = 8+1+1 Old :10
*/
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC 4M 无法进入Eden(此时6/8) 直接进入old
}
[GC (Allocation Failure) [PSYoungGen: 6439K->911K(9216K)] 6439K->5015K(19456K), 0.0031737 secs] [Times: user=0.09 sys=0.02, real=0.00 secs]
Heap
PSYoungGen total 9216K, used 7460K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 79% used [0x00000000ff600000,0x00000000ffc656f8,0x00000000ffe00000) //Eden(此时6/8) 大约6M
from space 1024K, 88% used [0x00000000ffe00000,0x00000000ffee3ca0,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) //Old 使用4M
object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
Metaspace used 3091K, capacity 4556K, committed 4864K, reserved 1056768K
class space used 324K, capacity 392K, committed 512K, reserved 1048576K
8.2大对象进入老年代
大对象设置直接进入老年代;因为在年轻代的话,可能需要很多代之后才能进入老年代,带来了很多的复制成本;
8.3 长期存活对象进入老年代
如果垃圾回收器采用分代策略管理堆内存,每次Minor GC
后年龄+1 等到15后升级到老年代;
8.4动态年龄判定
如果Survivor空间中相同年龄的对象大小大于Survivor 一半,大于等于该年龄的就直接老年代,不需要等前面设置的最大年龄
8.5 空间担保分配
Minor GC
前;虚拟机检查老年代连续空间是否大于新生代所有空间;如果大于,说明Minor GC
安全;如果小于,再看参数HandlePromotionFailure
是否允许担保失败,如果 true ;对比老年代最大连续空间 和 历次晋升老年代的平均内存大小,大于 可以Minor GC
,小于Full GC
;