JAVA垃圾回收基础
1、垃圾回收器怎么判断什么是垃圾
引用计数器:根据引用次数计算,存在循环引用的问题(A引用B,但A、B都没有其他对象引用,两者都是垃圾)
GC Root:可达性分析,从根向下搜索标记, 一般从栈帧的局部变量表开始,寻找他们的引用对象,再从引用对象找其他的变量;常见Root:线程栈变量、JNI指针、常量池、方法区静态变量
2、垃圾回收算法
Mark-Sweep:标记清除,分为标记和清除两个阶段,先标记,再清除。不能整理内存空间,存在碎片化内存空间,分配大内存可能触发新一轮垃圾回收
Coping:复制算法,内存分为两个大小相等的块,每次用其中一块,一块用完将存活对象移动到另一块并清除当前块。优点:解决内存碎片问题,只需要移动堆指针,简单高效;缺点:内存缩小一半;注:研究表明98%对象朝生夕死,因此使用大的Eden区+两块小的S区,将Eden存活对象放进其中一块S区,当S区空间不够,由老年代内存进行分配担保。
Mark-Compact:标记整理(带压缩),让存活对象移动,解决内存碎片问题,但效率略低;
3、三色标记算法
并发情况下一边工作一边标记;把对象逻辑上分成三种类型(黑、白、灰),从GC Root上开始标记
白色:还未标记的对象,即要被回收的垃圾
灰色:标记了对象,确认不是垃圾,但还未标记其成员变量。field
黑色:标记了对象,确认不是垃圾,成员变量也标记完成。field
漏标问题:对于漏标的对象,即白色对象从灰色对象中断开引用,此时黑色对象引用了白色对象,此时从黑色对象开始标记,发现黑色不用往下标记,但白色对象虽然被黑色对象引用了,却不能变成灰色,此时进行回收,造成异常。
CMS:三色标记用于并发标记阶段。对于漏标问题,CMS采用:Increamental Update,即将黑色标记变成灰色(黑+白=灰),但并发阶段,这种变色方式的标记会出现被其他线程修改的问题,导致了异常,这也是在1.9后CMS被废弃原因。
G1:三色标记称为STAB,Snapshot At Begining 初始化快照,通过对初始化标记放在栈中存储,被再次引用的对象从栈中拿出,剩余的被清理,因此不存在此类问题
ZGC:采用颜色指针,其类指针不再压缩是8字节即8*8=64位指针,18位空+4位颜色标记+42位对象 地址=64,因此2^42=4T,ZGC可以管理4T空间。
4、MinorGC和FullGC
MinorGC停顿时间远小于FullGC,是因为新生代存活的对象一般较少,标记时间较短;而老年代大多数对象是存活的,标记时间较长。
5、对象直接进入老年代
大对象直接进入老年代,JVM参数设置:-XX:PretenureSizeThreshold=10000000(默认1M) -XX:+UseSerialGC
一次minorGC之后,幸存的对象survivor区存放不了。经历过15次minorGC之后还存活。
老年代内存担保分配机制:
每次minorGc之前JVM会计算老年代剩余可用空间,若其小于年轻代所有对象之和.查看是否有设置参数: -XX:HandlePromotionFailure(JDK1.8默认设置)。如设置了,看老年代可用空间大小是否大于历次minorGC之后进入老年底的对象的平均大小,如小于则直接触发FullGC。
对象年龄动态判断:当前放对象的Survivor区域里,一批相同年龄段的对象的总大小大于这块空间的50%,那么大于等于这批对象年龄最大值的对象,可以直接进入老年代。这样是希望那些可能是长期存活的对象尽早进入老年代。对象动态年龄判断机制一般是在minor GC之后触发。
6、finalize()方法
两次标记:
第一次标记并进行一次筛选:筛选的条件是此对象是否有必要进行finalize()方法;如没有覆盖finalize(),则对象直接被回收。
第二次标记,如果覆盖了finalize(),则查看能否与引用链上的变量建立联系(把自己赋值给某个类变量或者对象的成员变量)。
7、方法区中的类什么时候被回收
所有实例对象都被回收,那就没有指针指向类;
类加载器classLoader被回收;
该类的所有的java.lang.Class对象没有在任何不地方被引用,也就是没有在任何地方被反射调用。无法在任何地方通过反射调用类的方法。
总结:实例全被回收、类加载器被回收、没有任何地方引用(包括反射)。
可以回收,但不是一定回收。
8、垃圾回收器种类
垃圾回收器发展:从分代带不分代,从支持小内存到大内存。
Serial 串行收集器:单线程、STW(stop-the-world);新生代,复制算法
Serial Old:Serial 的老年代版本,标记-整理算法,
缺点:单线程,STW
优点:简单高效
适用场景:适用于Client模式下的虚拟机。
Parallel Scavenge:新生代,复制算法,是Serial的并行(多线程)版本,高吞吐量可以高效利用CPU时间,尽快完成程序运算任务,适合后台任务;
与CMS比较:
CMS:尽可能缩短垃圾收集时用户停顿时间
Parallel Scavenge:可控制吞吐量,吞吐优先;吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集器时间);例:总运行时间100min,其中垃圾回收1分钟,则吞吐量=99%;停顿时间越短,越适合用户交互程序;
参数:-XX:MaxGCPauseMillis=
-XX:GCTimeRatio=
-XX:+UseAdaptiveSizePolicy 开启后不需要手工指定新生代大小比例等参数,自适应调节策略
暂停时间和吞吐量是相互矛盾的,不可兼容
Parallel Old:Parallel Scavenge的老年代版本,使用多线程标记-整理算法,在注重吞吐量以及CPU资源敏感场合,可优先使用PS+PO
ParNew:只用于新生代,和PS一样是多线程的,不同的是ParNew是为了配合CMS使用而增强的PS,也会STW。Serial的多线程版本,其余行为和Serial一致,适用Server模式下虚拟机,只有Serial和ParNew可以和CMS配合使用;-XX:ParllerGCThreads配置线程数。
CMS:Concurrent Mark Sweep(CMS垃圾回收器没有在任何一个版本JDK上设置为默认GC,在1.9后更是完全移除,原因是设计有缺陷)
获取最短回收停顿时间为目标的收集器,适用B/S服务端,注重服务响应速度,希望停顿时间短;
CMS过程:
初始标记:标记垃圾,从根GC Root开始,Stop The World(STW),因为不用清理,所以停顿短暂(单线程)
并发标记:跟随垃圾对象,进行跟踪,部分垃圾转为不是垃圾,最耗时,但此阶段与程序并发执行
重新标记:再次标记,STW(多线程)
并发清理:程序执行时并发清理
CMS缺点:
并发标记过程中会新生成一部分垃圾,CMS不能对新生成的浮动垃圾进行回收,浮动垃圾会在下次GC时候进行回收。例如:初始标记ABC,并发阶段产生浮动垃圾D,此时C对象被引用不再是垃圾。重新标记AB,并发清理AB,浮动垃圾D下次GC再清理。浮动垃圾并不是影响CMS被抛弃的主要原因,另外在大量垃圾时,CMS回收采用的居然是Serial做串行垃圾回收(只能搭配串行收集器)导致在非常大内存需要做垃圾回收时出现GC时间巨长的问题。
CPU资源敏感度高,用户线程会和GC线程抢占CPU资源;
执行过程中的不确定性:在执行过程中,可能存在上一次垃圾回收还没有执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发整理阶段,一边回收、系统一边运行,也许没有回收完就再次触发FullGC,也就是“concurrent mode failure”,此时会STW,当CMS产生concurrent mode failure就会进入备用方案,此时由Serial Old(单线程收集模式)垃圾收集器代替CMS,因此这是CMS搭配Serial Old的原因。
CMS缺陷:参考三色标记算法。
参数:
-XX:+UseConcMarkSweepGC 老年代启用CMS
–XX:ParallelGCThreads=n 并发回收线程数量:(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) /
-XX:+UseCMSCompactAtFullCollection FullGC后做压缩整理
-XX:+CMSFullGCBeforeCompaction 多少次FullGC后压缩一次,默认是0,每次都会压缩
-XX:CMSInitiatingOccupancyFraction 当堆满之后,并行收集器便开始进行垃圾收集,默认是92,即百分比。
G1:Garbage First:能支持上百G内存的垃圾回收。
分区回收(不分代),内存分成一块一块区域称为Region,优先清理垃圾区,有STW,有FullGC,为每个区标记不同的年代,有Old、survivor、Eden、humongous(大对象)。G1将堆划分为多个大小相等的独立区域(Region),JVM最多分配2048个Region。一般region大小等于堆大小除以2048,比如堆大小为4096M,则region大小为2M,可用参数-XX:G1HeapRegionSize指定大小。
G1保留了年轻代和老年代的概念,但是不再是物理隔阂了。默认年轻代对堆内存的占比是5%,可通过参数 -XX:G1NewSizePercent调整;
在运行中JVM会不停的给年轻代增加更多的Region,但最大是60%。年轻代中Eden和survive的比例是8:1:1。
注意region可能之前是年轻代,但如果经历了垃圾回收,可能变成老年代。
G1对大对象的处理与其他的不同,直接放入Humongous区。G1中大对象的判定准则是超过一个Region的50%。对于一个超过单个region大小的对象会放入连续的region区域。大对象放入H区,是为了节省老年代空间。
垃圾收集过程:
初始标记:会STW,标记GCRoots直接引用的对象;
并发标记:和CMS并发标记一样;
最终标记:和CMS的重新标记一样;
筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(-XX:MaxGCPauseMillis)来制定回收计划(默认200毫秒)。会优先回收耗时较小的region区域的垃圾。
在G1中,不论是年轻代还是老年代,回收算法主要使用复制算法,将一个region中的存活对象复制到另外一个region,不像CMS那样回收完之后还要整理内存碎片(单独的region大小2M,不算内存碎片)。G1可预测的停顿,能够建立预测模型,在指定时间内消耗在垃圾回收上的时间(近乎实时的垃圾收集器特征)
米格区域都有一个通过remember set,把相关引用信息记录到remember set,使用remember set避免全局扫描
G1与CMS的异同点:
相同点:收集过程
不同点:G1复制算法、CMS标记清理;G1可指定清理时间。
G1三种回收方式:youngGC、fullGc、mixedGc
youngGc:假设eden区已经堆满,需要出发GC,默认一次GC时间200Ms,但是预计算得知回收eden去只需要10MS,这时G1会放弃回收,而是增加eden区域的数目,直到一次youngGc所需时间接近于指定时间,才会出发youngGC;
mixedGc:老年代region的堆占有率达到指定标准时出发,回收所有的eden区和部分old去以及H区,采用复制算法;
FullGc:mixed过程中,如没有足够的空的region支撑复制算法时触发,fullGc中会STW(采用标记清除算法),正如CMS中并行收集失败会串行收集。
参数:
-XX:+UseG1GC 使用 G1 (Garbage First) 垃圾收集器
-XX:ParallelGCThreads=n 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同.
-XX:G1HeapRegionSize=n 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb.
-XX:InitiatingHeapOccupancyPercent=n 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比。值为 0 则表示"一直执行GC循环". 默认值为 45.
使用场景:
50%以上的堆被存活对象占用
对象分配和晋升速度变化非常大
垃圾回收时间长
8GB以上堆内存
停顿时间在500ms以内
ZGC:只支持64位操作系统
复制并压缩方式,使用了读屏障,指针跳跃。
并发标记漏标问题处理方案:
CMS:写屏障+增量更新
G1:写屏障+SATB
ZGC:读屏障
垃圾回收器常见搭配:
SerialSerial Old不常用,淘汰了;一般支持几百M
ParNewCMS一般支持几十G
PSPO1.8默认;一般支持几个G
9、调优案例
1、亿级流量的电商网站(ParNew + CMS)
背景:
正常每秒50个订单;促销期间每秒1000单;以每秒1000单计算,分布式三台系统(内存8G),每台300单;
假设每个订单对象为1KB,300KB/s对象生成;
考虑到库存、优惠、积分等计算,放大20倍;即300*20KB/s。这些对象1s后都是垃圾
8G服务器,分配4GB给JVM,参数配置如下:
-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8
堆-老年代:1.5G
堆-伊甸区:1.2G
堆-S0:150M
堆-S1:150M
方法区:256M
栈:1M
计算:按照60MB每秒的对象生成速度,20s后Eden区满,触发MinorGC,按照对象回收95%计算,估计到S0区对象为100M,这100M对象同龄,总和大于S0区50%;根据动态对象年龄判断原则,这100M对象会被挪到老年代,到达老年代后会变成垃圾。
总结:系统业务对象生命周期短,不该进入老年代,survivor区太小导致对象进入老年代,同时老年代内存空间无需这么大,应该把对象尽量留在新生代
优化:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8
-Xmn是年轻代
堆-老年代:1G
堆-伊甸区:1.6G
堆-S0:200M
堆-S1:200M
方法区:256M
栈:1M
优化后:25s后Eden区满,触发MinorGC,估计到S0区对象为100M,不足50%,不会进入老年代,会经历多次MinorGC。
优化思路:让短期存活的对象尽量留在survivor区,不要进老年代,从而减少FullGC
继续优化:
关于对象年龄什么时候进入老年代:本例中MinorGC触发时间在25s,大多数对象存活周期在几秒内变成垃圾,因此可以将年龄降低,从默认的15->5;长对象存活时间在2分钟左右,移动到老年代减少survivor区的占用。
关于对象多大进入老年代:预估大对象大小,一般1M认为很大了。
优化后参数:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8-XX:MaxTenuringThreshold=5 -XX:PertenureSizeThreshold=1M -xx:+UseParNewGC -XX:+UseConcMarkSweepGC
使用了ParNew+CMS
计算FullGC大概时间,根据进入老年代输了,预估价每隔半个小时到一个小时才会触发一次,老年代空间分配担保机制在此处几乎不会存在,因此不大可能出现担保失败造成的FullGC
综上:只要年轻代设置合理,老年代几乎可以使用默认值。最终结果
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8-XX:MaxTenuringThreshold=5 -XX:PertenureSizeThreshold=1M -xx:+UseParNewGC -XX:+UseConcMarkSweepGC-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSCompactAtFullCollection -XX:+CMSFullGCBeforeCompaction=0
调优总结:
1、年轻代大小选择:
响应时间优先:尽可能大,此时年轻代发生手机频率小,同时减少到达老年代的对象
吞吐量优先:尽可能大,垃圾回收可以设置为并行
2、年老代选择:
响应时间优先:考虑并发会话率和会话持续时间,过小则频率过高,过大则回收时间过长
吞吐量优先:很大的年轻代,较小的年老代,减少中期对象存活。
10、调优工具
artahs:开源调优工具,阿里出品,可参考Github,调优可说使用过此产品
-help 查看参数
dashboard 仪表盘
jad class 可用于反编译,用于查看程序版本
jvm 类型jinfo功能
redefine class 动态替换class文件,热部署
thread 类型jstack thread -b 可以查看死锁
11、常用指令和参数
查看GC类型指令:java -XX:+PrintCommandLineFlags -version
jps:查看进程
jmap -histo PID:可查看实例数量以及对应的类
jmap -heap PID:可以查看堆内存设置和堆各个内存空间的使用情况
jmap -dump:导出dump堆
jstack -PID:查看死锁情况
jstat -gc PID :查看GC详细信息;jstat -gc PID 1000 10:每隔1S输出一次GC信息,执行10次;可以根据这个参数来计算年轻代GC执行时间,以及老年代对象增长速度从而调优。
调优参数:
-Xms -Xmx 设置最小、最大堆空间,上限下限设置一样的可以避免扩容带来的运算
-XX:+PrintGC 打印GC日志
jstack 打印线程栈,可以用于查看死锁问题
jinfo pid 查看虚拟机信息
jstat -gc pid 输出GC信息,后加数字,可不断刷新:jstat -gc pid 500(500ms输出一次)
设置GC日志参数:
-Xloggc:/opt/xxx/logs/xxx-xx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause 循环生成5个20MGC日志
jmap -histo pid | head -20 前20个大对象
jmap -dump:format=b,file=xxx pid 导出堆栈信息
设置:-XX:+HeapDumpOnOutOfMemoryError OOM时自动生成转储文件
开启GC日志:-XX:+PrintGCDetails
大对象直接进入老年代:-XX:PretenureSizeThreshold 指定(只对serial parNew有效,ps不认,可以用cms+parNew)
长期存活对象进入老年代:-XX:MaxTenuringThreshold 指定对象年龄
12、安全点和安全区域
安全点:就是指代码中一些特定的位置,当线程运行到这些位置时,它的状态是确定的,这样JVM就可以安全的进行一些操作,比如GC等。GC并不是随时都可以触发,而是需要等待所有线程运行到安全点后才能触发。
特定安全点的位置主要有:
方法返回之前
调用某个方法之后
抛出异常的位置
循环的末尾
安全区域:Safe Ponit是对正在执行的线程设定的,如果一个线程处于Sleep或者中断状态,它就不能响应JVM的中断请求,再运行到Safe Ponit。
JVM引入Safe Region是指一段代码片段中,引用关系不会发生变化,在这个区域内的任意地方开始GC都是线程安全的。线程在进入Safe Region的时候先标记自己已经进入了Safe Region,等到被唤醒时准备离开Safe Region时,先检查能否离开,如果GC完成了,那么线程可以离开,否则它必须等待直到安全离开的信号为止。