HotSpot GC
2020-02-06 13:42:36 0 举报
AI智能生成
HotSpot GC
作者其他创作
大纲/内容
基本概念
串行(Serial)
单个 gc thread
work thread 挂起
并行(Parallel)
多个 gc thread
work thread 挂起
并发(Concurrent)
单个或多个 gc thread
work thread 正常执行
STW(Stop The World)
除了垃圾收集线程外,其它应用线程被挂起
引用(Reference)
强引用
弱引用
软引用
虚引用
JVM内存运行时布局
划分1
线程独享
本地方法栈(Native Method Stack)
虚拟机栈(VM Starck)
栈帧(Frame)组成
Local Variable Array
Operand Stack
Reference to Constant Pool
程序计数器(Program Counter Register)
记录线程当前的指令
eg:当前执行到哪一条指令,下一条该取哪条指令
线程共享
方法区(Method Area)
类结构
类成员定义
静态成员
常量池区(Runtime Constant Pool)
堆(Heap)
Java内存模型
屏蔽掉各种硬件和操作系统的内存访问差异
划分
主内存
工作内存
特性
原子性
可见性
有序性
对象的内存布局
Java Object
对象头
Mark Word
Class Pointer
对象中的实际数据
对齐填充
Array Object
对象头
Mark Word
Class Pointer
Length
对象中的实际数据
对齐填充
异常
StackOverflowError
OutOfMemoryError
划分2
栈
程序执行逻辑相关的指令数据
堆
对象实例相关的数据
哪些内存需要回收
无需GC
原因
编译期即可知道内存占用大小
内存的分配和回收都具有确定性
特点
随线程而生,随线程而灭
范围
程序计数器
虚拟机栈
本地方法栈
需要GC
原因
运行期间动态创建
内存的分配和回收具有不确定性
范围
虚拟机栈
静态区
常量池
查找内存中不再使用的对象
引用计数法
「循环引用」内存泄漏
可达性分析
GC Roots
虚拟机栈
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈
JNI引用的对象
方法区
静态属性,引用的对象
常量属性,引用的对象
释放这些对象占用的内存,才能再次使用,防止内存泄漏
如何回收内存
性能
吞吐量
用户线程运行时间 / JVM运行总用时
最大暂停时间
堆使用效率
访问的局部性
把具有引用关系的对象安排在堆中较近的位置,从而提高在缓存中读取到想利用的数据的概率,令用户线程高速运行
算法
标记-清除(Mark-Sweep)
标记-压缩(Mark-Compact)
复制(Copying)
分代(Generational GC)
以上三种算法各有优缺点,在现代JVM往往综合使用
指标对比
时间
空间
是否移动
分代(根据对象存活周期不同)
新生代
收集器
Serial
复制
单线程
串行
ParNew
复制
多线程
并行
优化
频率高
增大新生代
时间长
减小新生代
Parallel Scavenge
复制
多线程
并行
关注吞吐量
内存结构
Eden区
新申请的对象,分配在这个区
Survivor 0 区
复制策略使用的复制区
Survivor 1 区
复制策略使用的复制区
什么时候触发Young GC
新生代空间不足时:新分配对象时,发现 Eden 空间不足,此时,会触发 Young GC
具体过程
Eden 区存活对象:复制到 Survivor 1 区
Survivor 2 区中存活对象:复制到 Survivor 1 区
Eden 区 和 Survivor 1 区:清空
什么时候触发对象进入「老年代」
对象年龄
对象在「新生代」中survivor 区,存活的年龄,达到阈值(默认为 15),则,进入「老年代」
-XX:MaxTenuringThreshold
同龄对象过半
年龄动态计算,对象在「新生代」中survivor 区,同龄对象大小之和,超过了 survivor 区的 50%,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值,集体进入「老年代」
引入动态年龄计算,主要基于如下两点考虑
如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件
MaxTenuringThreshold设置的过大,原本应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,一旦溢出发生,Eden+Svuvivor中对象将不再依据年龄全部提升到老年代,这样对象老化的机制就失效了
MaxTenuringThreshold设置的过小,“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的Major GC。分代回收失去了意义,严重影响GC性能
相同应用在不同时间的表现不同:特殊任务的执行或者流量的变化,都会导致对象的生命周期分布发生波动,那么固定的阈值设定,因为无法动态适应变化,会造成和上面相同的问题
大对象
大对象,在「新生代」放不下,直接进入「老年代」
老年代引用新生代的对象,GC怎么处理
因为跨代引用,这样在Young GC必须扫描整个老年代来判断对象是否存活
怎么避免,经过统计信息显示,老年代持有新生代对象引用的情况不足1%
卡标记 Card Marking
卡表 Card Table,字节数组结构,卡表的每个标记项为1个字节
老年代划分为卡页,每个512B,如果老年代对象发生了修改,或者老年代对象指向了新生代对象,就把这个老年代对象所在的 Card 标记为脏 dirty。Young GC 时,dirty card 加入待扫描的 GC Roots 范围,避免扫描整个老年代
单独存储一个卡表,标识哪个卡页存在指向年轻代的引用
存在指向年轻代对象的卡页,对应的卡表记录,被标记为 dirty
扫描 dirty的卡表记录,从而避免全堆扫描
我引用了谁 points-out结构
标识了哪个「老年代」区域,存在指向「年轻代」对象的引用
写屏障 Write Barrier
Incremental update,只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来
问题
无条件写屏障带来的性能开销
每次对引用的更新,无论是否更新了老年代对新生代对象的引用,都会进行一次写屏障操作
高并发下虚共享(false sharing)带来的性能开销
假设CPU缓存行大小为64字节,由于一个卡表项占1个字节,这意味着,64个卡表项将共享同一个缓存行
HotSpot每个卡页为512字节,那么一个缓存行将对应64个卡页一共64*512=32KB
如果不同线程对对象引用的更新操作,恰好位于同一个32KB区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序性能
解决
先检查卡表标记,只有当该卡表项未被标记过才将其标记为dirty,这样可以减少并发写操作
JDK7 -XX:+UseCondCardMark
反之不成立
老年代的 GC 是低频操作
新生代,绝大多数对象,存活时间非常短
如果记录「新生代」到「老年代」的引用,会耗费比较多的存储空间
关于 CMS 垃圾收集器:并发标记阶段,应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如
新生代的对象晋升到老年代
直接在老年代分配对象
老年代对象的引用关系发生变更
...
老年代
收集器
Serial Old
标记-整理
单线程
串行
Parallel Old
标记-整理
多线程
并行
CMS
标记-清除
多线程
并发
收集步骤
初始标记 CMS-initial-mark
标记老年代中所有的GC Roots引用的对象
标记老年代中被年轻代中活着的对象引用的对象(初始标记也会扫描新生代)
STW
并发标记 CMS-concurrent-mark
从初次标记收集到的「根」对象引用开始,遍历所有能被引用的对象
垃圾回收线程和应用线程同时运行,受到影响的老年代对象所在的card会被标记为dirty,用于重新标记阶段扫描
新生代晋升到老年代的对象
老年代新分配的对象(大对象直接进入老年代)
用户线程更新的对象
并发预清理 CMS-concurrent-preclean
目的是为了让重新标记阶段的STW尽可能短
目标是在并发标记阶段被应用线程影响到的老年代对象
老年代中card为dirty的对象
幸存区(from和to)中引用的老年代对象
可中断的并发预清理 CMS-concurrent-abortable-preclean
减轻重新标记阶段的工作量,可中断预清理的价值
在进入重新标记阶段之前尽量等到一个Minor GC,尽量缩短重新标记阶段的停顿时间
会在Eden达到50%的时候开始,这时候离下一次minor gc还有半程的时间,避免短时间内连着的两个停顿
如何确定老年代的对象是活着的
必须扫描新生代来确保。这也是为什么CMS虽然是老年代的gc,但仍要扫描新生代的原因,从GC日记可以看出
这样全量的扫描新生代和老年代必然会慢,发生一次Minor GC
不会进入abortable-preclean阶段
-XX:CMSScheduleRemarkEdenSizeThreshold > 2m,当eden使用大于此值时
-XX:CMSScheduleRemarkEdenPenetration >= 50%,当eden使用率大于等于此值时
进入abortable-preclean阶段(可能会执行多次),怎么中断
-XX:CMSMaxAbortablePrecleanLoops > 0,执行的次数超过了这个值
-XX:CMSMaxAbortablePrecleanTime > 5000(ms),执行可中断预清理的时间超过了这个值
当中止此阶段,进入Remark,这个时候新生代仍然有很多对象,对这种情况CMS提供CMSScavengeBeforeRemark参数,用来保证Remark前强制进行一次Minor GC
如何处理并发阶段被修改了的对象
场景
引用关系变化,导致被错误回收(A->B->C变成A->C)
三色标记法(已经有的对象的变更)
白色
还没有搜索过的对象(会被当成垃圾对象)
灰色
正在搜索的对象
黑色
搜索完成的对象(不会当成垃圾对象,不会被GC)
描述说明:
「写入屏障」
A已经被标记了黑色,那么用户线程改动A->C的时候,会把C变成灰色,这样以后就可以搜索C了
GC 线程和 用户线程并发的时候,用户线程把失效的对象又至为有效,这个时候怎么处理
重新标记 CMS-final-remark
重新扫描堆中的对象,进行可达性分析,标记活着的对象,如果预清理的工作没做好,这一步扫描新生代的时候就会花很多时间,导致这个阶段的停顿时间过长。这个过程是多线程的
新生代的对象
GC Roots
前面被标记为dirty的card对应的老年代对象
「浮动垃圾」
STW
并发清理 CMS-concurrent-sweep
这个阶段产生新的垃圾在此次GC无法清理
会产生大量空间碎片
并发重置 CMS-concurrent-reset
重置内部的数据结构
什么时间会出现 Full GC
Perm空间不足
-XX:PermSiz和-XX:MaxPermSize设置成一样
concurrent mode failure
并发标记、清除过程,work thread 在运行,申请「老年代」空间可能失败,降级为Serial Old 收集器,导致STW,直接进行Serial Old GC
promotion failed
当进行 Young GC 时,有部分新生代代对象仍然可用,但是S0或S1放不下,因此需要放到老年代,但此时老年代空间无法容纳这些对象
统计得到的Young GC晋升到老年代的平均大小大于老年代的剩余空间
主动触发Full GC(执行jmap -histo:live [pid])来避免碎片问题
优化(解决办法)
开启压缩,减少因为内存碎片,导致的 CMS 退化为 Serial Old 概率
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction = 4
多少次 full gc 进行一次压缩,一般设置为4
降低触发 full gc 的阈值:老年代已使用内存空间占比,为了预留足够的内存
-XX:CMSInitiatingOccupancyFraction = 92%
默认92%,建议68%
-XX:+CMSScavengeBeforeRemark
会在重新标记阶段前强制进行一次 Young GC,从而提高老年代的GC速度
-XX:ConcGCThreads
CMS 默认的回收线程数是( CPU 个数+3)/4,当 CPU 大于4个时,保证回收线程占用至少25%的 CPU 资源,这样用户线程占用75%的 CPU
触发条件
-XX:+UseCMSInitiatingOccupancyOnly(指定用-XX:CMSInitiatingOccupancyFraction来控制)
未设置
预测 CMS GC 完成所需要的时间大于预计的老年代将要填满的时间(基于历史的 CMS GC 统计指标)
第一次统计数据还没有,检查老年代占比到50%
已设置
老年代空间使用占比大于-XX:CMSInitiatingOccupancyFraction = 92%
增量 GC失败,Young Gen晋升失败
metaspace扩容
应用程序主动GC
目标
CMS的设计聚焦在获取最短的时延,为此它“不遗余力”地做了很多工作,包括尽量让应用程序和GC线程并发、增加可中断的并发预清理阶段、引入卡表等,虽然这些操作牺牲了一定吞吐量但获得了更短的回收停顿时间
常用组合
吞吐率优先(计算密集型)
Parallel Scavenge + Parallel Old
+XX:+UseParallelOldGC
参数
MaxGCPauseMillis
gc时间的最大值
GCTimeRatio
gc时间占总时间的比例
UseAdaptiveSizePolicy
gc内存分配的"自适应调节策略"
新生代大小
Eden与Survior的比例
晋升老年代的对象年龄
响应时间优先(交互型)
ParNew + CMS
+XX:+UseConcMarkSweepGC
Full GC、Magjor GC、Minor GC、Young GC 之间的关系
Full GC == Major GC指的是对老年代/永久代的stop the world的GC
Full GC的次数 = 老年代GC时 STW 的次数
Full GC的时间 = 老年代GC时 STW 的总时间
CMS ≠ Full GC,CMS 分为多个阶段,只有 STW 的阶段被计算到了Full GC的次数和时间,而和业务线程并发的 GC 的次数和时间则不被认为是Full GC
Full GC本身不会先进行 Young GC,我们可以配置,让 Full GC之前先进行一次 Young GC,因为老年代很多对象都会引用到新生代的对象,先进行一次Young GC可以提高老年代GC的速度
各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点
活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小
总大小,3-4 倍活跃数据的大小
新生代,1-1.5 活跃数据的大小
老年代,2-3 倍活跃数据的大小
永久代,1.2-1.5 倍Full GC后的永久代空间占用
分区(将堆空间划分连续不同的小空间)
G1 响应时间优先同时也要兼顾吞吐率
-XX:+UseG1GC
内存布局
Region(取值范围1-32M,大小为2的n次方)
Eden
Survivor
Old
Humongous
通常 >= 1/2 Region Size,且只有 Full GC 阶段,才会回收 H 区,避免了频繁扫描、复制 / 移动大对象
JVM最多支持2000个区域,可推算G1能支持的最大内存为2000*32M = 62.5G
新生代/老年代不要求连续内存空间
执行过程
全局并发标记(Global Concurrent Marking)
初始标记 STW initial marking
扫描根集合,标记所有从根集合可直接到达的对象并将它们的字段压入扫描栈。在分代式G1模式中,初始标记阶段共用 Young GC 的暂停,因而没有额外的、单独的暂停阶段
并发标记 concrrent marking
这个阶段可以并发执行,GC 线程 不断从扫描栈取出引用,进行递归标记,直到扫描栈清空
最终标记 STW final marking(Remark)
重新标记写入屏障( Write Barrier)标记的对象,这个阶段也进行弱引用处理(reference processing)
清理 STW cleanup
统计每个 Region 被标记为活的对象有多少,如果发现完全没有活对象的 Region 就会将其整体回收到可分配 Region 列表
拷贝存活对象(Evacuation)
STW 负责把一部分 Region 里的活对象拷贝到空 Region 里去,然后回收原本的 Region 的空间
分代回收
Young GC
选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销
Mixed GC
选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region
注意:Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap
辅助结构
SATB
Snapshot-At-The-Beginning,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 那么它是怎么维持并发GC的正确性的呢?
RSet
Remembered Set,每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)
RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index
谁引用了我 points-into结构
CSet
Collection Set,记录了GC要收集的Region集合,集合里的Region可以是任意年代的
主要参数
-XX:InitiatingHeapOccupancyPercent=45,表示垃圾对象在整个G1堆内存的空间占比
-XX:MaxGCPauseMillis,新生代最小值,默认值5%
-XX:G1NewSizePercent,新生代最大值,默认值60%
-XX:ParallelGCThreads,STW期间,并行GC线程数
-XX:ConcGCThreads=n,并发标记阶段,并行执行的线程数
-XX:G1HeapRegionSize=n,设置Region大小,并非最终值
OOM触发条件
GCHeapFreeLimit,可用空间占比,默认98%
GCTimeLimit,GC时间占比,默认98%
收藏
收藏
0 条评论
下一页