JVM的GC 策略及优化
2023-04-21 01:12:30 0 举报
AI智能生成
JVM的GC 策略及优化
作者其他创作
大纲/内容
四、JVM调优
目的
减少GC的频率和Full GC的次数
名词概念
Full GC
概念
对整个堆进行整理,包括新生代、老年代、永久代。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数
Minor GC,Major GC,Full GC的区别
Full GC = Minor GC(Young GC) + Major GC(Old GC)+Metaspace GC
什么时候会触发FullGC
老年代空间被写满
元空间不足
Minor GC后存活的对象晋升到老年代的平均大小大于老年代的剩余空间
CMS GC时出现promotion failed和concurrent mode failure
调用 System.gc() 也会安排一次 Full GC
STW(Stop the World)
概念
指在执行垃圾回收的过程冻结所有业务线程的运行,直到垃圾回收线程执行结束
为什么要STW?
GC发生时,GC线程和业务线程操作的是同一块内存区域,如果业务线程不停止,GC线程前脚刚标记完一块区域,业务线程就在这块区域创建了一个对象,当回收的时候,可能这个对象还是存活的,那么会导致该对象被清除了,对于业务而言会出现很大的影响,存在线程安全问题
垃圾收集线程与业务线程一起运行的后果
该回收的垃圾没有回收(并发情况下的对象突然失去引用,但被GC roots标记为可达对象),不该回收的对象被回收(并发情况下的对象突然被其他对象引用,但没有被GC roots标记为可达对象)
STW的场景
整理内存碎片
GC Roots根节点枚举
内存泄漏与内存溢出的区别
内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。
内存溢出(OOM)的原因
大并发情况下
内存泄露导致内存溢出
调优参数
1. 常见标准参数
-version
查看版本
-help
检索出所有的标准参数
-X参数
-Xint
解释执行
-Xcomp
第一次使用就编译成本地代码
-Xmixed
混合模式,JVM自己来决定
2. -XX参数
1. Boolean类型
格式
-XX:[+-]<name>
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
2. 非Boolean类型
格式
-XX<name>=<value>
-XX:MaxGCPauseMillis=500
3. 其他参数
-Xms1000M等价于-XX:InitialHeapSize=1000M
-Xmx1000M等价于-XX:MaxHeapSize=1000M
-Xss100等价于-XX:ThreadStackSize=100
4. 查看参数
java -XX:+PrintFlagsFinal -version > flags.txt
sz flags.txt
5. 设置参数的常见方式
a. 开发工具中设置比如IDEA,eclipse
b. 运行jar包的时候:java -XX:+UseG1GC xxx.jar
c. web容器比如tomcat,可以在脚本中的进行设置
d. 通过jinfo实时调整某个java进程的参数(参数只有被标记为manageable的flags可以被实时修改)
6. 常用参数含义
参数
-XX:CICompilerCount=3
最大并行编译数
如果设置大于1,虽然编译速度会提高,但是同样影响系统稳定性,会增加JVM崩溃的可能
-XX:InitialHeapSize=100M
初始化堆大小
简写-Xms100M
-XX:MaxHeapSize=100M
最大堆大小
简写-Xms100M
-XX:NewSize=20M
设置年轻代的大小
-XX:MaxNewSize=50M
年轻代最大大小
-XX:OldSize=50M
设置老年代大小
-XX:NewRatio
新老生代的比值
比如-XX:Ratio=4,则表示新生代:老年代=1:4,也就是新生代占整个堆内存的1/5
-XX:SurvivorRatio
两个S区和Eden区的比值
比如-XX:SurvivorRatio=8,也就是(S0+S1):Eden=2:8,也就是一个S占整个新生代的1/10
-XX:+HeapDumpOnOutOfMemoryError
启动堆内存溢出打印
当JVM堆内存发生溢出时,也就是OOM,自动生成dump文件
-XX:HeapDumpPath=heap.hprof
指定堆内存溢出打印目录
表示在当前目录生成一个heap.hprof文件
-XX:MaxTenuringThreshold=6
提升年老代的最大临界值
默认值为 15
-XX:MetaspaceSize=50M
设置方法区大小
-XX:MaxMetaspaceSize=50M
方法区最大大小
-XX:+UseParallelGC
使用UseParallelGC
新生代,吞吐量优先
-XX:+UseParallelOldGC
使用UseParallelOldGC
老年代,吞吐量优先
-XX:+UseConcMarkSweepGC
使用CMS
老年代,停顿时间优先
-XX:+UseG1GC
使用G1GC
新生代,老年代,停顿时间优先
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:g1-gc.log
打印出GC日志
可以使用不同的垃圾收集器,对比查看GC情况
-Xss128k
设置每个线程的堆栈大小
经验值是3000-5000最佳
-XX:InitiatingHeapOccupancyPercent
启动并发GC周期时堆内存使用占比
G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比.值为 0 则表示”一直执行GC循环”.默认值为 45
-XX:G1HeapWastePercent
允许的浪费堆空间的占比
默认是10%,如果并发标记可回收的空间小于10%,则不会触发MixedGC。
-XX:MaxGCPauseMillis=200ms
G1最大停顿时间
暂停时间不能太小,太小的话就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。
-XX:ConcGCThreads=n
并发垃圾收集器使用的线程数量
默认值随JVM运行的平台不同而不同
调优命令行工具有哪些?
1. jps
全称JVM Process Status Tool
查看java进程
2. jstat
全称JVM statistics Monitoring
用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据
查看类装载信息
jstat -class PID 1000 10
查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次
查看垃圾收集信息
jstat -gc PID 1000 10
3. jmap
全称JVM Memory Map
用于生成heap dump文件
打印出堆内存相关信息
jmap -heap PID
4. jhat
全称JVM Heap Analysis Tool
与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
5. jstack
查看线程堆栈信息
jstack PID
举例
分支主题
运行结果
jstack分析
把打印信息拉到最后可以发现
6. jinfo
全称JVM Configuration info
实时查看和调整虚拟机运行参数
查看用法
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID
修改
参数只有被标记为manageable的flags可以被实时修改
jinfo -flag [+|-] PID
jinfo -flag <name>=<value> PID
查看曾经赋过值的一些参数
jinfo -flags PID
4种JVM调优工具
JvisualVM(jdk自带)
分析内存快照、线程快照,监控内存变化、GC变化等
Jconsole(jdk自带)
用于对JVM中内存,线程和类等的监控
MAT
查找内存泄漏和减少内存消耗
阿里Arthas
运行时优化
dump文件分析
工作遇到Java heap space报错,进行分析
0:查看内存当前进程 ps -aux|grep serviceName
1:生成JVM运行时快照DUMP
jmap -dump:live,format=b,file=heap-dump.bin<pid>
出现OOM时生成堆dump
:-XX:+HeapDumpOnOutMemoryError
生出堆文件地址
-XX:HeapDumpPath=/opt/jvmlogs/
2.DUMP文件分析
使用JvisualVM分析dump文件
看哪些数据结构占用空间大小,进行数据分析
JVM内部的优化逻辑
方法内联
为什么会出现方法内联呢?
逃逸分析
什么是逃逸分析?
逃逸分析不是直接的优化手段,而是代码分析手段
逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
什么是“对象逃逸”?
对象逃逸的本质是对象指针的逃逸
当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作对象的指针(或对象)的逃逸(Escape)
public User doSomething1() {
User user1 = new User ();
user1 .setId(1);
user1 .setDesc("xxxxxxxx");
// ......
return user1 ;
}
public void doSomething2() {
User user2 = new User ();
user2 .setId(2);
user2 .setDesc("xxxxxxxx");
// ......
}
死锁排查
计算机系统中的死锁
竞争不可抢占性资源引起死锁
竞争可消耗资源引起死锁
进程推进顺序不当引起死锁
死锁的定义
定义
如果一组进程中的每一个进程都在等待仅由该进程中的其他进程才能引发的事件,那么该组进程是死锁的
产生死锁的必要条件
互斥条件
请求和保存条件
不可抢占条件
循环等待条件
如果每个资源只有一个实例,则环路等待条件是死锁存在的充分必要条件
处理死锁的方法
预防死锁
静态方法
在进程执行前采取的措施,通过设置某些限制条件,去破坏产生死锁的四个条件之一,防止发生死锁。
预防死锁的策略
破坏请求和保存条件
第一种协议
所有进程在开始运行之前,必须一次性地申请其在整个运行过程中所需的全部资源
优点
简单,易行,安全
缺点
资源被严重浪费,严重地恶化了资源的利用率
使进程经常会发生饥饿现象
第二种协议
它允许一个进程只获得运行初期所需的资源后,便开始运行。进程运行过程中再逐步释放已分配给自己的,且已用毕的全部资源,然后再请求新的所需资源
破坏不可抢占条件
当一个已经保存了某些不可被抢占资源的进程,提出新的资源请求而不能得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请
破坏循环等待条件
对系统所以资源类型进行线性排序,并赋予不同的序号
例如令输入机的序号为1,打印机序号为2,磁盘机序号为3等。所有进程对资源的请求必须严格按资源序号递增的次序提出。
避免死锁
避免死锁的策略
系统安全状态
安全状态
某时刻,对于并发执行的n个进程,若系统能够按照某种顺序如<p1,p2…pn>来为每个进程分配所需资源,直至最大需求,从而使每个进程都可顺利完成,则认为该时刻系统处于安全状态,这样的序列为安全序列
安全状态之例
由安全状态向不安全状态的转换
利用银行家算法避免死锁
含义
每一个新进程在进入系统时,它必须申明在运行过程中,可能需要每种资源类型的最大单元数目,其数目不应超过系统所拥有的资源总量。当进程请求一组资源时,系统必须首先确定是否有足够的资源分配给该进程。若有,再进一步计算在将这些资源分配给进程后,是否会使系统处于不安全状态。如果不会,才将资源分配给它,否则让进程等待
银行家算法中的数据结构
可用资源向量 Available[m]:m为系统中资源种类数,Available[j]=k表示系统中第j类资源数为k个。
最大需求矩阵 Max[n,m]:n为系统中进程数,Max[i,j]=k表示进程i对j类资源的最大需求数为中k。
分配矩阵 Allocation[n,m]:它定义了系统中每一类资源当前已分配给每一进程资源数, Allocation[i,j] = k表示进程i已分得j类资源的数目为k个。
需求矩阵 Need[n,m]:它表示每个进程尚需的各类资源数,Need[i,j]=k 表示进程i 还需要j类资源k个。Need[i,j]=Max[i,j] - Allocation[i,j]
银行家算法
安全性算法
银行家算法之例
死锁的检测与解除
死锁的检测
资源分配图
简化步骤
选择一个没有阻塞的进程p
将p移走,包括它的所有请求边和分配边
重复步骤1,2,直至不能继续下去
死锁定理
若一系列简化以后不能使所有的进程节点都成为孤立节点
检测时机
当进程等待时检测死锁 (其缺点是系统的开销大)
定时检测
系统资源利用率下降时检测死锁
死锁检测中的数据结构
死锁的解除
抢占资源
终止(或撤销)进程
终止进程的方法
终止所有死锁进程
逐个终止进程
代价最小
进程的优先级的大小
进程已执行了多少时间,还需时间
进程在运行中已经使用资源的多少,还需多少资源
进程的性质是交互式还是批处理的
付出代价最小的死锁解除算法
是使用一个有效的挂起和解除机构来挂起一些死锁的进程
解除死锁
三、不分代垃圾收集器
G1回收器
逻辑分代,物理不分代
分支主题
概述
面向服务端的回收器
G1适用于多处理器和大容量内存服务器的垃圾收集器,满足GC停顿时间要求和高吞吐量
特点分析
并行+并发
G1能充分利用多CPU、多核环境的优势来缩短STW的停顿时间
分代收集堆
G1收集器不需要与其它收集器配合就可以独立管理整个
可预测的停顿
这是G1相对于CMS的优势,降低停顿时间是二者共同的关注点,但G1除了降低停顿时间外,还能建立可预测的停顿时间模型
G1收集器去跟踪各个 Region 里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值。在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。 这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
Region
概念
G1将新生代,老年代的物理空间划分取消了,取而代之的是G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了
Humongous区域
概念
用于存储巨型对象(一个对象占用的空间超过了分区容量50%以上)如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC
为何设置H区?
如果短期的大对象放入老年代,会影响效率
记忆集RSet(RememberedSet)
概念
G1 的记忆集在存储结构的本质上是一种哈希表, Key 是别的 Region 的起始地址,Value 是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂
RSet的数据结构
稀疏表
通过哈希表方式(哈希表底层使用数组)来存储
细粒度表
通过数组来存储,每个数组元素指向引用者分区中512直接内存块对本分区的引用情况
粗粒度位图
通过位图来指示,每1位表示对应的分区有引用到本分区
需要使用RSet保存引用关系的情况
分支主题
分区内部有引用关系
新生代分区到新生代分区之间有引用关系
新生代分区到老生代分区之间有引用关系
老生代分区到新生代分区之间有引用关系
老生代分区到老生代分区之间有引用关系
回收集CSet(Collection Set)
概念
垃圾收集的目标范围
在 G1 收集器出现之前的所有其他收集器,包括 CMS 在内,垃圾收集的目标范围要么是整个新生代(Minor GC) ,要么就是整个老年代(Major GC) ,再要么就是整个 Java 堆(Full GC) 。而 G1 跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称 CSet)进行回收
Mixed GC模式
不再以属于哪个分代为衡量标准,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是 G1 收集器的 Mixed GC 模式
-XX : G1HeapRegionSize
设定Region大小,取值范围1MB~32MB
工作流程
分支主题
Young GC(新生代)
根扫描
扫描根和本地变量
更新Rset
处理dirty card队列,更新Rset
处理Rset
检测年轻代指向老年代的对象
复制对象
年轻代复制到survival/old区
处理引用
处理软引用、弱引用、虚引用
G1与其它JVM垃圾回收器比较
相同
都有Young GC
不同
1. G1会根据预测时间动态地改变新生代的大小
2. 在任意时刻只有一部分老生代分区会被回收,并且这部分老生代分区将在下一次增量回收时与所有的新生代分区一起被回收,这就是混合回收(Mixed GC)
全局并发标记(老年代)
初始化标记
标记GCRoots直接可达对象,此过程会触发一个Minor GC,因为要更新Rset引用(修改TAMS指针的值)
TAMS(Top at Mark Start)的指针
G1为每一个Region设计了两个名为TAMS的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围
需要停顿线程,但耗时很短(STW)
并发标记
扫描标记在整个堆中存活的对象,找出要回收的对象
耗时长,但可与用户程序并发执行,引用对象存在可能变更,出现漏标(并发标记问题)
解决并发标记问题
STAB(原始快照)
STAB破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,并发扫描结束后,在将这些记录重新扫描一次。
分支主题
写屏障
使用写屏障,每次修改引用时都会将旧值写入long buffer。在最终标记时会修改快照
最终标记
用于处理并发阶段结束后仍遗留下来的最后少量的SATB记录
短暂停顿用户线程(STW)
筛选回收
1. 统计存活对象,统计的结果将会用来排序分区,以用于下一次的垃圾回收时分区的选择。
2. 交换标记位图,为下次并发标记做准备。
3. 把空闲分区放到空闲分区列表中。
暂停用户线程(STW)
G1相对于CMS的优势
1.G1在压缩空间方面有优势
2.G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
3.Eden,Survivor,Old区不再固定,在内存使用效率上来说更加灵活
4.G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
5.G1在回收内存后会马上同时做合并空闲内存的工作,而CMS默认是在STW的时候做
G1相对于CMS的劣势
因垃圾收集产生的内存占用(Footprint)
程序运行时的额外执行负载(Overload)
G1的意义
从 G1 开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率(Allocation Rate) ,而不追求一次把整个 Java 堆全部清理干净。这样, 应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。这种新的收集器设计思路从工程实现上看是从 G1 开始兴起的,所以说 G1 是收集器技术发展的一个里程碑。
衡量垃圾收集器的指标
内存占用(Footprint)
吞吐量(Throughput)
延迟(Latency)
ZGC
概述
ZGC(The Z Garbage Collector)是JDK 11(只能在64位的linux上使用)中推出的一款追求极致低延迟的实验性质的垃圾收集器
设计目标
停顿时间不超过10ms
支持8MB~4TB级别的堆,未来支持16TB
停顿时间不会随着堆的大小,或者活跃对象的大小而增加
特点
基于Region内存布局,不设分代
不设分代使得ZGC进行垃圾回收时,就是全量回收,也就是每发生一次垃圾回收就是一次FGC,而每次垃圾回收的停顿时间在10ms以下
颜色指针(color pointer)
通过设计不同的标记位区分不同的虚拟空间,而这些不同标记位指示的不同虚拟空间通过mmap映射在同一物理地址;颜色指针能够快速实现并发标记、转移和重定位
读屏障
实现并发标记和并发转移(整理)的处理
NUMA
尽量把对象分配在访问速度比较快的地方
与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进在,在ZGC 中出现Stop The World的情况更少
不同垃圾回收器的并发执行
工作流程
(1)初始标记
该步骤从根集合出发,找出根集合直接引用的活跃对象,并入栈;该步需要STW
(2)并发标记
根据初始标记找到的对象,作为并发标记的根对象,使用深度优先遍历对象的成员变量进行标记;并发标记需要解决标记过程中引用关系变化导致的漏标记问题
(3)最终标记和非强根并行标记
在并发标记结束后尝试终结标记动作,理论上并发标记结束后所有待标记的对象会全部完成,但是因为GC工作线程和应用程序线程是并发运行,所以可能存在GC工作线程执行结束标记时,应用程序线程又有新的引用关系变化导致漏标记,所以这一步先判断是否真的结束了对象的标记,如果没有结束就还会启动并行标记,所以这一步需要STW。另外,在该步中,还会对非强根(非强引用根对象)进行并行标记
4)并发处理非强引用和非强根并发标记
在非强引用处理时对定义了finalize()函数 [1] 的对象需要特殊处理,为此ZGC设计了特殊的标记。另外,ZGC为了优化停顿时间,把一些需要在STW中并行处理的任务并发运行,这都被设计成非强根的并发标记
(5)重置转移集合中的页面
实际上第一次垃圾回收时无须处理这一步
(6)回收无效的页面
在内存充足的情况下不会触发这一步
(7)并发选择对象的转移集合
转移集合中就是待回收的页面
(8)并发初始化转移集合中的每个页面
在后续重定位(也称为Remap)时需要的对象转移表(Forward Table)就是在这一步初始化的
(9)转移根对象引用的对象
该步需要STW
(10)并发转移
把对象移动到新的页面中,这样对象所在的老的页面中所有活跃对象都被转移了,页面就可以被回收重用
一、JVM垃圾回收算法
为什么要有 GC?
概念
GC 是垃圾收集的意思,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收
目的
Java 提供的GC 功能可以自动监测对是否超过作用域从而达到自动回收内存的目的,有效的防止内存泄露,有效的使用可以使用的内存
当Eden区或者S区空间不够用
老年代空间不够用
方法区空间不够用
System.gc()
System.gc()只是通知JVM要回收,什么时候回收由JVM决定。不建议手动调用该方法,因为GC消耗的资源比较大
涉及垃圾回收的线程
JavaThread
要执行的Java代码的线程
CompilerThread
执行JIT的线程
NameThread
JVM内部使用的线程
VMThread
JVM执行垃圾回收的同步线程,是JVM最关键的线程之一,主要的用途之一是处理垃圾回收
ConcurrentGCThread
并发执行垃圾回收任务的线程
分代收集理论
当前主流商业 JVM 的垃圾收集器,大多数都遵循了分代收集(Generational Collection)的理论进行设计,分代收集只是一种理论,一套指导方针,一套符合大多数程序运行实际情况的经验法则,它建立在几个分代假说之上
绝大多数对象朝生夕死
活得越久的对象,也就是熬过很多次垃圾回收的对象是越来越难以消亡的
存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的
记忆集(Remembered Set)记录老年代跨代引用的内存块
意义
虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的
例子
假设我们只回收新生代,但存在跨代引用问题(老年代引用新生代),如果标记时吧老年代中的引用对象全部标记,但回收时并没有回收老年代,则浪费时间。所以算法设计者做了这样的设计——用一个RSet记录从非收集部分指向收集部分的指针的集合,而这个集合描述就是对象的引用关系
在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本
卡表(Card Table)每个记录精确到一块内存区域,该区域内有对象含有跨代指针
1. 标记-清除算法
原理
第一步
从引用根节点开始遍历,标记所有被引用的对象,一般在对象的Header中记录为可达对象,未被标记的对象视为垃圾对象(步骤比较耗时)
第二步
清除掉没有标记的垃圾对象,释放出对应的内存空间
解决产生内存碎片时放置对象的分配策略(动态分区分配策略)
在遍历空闲链表的时候,一旦发现有大小等于需要的大小的内存空间之后,就立即返回该块空间分配给对象(遍历到合适的内存空间就停止)
在遍历空闲链表的时候,返回刚好等于需要大小的块(全部遍历再分配合适的内存空间)
在遍历空闲链表的时候,找出空闲链表中最大的分块,将其分割给申请的对象,其目的就是使得分割后的分块最大化,以便下次好分配,不过这种分配算法很容易产生很多很小的分块,这些分块很难被利用
分支主题
适合场景
存活对象较多的情况下比较高效
适用于老年代
缺点
容易产生内存碎片,再来一个比较大的对象时,会提前触发垃圾回收
典型情况:该对象的大小大于空闲表中的每一块大小,但是小于其中两块的和
扫描了整个空间两次(1.标记存活对象;2.清除没有标记的对象)
2. 标记-复制算法
原理
概括
划分内存,一分为二将内存划分为两块相等的区域,每次只使用其中一块
第一步
从引用根节点开始遍历,标记所有被引用的对象,一般在对象的Header中记录为可达对象,未被标记的对象视为垃圾对象(步骤比较耗时)
第二步
当其中一块内存使用完了,会把还存活的对象复制到另外一块上面,复制过去的对象会形成连续的地址空间,所以不会产生内存碎片,最后把已经使用过的内存空间清理一次清除掉
适合场景
存活对象较少的情况下比较高效
扫描了整个空间一次(标记存活对象并复制移动)
Eden区和两块Survivor空间,若Survivor空间不足容纳一次Minor GC后存活的对象,由老年代分配担保
缺点
需要一块空的内存空间
需要复制移动对象
标记-整理算法
原理
第一步
从引用根节点开始遍历,标记所有被引用的对象,一般在对象的Header中记录为可达对象,未被标记的对象视为垃圾对象(步骤比较耗时)
第二步
清除掉没有标记的垃圾对象,释放出对应的内存空间
第三步
整理算法
随机整理
概念
对象的移动方式和它们初始的对象排列及引用关系无关
双指针回收算法
概述
任意顺序整理实现简单,且执行速度快,但任意顺序可能会将原本相邻的对象打乱到不同的告诉缓存行或者虚拟内存页中(打乱到内存的各个地方),会降低赋值器的局限性。包括只能处理固定大小的对象,一旦对象大小不固定,就会增加其它的逻辑(扫描指针scan停止移动等待空闲指针free找到可以安放的内存空间)
流程
第一次遍历
移动位置但是并不更新标记(引用地址)
第二次遍历
更新标记
线性整理(一般不怎么用)
概念
将具有关联关系的对象排列在一起,不能很好解决内存碎片问题
滑动整理
概念
将对象"滑动"到堆的一端,从而"挤出"垃圾,可以保持对象在堆中原有的顺序
Lisq2算法
概念
需要在对象头用一个额外的槽来保存迁移完的地址,是一个三指针算法,并且可以处理不同大小的对象。但是需要三次遍历,并且由于对象大小不一样,所以需要额外的空间存储,而不是直接移动
流程
整理前
分支主题
第一次遍历
Free指针是为了留位置,而Scan对象是为了找存活对象
分支主题
第二次遍历
更新对象地址
分支主题
第三次遍历
移动对象
分支主题
单次遍历算法
概念
单次遍历算法的重点在于提前记录我们需要转移的位置
关键词:偏移量,标记向量以及内存索引号
流程
分支主题
注意:空闲列表不包括划分的堆内存,只做记录。单次遍历算法规定划分
空闲列表
概念
适用于堆内存不完整(内存碎片)的情况,已分配的内存和空闲内存相互交错,JVM通过维护一张内存列表记录可用的内存块信息,当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录,最常见的使用此方案的垃圾收集器就是CMS
指针碰撞
概念
假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞"
本质是在标记清除的基础上进行整理
所有现代的标记-整理回收器均使用滑动整理,它不会改变对象的相对顺序,也就不会影响赋值器的空间局部性。复制式回收器甚至可以通过改变对象布局的方式,将对象与其父节点或者兄弟节点排列的更近以提高赋值器的空间局部性。
适合场景
存活对象较多的情况下比较高效
适用于老年代
缺点
任意顺序算法只能处理单一大小的对象,或者针对大小不同的对象需要分批处理
整理过程需要2次或者3次遍历堆空间;对象头部可能需要一个额外的槽来保存迁移的信息
存活对象太多,移动对象效率极慢,而且对象移动操作必须全程暂停用户应用程序才能进行(Stop The World)
与标记-清除算法进行比较思考
标记-清除算法
不需要移动对象,产生内存碎片,内存分配复杂
标记-整理算法
需要移动对象,移动操作和内存回收变复杂
二、分代垃圾收集器
新生代的垃圾收集器
Serial
概述
单线程收集器
只会使用一个CPU或者一个垃圾收集器去完成收集工作
进行垃圾回收时,必须要暂停业务线程,直到收集结束
垃圾收集算法
标记复制算法
工作流程
分支主题
ParNew
概述
Serial收集器的多线程版本
多个线程进行垃圾回收
进行垃圾回收时,必须要暂停业务线程,直到收集结束
目前只有它能够和CMS收集器配合工作(并行)
并发与并行区别
并发
垃圾收集线程与业务线程一起执行的过程叫并发(操作系统是把CPU的时间段划分成时间片,通过操作系统的管理,把这些时间片依次轮流地分配给各个应用使用,常出现在单核CPU中)
并行
多个垃圾收集线程进行执行的过程叫并行(当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行)
垃圾收集算法
标记复制算法
特点及使用场景
CPU核数越多,越能发挥优势和资源利用
运行在Server模式下的首选新生代回收器
工作流程
分支主题
Parallel Scavenge
概述
并行的多线程收集器
吞吐量优先,可以精准地控制
吞吐量 = 程序运行时间/(程序运行时间+垃圾收集时间)
特点及使用场景
在吞吐量优先的场景下使用
垃圾收集算法
标记复制算法
工作流程
分支主题
老年代的垃圾收集器
Serial Old
概述
单线程回收器
主要用于client的jvm回收
垃圾收集算法
标记整理(压缩)算法
工作流程
分支主题
Parallel Old
概述
Parallel Scavenge的老年代回收器
垃圾收集算法
标记整理(压缩)算法
特点及使用场景
在吞吐量优先的场景下使用
工作流程
分支主题
CMS
概述
以最短响应为目标的回收器
适用于BS架构的环境
垃圾收集算法
标记清除算法(三色标记算法+增量更新)
优点和缺点分析
优点
并发收集,低停顿
缺点
CMS由于是标记清除算法,会产生内存碎片,需要加压缩参数
并发阶段会降低吞吐量
JDK8为什么不用CMS做为默认垃圾收集器?
CMS单线程或者双线程情况下效率很低
CMS会并发失败
CMS可中止的预处理会导致极限5S停顿
并发失败进入foregroud还会导致进入Full GC,全局MSC整理
CMS吞吐的设计并不是很优秀
工作流程
JDK1.7初始化标记阶段是串行;JDK8以后默认是并行
三色标记(遍历过程)
1.初始时,所有对象都在 【白色集合】中;
2.将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
3.从灰色集合中获取对象:
3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
3.2. 将本对象 挪到 【黑色集合】里面。
3.3重复步骤3.1和3.2,直至【灰色集合】为空时结束
4.结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收
(1)初始标记
标记直接相关联的第一个对象
(2)并发标记
①
②
③
并发标记出现的问题
浮动垃圾(多标)
对象消失(漏标)
漏标必须同时满足的两个条件
1.赋值器插入了一条或者多条从黑色对象到白色对象的新引用
2.赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
(3)并发预处理
并发标记阶段与用户线程一起运行,所以会产生新的对象之间的关系,新增关系的对象所在Card会被标记为Dirty Card
(4)可中止的预处理
(5)重新标记
解决并发标记问题
增量更新:Incremental Update
对Dirty Card 的对象能到达的对象进行标记,并清除Dirty Card标记
增量更新破坏的是第一个条件,在新增一条引用时,将该记录保存。实际的实现中,通常是将引用相关的节点进行重新标记
引用的对象是黑色对象,且新的引用的目标对象为灰色或白色,那么我们就把发出引用的对象涂成灰色,将在后续被重新扫描。
缺点
重新扫描黑改灰对象的引用链,效率较慢
写屏障
(6)并发清除
young区策略
为什么并发标记细化之后还会额外有两个流程出现呢?
当老年代被回收的时候,我们如何判断A对象是存活对象?(如图)
必须扫描新生代来确定,所以CMS虽然是老年代的垃圾回收器,却需要扫描新生代的原因。而全量扫描使得停顿时间变长,与CMS的"最短回收停顿时间"的设计理念不符。
怎么让回收变快?
CMS想到了一种方式,就是先进行新生代的垃圾回收,也就是一次young GC,回收完毕之后。是不是新生代的对象就变少了,那么再进行垃圾回收,是不是就变快了。
触发young GC的设置
CMSScheduleRemarkEdenSizeThreshold 默认值:2M
CMSScheduleRemarkEdenPenetration 默认值:50%
Eden空间使用超过2M的时候启动可中断的并发预处理(CMS-concurrent-abortable-preclean),到Eden空间使用率到达50%的时候中断(但不是结束),进入Remark(重新标记阶段),发生了minor GC,就中止预处理。
怎么知道young区什么时候发生minor GC?
CMSMaxAbortablePrecleanTime 默认为5S
只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,进入remark
如果在5S内还是没有执行Minor GC怎么办?
CMS提供CMSScavengeBeforeRemark参数,使remark前强制进行一次Minor GC
old区策略
记忆集(理论)
概念
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的数据结构
缺点
假如所有的老年代对象都有指针指向了新生代,那么我们需要维护整个老年代大小的记忆集,成本太高
实现
卡表
概念
卡表是使用一个字节数组实现:CARD_TABLE[],每个元素对应着其标识的内存区域一块特定大小的内存块,称为"卡页"。hotSpot使用的卡页是2^9大小,即512字节。一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0。GC时,只要筛选本收集区的卡表中变脏的元素加入GC Roots里。
脏卡(dirty card)
对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0。GC时,只要筛选本收集区的卡表中变脏的元素加入GC Roots里
如何在对象赋值(跨代引用)的那一刻去更新维护卡表?(谁来把卡页变脏?)
写屏障(Write Barrier)
应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作
并发模式失败CMS(concurrent mode failure)
概念
如果并发搜集器老年代中有效的空闲内存空间不能满足某一个内存的分配请求,此时应用会被暂停,并在此暂停期间开始垃圾回收(使用Serial Old垃圾收集器),直到回收完成才会恢复应用程序。这种无法并发完成搜集的情况就成为并发模式失败(concurrent mode failure)简单来说,进行并发标记的时候,内存不够了,这个时候我会进入STW,并且开始全局Full GC()
参数预防
-XX:CMSInitiatingOccupancyFraction
该值代表老年代堆空间的使用率,默认值为68。当老年代使用率达到此值之后,并行收集器便开始进行垃圾收集,该参数需要配合UseCMSInitiatingOccupancyOnly一起使用,单独设置无效。
-XX:+UseCMSInitiatingOccupancyOnly
该参数启用后,参数CMSInitiatingOccupancyFraction才会生效。默认关闭
看源码公式:
((100 - MinHeapFreeRatio) + (double)( CMSTriggerRatio * MinHeapFreeRatio) / 100.0) / 100.0
例如:当老年代达到: ((100 - 40) + (double) 80 * 40 / 100 ) / 100 = 92 %时,会触发CMS回收
减少并发模式失败ction
调低-XX:CMSInitiatingOccupancyFra
已经并发失败
jvm会放弃使用并发的CMS GC,转而采用单线程的Serial Old GC
收藏
收藏
0 条评论
下一页