- -Xms 初始堆大小
- -Xmx 最大堆大小
- -XX:NewSize 年轻代大小
- -XX:MaxNewSize 年轻代最大值
- -XX:PermSize 永生代初始值
- -XX:MaxPermSize 永生代最大值
- -XX:NewRatio 新生代与老年代的比例
预备知识
- 掌握 GC 相关的 VM 参数,会基本的空间调整 掌握相关工具。
- 掌握相关工具
- 明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则。
查看虚拟机参数命令
java -XX:+PrintFlagsFinal -version | findstr "GC" # windows 下
- 内存
- 锁竞争
- CPU 占用
- IO
- GC
科学计算:追求高吞吐量,响应时间长点没事。
互联网项目:响应时间很重要。
低延迟/高吞吐量? 选择合适的 GC
- CMS、G1、ZGC(响应时间优先,CMS 用的比较多。)
- ParallelGC
- Zing 虚拟机,0 停顿。几乎没有 STW 时间。
首先排除减少因为自身编写的代码而引发的内存问题
查看 Full GC 前后的内存占用,考虑以下几个问题
- 数据是不是太多?如:
resultSet = statement.executeQuery("select * from 大表 limit n")
- 数据表示是否太臃肿
- 对象图,数据不需要全部都用,用到哪个查哪个
- 对象大小
new Object() 占16个字节
- 包装对象 如 Integer 的一个对象头就 16 个字节。Integer 好像占 24 字节
- 能有基本类型就用基本类型
- 是否存在内存泄漏
- static Map map = 不断放对象,不溢出。
- 建议用软、弱引用
- 缓存数据不太推荐用 Java 的,用三方缓存实现,如 Redis
内存调优建议从新生代开始。
新生代的特点
- 所有的 new 操作分配内存都是非常廉价的
- TLAB thread-local allocation buffer
- new 的时候先检查 TLAB 中是否有可用内存,有就优先在这块内存区域进行对象的分配。对象的内存分配也有线程安全的问题。TLAB 让每个线程用自己私有的那块伊甸园区进行对象的分配。
- 死亡对象回收零代价
- 大部分对象用过即死(朝生夕死)
- MInor GC 所用时间远小于 Full GC
新生代内存越大越好么?
- 不是
- 新生代内存太小:频繁触发 Minor GC,会 STW,会使得吞吐量下降
- 新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长
- Oracle 官方推荐,25%Heap < young generation < 50%Heap
- 新生代内存设置为可容纳 [并发量 * (请求-响应)] 的数据为宜
幸存区
- 幸存区需要能够保存当前活跃对象+需要晋升的对象
- 晋升阈值配置得当,让长时间存活的对象尽快晋升
-XX:MaxTenuringThreshold=threshold
晋升阈值
-XX:+PrintTenuringDistribution
打印信息
Desired survivor size 48286924 bytes, new threshold 10 (max 10)
- age 1: 28992024 bytes, 28992024 total
- age 2: 1366864 bytes, 30358888 total
- age 3: 1425912 bytes, 31784800 total ...
以 CMS 为例
- CMS 的老年代内存越大越好
- 先尝试不做调优,如果没有 Full GC 那么已经...,否则先尝试调优新生代
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
- -XX:CMSInitiatingOccupancyFraction=percent 一般设置为 75%~80%
案例 1:Full GC 和 Minor GC 频繁
大量频繁的 GC 说明空间紧张,究竟是那部分空间紧张。如果是新生代的空间紧张,大量的对象被创建,很快就把新生代塞满了。幸存区空间紧张,对象的晋升值减低,很多本来生存周期很短的对象也晋升到老年代去,这样就进一步恶化了。进而触发老年代的 Full GC。
- 先试着增大新生代内存,垃圾回收就变得不那么频繁了。
- 增加幸存区的空间和晋升的阈值,让很多生命周期较短的对象留在新生代,进一步让老年代的 Full GC 不那么频繁。
案例 2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)【业务需求是低延时】
到底是那一部分耗费的时间长?
垃圾回收使用的 CMS。
查看 CMS 的日志,看 CMS 的哪个阶段耗时长。发现重新标记阶段耗时长!
重新标记的时候会扫描整个堆内存(new + old),如果业务高峰,新生代对象比较多,那么扫描时间就很长。在重新标记前先对新生代对象做一次 GC,减少新生代对象的遍历数量。-XX:+CMSScavengeBeforeRemark
回顾 CMS 的几个阶段
- 初始标记:仅仅标记 GC Roots的对象,并且 STW
- 并发标记:使用 GC Roots Tracing 算法,进行跟踪标记,不 STW
- 重新标记:因为之前并发标记,其他用户线程不暂停,可能产生了新垃圾,所以重新标记,STW。
- 重新标记的过程值追踪在并发标记过程中产生的变动的对象。
案例 3:老年代充裕的情况下,发生 Full GC (CMS jdk1.7)
GC 日志里没有竞争失败,并发失败,碎片过多的错误提示。说明老年代的空间是充裕的。后来发现应用部署在 jdk1.7 里。在 1.7 里,永久代空间的不足也会导致 Full GC,1.8 以后改成了元空间(用的本地内存)不会导致 Full GC。