文章目录
概述
上一篇主要说了GC的过程,这里总结一下java的几种收集器和算法
前置结论
- 尽可能将对象分配到新生代,因为full GC成本高于minor GC
- 尽量少使用大对象
- JIT编译参数
- 发生oom时执行脚本 -XX:OnoomError=D:\reset.bat
- 获取GC日志 -Xloggc:D:\gclog.txt
- tomcat catalina.bat参数调优
算法
标记-清除
首先标记出所有需要回收的对象,标记完成以后统一进行回收。
标记过程参照上一篇不可达对象部分,其缺点有2个
- 效率问题,标记、清除过程效率都不高
- 空间碎片问题,当有较大对象的时候,不得不触发一次GC
复制算法
- 虽然这种算法实现简单,运行高效,但是代价太高。只能使用一半的空间。但是这种算法也带来一种思路,新生代中的对象 消失的很快,不需要按照1:1来分配,HotSpot默认Eden和Survivor1、Survivor2是8:1:1,当回收时,将Eden和Survivor(其中一个)一起复制到另外一块没有使用的Survivor中, 也就等于新生代有90%可以利用。
- 如果另一块Survivor没有足够的内存来接收上一次存活的对象,这些对象通过分配担保机制直接进入老年代。
- 复制算法比较适用于新生代。因为复制算法在存活对象比较多的时候,效率会降低,所以老年代不适用。
标记-整理算法
- 针对老年代的特点,与标记-清除算法的标记过程一样,但是之后是让所有存活的对象向另一端移动,然后直接清理掉边界以外的对象
- 既避免了内存碎片,也不需要两块相同的空间,性价比较高
分代收集算法
当前商业虚拟机都采用该算法,根据对象存活时期的不同,将内存分为几块,一般是分为新生代和老年代,在根据不同年代的不同特性来选择最适合的收集算法
- 新生代 复制算法,默认8:1:1
- 老年代 标记-整理算法 或者 标记-清除算法
综上所述,收集器可以做以下分类
- 按照线程来分,串行收集器和并行收集器
- 按工作模式来分,并发式和独占式
- 按碎片处理方式,压缩式和非压缩式
- 按工作内存,新生代和老年代
几种主流的垃圾收集器
Serial 串行收集器
- 最基本,最久远,是1.3.1之前,jvm 新生代唯一的选择,单线程,该收集器工作时会Stop-the-World
- 简单高效,久经考验,没有线程切换的开销,单核CPU,或者性能一般的场景,其性能甚至可以超越其他并发收集器
- 应用于桌面场景
- 可以使用-XX:+UseSerialGC开启,GC日志标志为:DefNew
- 具有新生代版本和老年代版本,新生代 复制算法, 老年代 标记-整理算法 √
- 老年代版本作为CMS备用,发生ConCurrent Mode Failure时启用,标志位:Tenured
[DefNew: 16432K->2186K(18618K), 0.0186557 secs]
[Full GC 8.259: [Tenured: 43711K->40302K(43712K), 0.2960623 secs]
ParNew
- Serial多线程版本(并行收集器),与Serial策略基本相同
- 并行收集器,也是独占式,GC过程中, 应用程序都会暂停
- -XX:+UseParNewGC 强制开启ParNew,老年代依然是Serial
- -XX:+UseConcMarkSweepGC: 新生代使用 ParNew 回收器,老年代使用 CMS
- -XX:ParallelGCThreads,默认开启的线程数与cpu核心数相当,单CPU中或者并发能力较弱的系统,过多的线程表现可能还不如串行收集器
[GC (Allocation Failure) 2018-11-22T17:49:29.652+0800: 63796.264: [ParNew: 2462740K->181890K(2563200K), 0.3600056 secs] 3008148K->727298K(8155648K), 0.3603313 secs] [Times: user=0.63 sys=0.00, real=0.36 secs]
Parallel 新生代收集器
- 复制算法,并行多线程工作方式,好像跟ParNew一样? 区别是Parallel 注重吞吐量,
- -XX:GCTimeRatio 垃圾收集时间占比
- -XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间,如果设置过小,可能导致频繁GC
- Parallel old 版本,采用标记-整理算法
- 在1.6之前,如果新生代选择了Parallel,老年代只有Serial old
- 现在,可以选择Parallel 和 Paraller old 组合使用
日志标志 PSYoungGen
CMS
- CMS,ConCurrent Mark Sweep,并发-标记-清除,第一款并发收集器
- CMS作为老年代收集器,新生代需要配合ParNew或Serial其中一个
- 重视服务响应速度,其过程分为四步:
①初始标记
②并发标记
③重新标记
④并发清除
其中初始标记、重新标记需要STP,但是耗时较短
耗时最长的并发标记和并发清除,可以并发工作,所以总体来说CMS是并发收集器
但是其缺点依然存在:对CPU敏感,无法回收浮动垃圾,空间碎片 - 也正是由于其运行时清理垃圾,所以不能等内存满了才开始,1.5 默认老年代 68%触发,1.6 92%触发,如果清理失败,则启动 Serial old 来回收
- -XX:+UseConcMarkSweepGC 此参数将启动 CMS ,默认新生代是 ParNew
- -XX:CMSInitiatingOccupancyFraction设置触发阈值
- 晋升失败的问题
G1
- Garbage First,作为目前最前沿的收集器,预计在吞吐量和停顿控制方面要优于CMS(有待实战验证)
- 即可回收年轻代,也可回收老年代,采用标记-整理算法,解决了CMS碎片多的问题
- 进行精确的停顿控制,-XX:MaxGCPauseMillis 指定最大停顿时间(自动调整young和old比例,停顿时间短也会导致吞吐量下降)
-XX:GCPauseIntervalMillis = 200
-XX:MaxGCPauseMillis = 50
200毫秒内,停顿不超过50ms
- G1虽然有分代收集的思想,但是young和old不再是物理分隔的区域,G1将java堆划分为若干个region,之所以可以做到精准停顿,因为它有计划的对各个region按照优先级高低来回收,优先回收价值最大的Region(这也是为什么叫做Garbage-First),每个region都有一个Remembered Set,在GC roots根节点的枚举范围中加入这个set,保证不用每次对堆扫描,不算入Remembered Set的过程,G1可以认为是以下过程:
①初始标记
②并发标记
③最终标记
④筛选回收
关于触发Full GC的补充
- 执行jmap -histo:live [pid],主动触发
- CMS出现Concurrent mode failure,一般是在CMS工作的时候,年老代内存不足,直接Serial
- 统计得到的minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
看懂GC日志
学习知识还是为了解决问题,看懂GC日志,找到问题也是很关键的一步。
基于1.7来分析几个案例,持续更新
2018-10-26T15:04:56.565+0800: 4.199: [GC (Allocation Failure) 2018-10-26T15:04:56.565+0800: 4.199:
[ParNew: 436992K->29946K(480640K), 0.0867648 secs] 436992K->29946K(8344960K), 0.0869238 secs]
[Times: user=0.16 sys=0.02, real=0.09 secs]
- Allocation Failure – 引起垃圾回收的原因,本次GC是因为年轻代中没有合适的区域能够存放需要的内存,触发了ParNew的回收
- 436992K->29946K(480640K),回收前的young使用了 436,回收之后29,young总大小是480
- 方括号之外的 436992K->29946K(8344960K),GC前堆使用的内存-> GC后堆使用的内存,(java堆总内存),可以算出老年代占用
- 0.0869238 secs 持续时间
- Times: user=0.16 sys=0.02, real=0.09 secs,real是真实消耗,CMS并发执行,real不等于user+sys
优化方案:
- 如果频繁的 minor GC,可以适当增大年轻代,代码层面,比如Double换成double
2018-11-22T17:48:12.717+0800: 63719.329: [GC (CMS Initial Mark) [1 CMS-initial-mark: 4484103K(5592448K)] 4618265K(8155648K), 0.0782959 secs] [Times: user=0.18 sys=0.00, real=0.07 secs]
2018-11-22T17:48:12.796+0800: 63719.407: [CMS-concurrent-mark-start]
2018-11-22T17:48:13.160+0800: 63719.772: [CMS-concurrent-mark: 0.344/0.365 secs] [Times: user=0.45 sys=0.00, real=0.37 secs]
2018-11-22T17:48:13.160+0800: 63719.772: [CMS-concurrent-preclean-start]
2018-11-22T17:48:13.178+0800: 63719.789: [CMS-concurrent-preclean: 0.017/0.017 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2018-11-22T17:48:13.178+0800: 63719.789: [CMS-concurrent-abortable-preclean-start]
CMS: abort preclean due to time 2018-11-22T17:48:18.179+0800: 63724.791: [CMS-concurrent-abortable-preclean: 4.916/5.001 secs] [Times: user=5.60 sys=0.04, real=5.00 secs]
2018-11-22T17:48:18.180+0800: 63724.792: [GC (CMS Final Remark) [YG occupancy: 267610 K (2563200 K)]2018-11-22T17:48:18.180+0800: 63724.792: [Rescan (parallel) , 0.1234198 secs]2018-11-22T17:48:18.304+0800: 63724.915: [weak refs processing, 0.1341291 secs]2018-11-22T17:48:18.438+0800: 63725.050: [class unloading, 0.0653423 secs]2018-11-22T17:48:18.503+0800: 63725.115: [scrub symbol table, 0.0258100 secs]2018-11-22T17:48:18.529+0800: 63725.141: [scrub string table, 0.0032973 secs][1 CMS-remark: 4484103K(5592448K)] 4751714K(8155648K), 0.3842283 secs] [Times: user=0.72 sys=0.00, real=0.38 secs]
2018-11-22T17:48:18.573+0800: 63725.184: [CMS-concurrent-sweep-start]
2018-11-22T17:48:23.304+0800: 63729.916: [CMS-concurrent-sweep: 4.636/4.731 secs] [Times: user=5.20 sys=0.00, real=4.73 secs]
2018-11-22T17:48:23.304+0800: 63729.916: [CMS-concurrent-reset-start]
2018-11-22T17:48:23.319+0800: 63729.930: [CMS-concurrent-reset: 0.015/0.015 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
CMS一次完整过程, initial-mark和remark 对应初始标记和重新标记,会STP
CMS-initial-mark: 4484103K(5592448K)] 4618265K(8155648K)
- CMS Initial Mark – 收集阶段,开始收集所有的GC Roots和直接引用到的对象
- 4484103K 当前老年代使用
- 5592448K 当前老年代总量
- 4618265K 当前堆使用情况
- 8155648K 当前堆总量
关于内存小于3G不建议CMS
1.触发比例不好设置,例如3G内存,92%的默认比例,老年代只有100~200M左右,有可能无法装下年轻代晋升的对象
2.CMS抢占CPU资源
3.碎片
大内存建议CMS
参考:
《深入理解java虚拟机》–周志明
《java程序性能调优》–葛一鸣
http://hellojava.info/?p=142