国外几乎很少有Java程序员关注JVM,而JVM调优在国内有些类似与玄学,知识点来源很多,比如道听途说,官方文档,Google,涉及的JVM版本也很多,也没有什么科学的指导,依靠口口相传,大神内卷,网页转载传播。看完各种资料其实也看不出来个什么所以然,怎么调优,JVM有几百个参数,很多参数之间还是互斥制衡的,而且JVM内部也有统计信息,来自适应调整性能,并且大部分有利于性能的选项都开启了,JVM团队都不能准确的告诉你所谓的调优,一般根据硬件环境和产品类型设置合适的GC和内存参数就可以了。

JVM内存设计的目的

  • 分代(新生代和年老代)是为了对不同生命周期的对象分而治之的优化
  • 分区(Eden区和幸存区)是为对位于不同区域的对象分而治之的优化
  • region 将内存分为更小的颗粒度进行管理

JVM调优的基本思路

1. 确定吞吐量优先还是响应优先

2. 选择合适的堆大小

堆内存

  • 新生代-Eden
    • 小的新生代有利于提高响应性能,但以GC频繁震荡和牺牲吞吐量为代价
    • 大的新生代有利于最大化吞吐量,但以占用空间和牺牲暂停时间为代价
  • 新生代-Survivor0
    • 新生代中的幸存区太小,对象容易直接溢出到老年代
  • 新生代-Survivor1
    • 新生代中的幸存区太大,相当于浪费了另一个幸存区
  • 年老代
    • 堆的大小是固定的,增加年轻代会导致减少老年代,将增加Full GC的频率
    • 保持老年代足够大,以容纳应用程序在任何给定时间内需要使用的实时数据
  • 堆边界

    • 堆上下界相同可避免并行GC为保持堆大小,响应性和吞吐量的平衡产生的开销

    • 非堆内存

  • 线程栈

  • 永生区或元数据区(常量池,方法信息,类型信息)
  • 元数据
  • 直接缓冲区
  • 代码缓存区

3. 选择合适的GC类型

串行收集器

  • 如果应用程序的数据集很小(最多大约 100 MB),则选择串行收集器-XX:+UseSerialGC。
  • 如果应用程序将在单个处理器上运行并且没有暂停时间要求,则选择串行收集器-XX:+UseSerialGC

并行收集器

  • 如果应用程序吞吐量是第一优先级并且没有暂停时间要求或可以接受一秒或更长的暂停,则选择并行收集器-XX:+UseParallelGC
  • 基于收集线程的调优
    • 基于硬件线程数调整并行的GC线程数量 XX:ParallelGCThreads
  • 基于并发目标行为的性能期待
    • 最大暂停时间-XX:MaxGCPauseMillis
    • 吞吐量的大小-XX:GCTimeRatio
    • 开启自动驾驶-XX:+UseAdaptiveSizePolicy
  • 为满足暂停时间行为的堆收缩
    • 堆的收缩速度因子-XX:AdaptiveSizeDecrementScaleFactor=
  • 为满足吞吐量行为堆扩容调整
    • 新生代的堆的扩容 -XX:YoungGenerationSizeIncrement=
    • 年老代的堆的扩容-XX:TenuredGenerationSizeIncrement=

并发收集器

  • 如果响应时间比总吞吐量更重要,并且垃圾收集暂停必须保持在大约一秒以内,则选择G1收集器-XX:+UseG1GC或CMS-XX:+UseConcMarkSweepGC
  • 垃圾收集优先的G1,适用于具有大量内存的多处理器机器
  • CPU核数与并发收集关系
    • 并发收集器的并发收集线程数
      • 1 <= K <= 上限{N/4 }
    • 单核服务器对并发收集没益处,并发阶段至少需一个处理器收集垃圾
  • 并发模式可能失败和退化
    • 如果 CMS 收集器无法在老年代填满之前完成回收不可达对象,或者如果老年代中可用的空闲空间块无法满足分配。收集将在所有应用程序线程停止的情况下完成并退化成年老代的串行收集。无法同时完成收集被称为并发模式失败。
  • 并发收集内存溢出的判断
    • 如果总时间的 98% 以上花费在垃圾收集上,而堆回收不到 2%,则内存不足错误被抛出。可以通过将选项添加-XX:-UseGCOverheadLimit到命令行来禁用此功能
  • 垃圾收集时间两种计算法
    • 与应用程序并发运行时的GC时间
    • 应用程序停止时候的GC运行时间
  • 老年代的内存占用率与GC
    • 老年代的占用率超过初始占用率(老年代的百分比),并发收集也会开始
    • -XX:CMSInitiatingOccupancyFraction=92

4. 设置合适的内存空闲率

  • 堆的空闲率不能低于某个比例,即堆的使用率高,需要扩展堆的大小
    • -XX:MinHeapFreeRatio
  • 堆的空闲率不能高于某个比例,即堆的使用率低,需要收缩堆的大小
    • -XX:MaxHeapFreeRatio

5. 选择性能更好多核CPU

  • 随着处理器数量的增加,增加新生代的大小,因为分配可以并行化
  • 多核CPU时可以选择指定并行的GC线程数-XX:ParallelGCThreads

垃圾收集器的类型

  • 新生代(复制算法)
    • Serial(单线程&STW)
    • Parallel Scavenge(多线程&STW&吞吐量优先的GC)
    • ParNew(多线程&STW)
  • 年老代(标记清除)
    • Serial Old(单线程&STW)
    • Parallel Old(多线程&STW)
    • Concurrent Mark Sweep
  • G1
    • -XX:+UseG1GC

GC对内存排列的影响

  • 串行收集器中新生代的剩余空间和年老代的剩余空间不相邻
  • 并行收集器中新生代的剩余空间和年老代的剩余空间则相邻

垃圾收集与引用类型

  • 强引用
    • 它真的很强直到内存溢出
  • 软引用
    • 合理的利用内存敏感特性
  • 弱引用
    • 合理的利用 GC 敏感特性
    • 配合WeakHashMap使用
  • 虚引用
    • 合理的利用 GC 敏感特性
    • 合理的跟踪被释放的对象
  • finalize
    • finalize可能让对象复活

常用JVM虚拟机的参数

GC信息打印

  • 打印GC的基本或细节信息
    • -XX:+PrintGC
    • -XX:+PrintGCDetails
    • -XX:+PrintGCTimeStamps
  • 打印对象晋升失败的信息
    • -XX:+PrintPromotionFailure
  • 打印GC产生原因的信息
    • -XX:+PrintGCCause

GC日志转储

  • 日志文件的位置
    • -Xloggc:
  • 日志文件的大小
    • -XX:GCLogFileSize=512m
  • 日志文件的个数
    • -XX:NumberOfGCLogFiles=5

堆内存配置

  • 绝对大小配置
    • 设堆的初始尺寸和最大尺寸(新生代+年老代)
      • -Xms & -Xmx
    • 新生代初始尺寸和最大尺寸(Eden+2 Survivors)
      • -XX:NewSize & -XX:MaxNewSize
  • 比例大小配置
    • 堆中新生代与年老代的比例
      • -XX:NewRatio
    • 单个幸存区与伊甸区的比例
      • -XX:SurvivorRatio

非堆的配置

  • 设置NIO非堆内存大小
    • -XX:MaxDirectMemorySize=2g
  • 设置线程栈的内存大小避免栈溢出
    • -XX:ThreadStackSize=256
  • 设置元数据区域初始尺寸和最大尺寸
    • -XX:MetaspaceSize=512m
    • -XX:MaxMetaspaceSize=1g
  • 设置代码缓存的初始尺寸和最大尺寸
    • -XX:InitialCodeCacheSize=256m
    • -XX:ReservedCodeCacheSize=512m

新生代的晋升

  • 基于对象 MinorGC 频度控制进入年老代
    • -XX:InitialTenuringThreshold=8
    • -XX:MaxTenuringThreshold=15
  • 基于对象大小的阈值控制对象进入年老代
    • -XX:PretenureSizeThreshold=2m
  • 新生代对象总是立即晋升进入年老代(失去分代的意义)
    • -XX:+AlwaysTenure
  • 新生代对象直到新生代内存耗尽才晋升(阈值失去意义)
    • -XX:+NeverTenure

GC并行或并发

  • 并行的GC线程数
    • -XX:ParallelGCThreads=16
  • 并发的GC线程数
    • -XX:ConcGCThreads=2