JVM heap中各generation的大小(Sizing the Generations)

查看参数

使用 -XX:+PrintFlagsFinal 打印当前环境JVM参数默认值, 比如: java -XX:PrintFlagsFinal -version, 也可以用java [生产环境参数] -XX:+PrintFlagsFinal –version | grep [待查证的参数]查看具体的参数数据

JVM内存构成

一个2G堆大小的JVM,可能占用的内存 =  堆内存 + (线程数 * 线程栈) + 永久代 + 二进制代码 + 堆外内存 = 2G + (1000 * 1M) + 256M + 48~240M + (~2G) = 5.xG

  • 堆内存: 存储Java对象,默认为物理内存的1/64, 由Xms, Xmx, Xmn等参数控制
  • 线程栈: 存储局部变量(原子类型, 引用)及其他, JDK5 以后每个线程堆栈大小为1M, 以前每个线程堆栈大小为256K, 根据应用的线程所需内存大小进行调整. 在相同物理内存下,减小这个值能生成更多的线程. 但是操作系统对一个进程内的线程数还是有限制的, 不能无限生成, 经验值在3000~5000左右
  • 永久代: 存储类定义及常量池, JDK7以前为PermSize, MaxPermSize, JDK8之后为MetaspaceSize, MaxMetaspaceSize
  • 二进制代码: JDK7与8, 打开多层编译时的默认值不一样,从48到240M
  • 堆外内存: 被Netty 堆外缓存等使用, 默认最大值约为堆内存大小

也就是说堆内存为2G的JVM如果有1000个线程, 可能需要占5.5G. 在考虑系统占用 IO占用等等各种情况, 一台8G的服务器也就启动一个服务. 当然如果线程数少并发不高, 还是可以启动多个.

堆设置

-Xms2g 与 -Xmx2g
堆内存大小,第一个是最小堆内存,第二个是最大堆内存,比较合适的数值是2-4g,再大就得考虑GC时间

-Xmn1g 或 (-XX:NewSize=1g 和 -XX:MaxNewSize=1g) 或 -XX:NewRatio=1
设置新生代大小,JDK默认新生代占堆内存大小的1/3,也就是-XX:NewRatio=2。这里是设置的1g,也就是-XX:NewRatio=1。可以根据自己的需要设置。

-XX:MetaspaceSize=128m 和 -XX:MaxMetaspaceSize=512m
JDK8的永生代几乎可用完机器的所有内存,为了保护服务器不会因为内存占用过大无法连接,需要设置一个128M的初始值,512M的最大值保护一下

-XX:SurvivorRatio
新生代中每个存活区的大小,默认为8,即1/10的新生代, 1/(SurvivorRatio+2),有人喜欢设小点省点给新生代,但要避免太小使得存活区放不下临时对象而要晋升到老生代,还是从GC Log里看实际情况了

-Xss256k
在堆之外,线程占用栈内存, JDK5后默认每条线程为1M. 存放方法调用出参入参的栈、局部变量、标量替换后的局部变量等,有人喜欢设小点节约内存开更多线程. 但反正内存够也就不必要设小, 有人喜欢再设大点, 特别是有JSON解析之类的递归调用时不能设太小.

-XX:MaxDirectMemorySize
堆外内存/直接内存的大小, 默认为堆内存减去一个Survivor区的大小, 可以认为大小和堆内存一样. 超过这个数会引发OutOfMemoryError

-XX:ReservedCodeCacheSize
JIT编译后二进制代码的存放区, 满了之后就不再编译. 默认开多层编译240M, 可以在JMX里看看CodeCache的大小.

GC日志设置

GC过程可以通过GC日志来提供优化依据。

  • -XX:+PrintGCDetails: 启用gc日志打印功能
  • -Xloggc:/path/to/gc.log: 指定gc日志位置
  • -XX:+PrintHeapAtGC: 打印GC前后的详细堆栈信息
  • -XX:+PrintGCDateStamps: 打印可读的日期而不是时间戳
  • -XX:+PrintGCApplicationStoppedTime: 打印所有引起JVM停顿时间,如果真的发现了一些不知什么的停顿,再临时加上-XX:+PrintSafepointStatistics -XX: PrintSafepointStatisticsCount=1找原因。
  • -XX:+PrintGCApplicationConcurrentTime: 打印JVM在两次停顿之间正常运行时间,与-XX:+PrintGCApplicationStoppedTime一起使用效果更佳。
  • -XX:+PrintTenuringDistribution: 查看每次minor GC后新的存活周期的阈值
  • -XX:+UseGCLogFileRotation 与 -XX:NumberOfGCLogFiles=10 与 -XX:GCLogFileSize=10M: GC日志在重启之后会清空,但是如果一个应用长时间不重启,那GC日志会增加,所以添加这3个参数,是GC日志滚动写入文件,但是如果重启,可能名字会出现混乱。
  • -XX:PrintFLSStatistics=1: 打印每次GC前后内存碎片的统计信息

GC设置

目前比较主流的GC是CMS和G1, 大内存(>8G)建议用G1. 现在用的比较多的是CMS收集器, 下面的参数也是针对CMS收集器的

  • -XX:+UseConcMarkSweepGC: 启用CMS收集器
  • -XX:CMSInitiatingOccupancyFraction=80 与 -XX:+UseCMSInitiatingOccupancyOnly: 两个参数需要配合使用, 否则第一个参数的75只是一个参考值, JVM会重新计算GC的时间。指定的百分比应该比垃圾回收之后存活对象的占用率更高, 如果<percent>不比存活对象的占用量大, CMS垃圾回收器会一直运行. 通常的建议是这个百分比应该为存活对象的占用率的1.5倍.
  • -XX:MaxTenuringThreshold=15: 对象在Survivor区熬过多少次Young GC后晋升到年老代, 默认是15. Young GC会频繁发生, 而新生代里GC后存活对象的多少又直接影响停顿的时间, 所以如果清楚Young GC的执行频率和应用里大部分临时对象的最长生命周期, 可以把它设的更短一点, 让新生代长期对象尽快晋升到年老代.
  • -XX:-DisableExplicitGC: 禁止System.gc()主动调用GC. 有的JVM优化建议设置-XX:-DisableExplicitGC 关闭手动调用System.gc(), 这是因为System.gc()触发Full GC, 而频繁的Full GC会严重影响性能, 但是很多NIO框架比如Netty, 会使用堆外内存, 如果没有Full GC的话, 堆外内存就无法回收. 如果不主动调用System.gc(), 就需要等到JVM自己触发Full GC, 这个时候, 就可能引起长时间的停顿(STW), 而且机器负载也会升高. 所以不能够完全禁止System.gc(), 又得缩短Full GC的时间, 那就使用-XX:+ExplicitGCInvokesConcurrent或-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses选项, 使用CMS收集器来触发Full GC. 这两个选项需要配合-XX:+UseConcMarkSweepGC使用. 
  • -XX:+ExplicitGCInvokesConcurrent: 调用System.gc()时触发CMS GC 而不是Full GC. 默认是不开启的, 只有使用-XX:+UseConcMarkSweepGC选项的时候才能开启这个选项.
  • -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses:使用System.gc()时,永久代也被包括进CMS范围内。只有使用-XX:+UseConcMarkSweepGC选项的时候才能开启这个选项。
  • -XX:+ParallelRefProcEnabled: 默认为false, 并行的处理Reference对象, 如WeakReference. 除非在GC log里出现Reference处理时间较长的日志, 否则效果不会很明显.
  • -XX:+ScavengeBeforeFullGC: 在Full GC执行前先执行一次Young GC
  • -XX:+UseGCOverheadLimit: 限制GC的运行时间. 如果GC耗时过长就抛OOM.
  • -XX:+UseParallelGC:设置使用并行垃圾收集器
  • -XX:+UseParallelOldGC:设置老年代使用并行垃圾收集器
  • -XX:-UseSerialGC:设置使用串行垃圾收集器
  • -XX:+CMSParallelInitialMarkEnabled 和 -XX:+CMSParallelRemarkEnabled: 降低标记停顿
  • -XX:+CMSScavengeBeforeRemark: 默认为关闭, 在CMS remark前先执行一次minor GC将新生代清掉, 这样从老生代的对象引用到的新生代对象的个数就少了, CMS remark阶段就短一些. 如果看到GC日志里remark阶段的时间超长, 可以打开此项看看有没有效果, 否则还是不要打开了, 白白多了次YGC.
  • -XX:CMSWaitDuration=10000: 设置垃圾收集的最大时间间隔, 默认是2000.
  • -XX:+CMSClassUnloadingEnabled: 在CMS中清理永久代中的过期的Class而不等到Full GC, JDK7默认关闭而JDK8打开. 看自己情况, 比如有没有运行动态语言脚本如Groovy产生大量的临时类, 它会增加CMS remark的暂停时间. 所以如果新类加载并不频繁, 这个参数还是不开的好。

其他设置

  • -ea: 启用断言, 可以选择启用或这选择不启用, 没有什么大的差异, 完全根据自己的系统进行处理.
  • -XX:+UseThreadPriorities: 启用线程优先级,主要是因为我们可以给予周期性任务更低的优先级,以避免干扰客户端工作。在我当前的环境中,是默认启用的。
  • -XX:ThreadPriorityPolicy=42: 允许降低线程优先级
  • -XX:+HeapDumpOnOutOfMemoryError: 发生内存溢出是进行heap-dump
  • -XX:HeapDumpPath=/path/to/java_pid.hprof: 这个参数与-XX:+HeapDumpOnOutOfMemoryError共同作用, 设置heap-dump时内容输出文件.
  • -XX:ErrorFile=/path/to/hs_err_pid.log: 指定致命错误日志位置. 一般在JVM发生致命错误时会输出类似hs_err_pid.log的文件, 默认是在工作目录中(如果没有权限,会尝试在/tmp中创建), 不过还是自己指定位置更好一些, 便于收集和查找, 避免丢失.
  • -XX:StringTableSize=1000003:指定字符串常量池大小,默认值是60013。对Java稍微有点常识的应该知道,字符串是常量,创建之后就不可修改了,这些常量所在的地方叫做字符串常量池。如果自己系统中有很多字符串的操作,且这些字符串值比较固定,在允许的情况下,可以适当调大一些池子大小。
  • -XX:+AlwaysPreTouch:在启动时把所有参数定义的内存全部捋一遍。使用这个参数可能会使启动变慢,但是在后面内存使用过程中会更快。可以保证内存页面连续分配,新生代晋升时不会因为申请内存页面使GC停顿加长。通常只有在内存大于32G的时候才会有感觉。
  • -XX:-UseBiasedLocking:禁用偏向锁(在存在大量锁对象的创建且高度并发的环境下(即非多线程高并发应用)禁用偏向锁能够带来一定的性能优化)
  • -XX:AutoBoxCacheMax=20000:增加数字对象自动装箱的范围,JDK默认-128~127的int和long,超出范围就会即时创建对象,所以,增加范围可以提高性能,但是也是需要测试。
  • -XX:-OmitStackTraceInFastThrow:不忽略重复异常的栈,这是JDK的优化,大量重复的JDK异常不再打印其StackTrace。但是如果系统是长时间不重启的系统,在同一个地方跑了N多次异常,结果就被JDK忽略了,那岂不是查看日志的时候就看不到具体的StackTrace,那还怎么调试,所以还是关了的好。
  • -XX:+PerfDisableSharedMem:启用标准内存使用。JVM控制分为标准或共享内存,区别在于一个是在JVM内存中,一个是生成/tmp/hsperfdata_{userid}/{pid}文件,存储统计数据,通过mmap映射到内存中,别的进程可以通过文件访问内容。通过这个参数,可以禁止JVM写在文件中写统计数据,代价就是jps、jstat这些命令用不了了,只能通过jmx获取数据。但是在问题排查是,jps、jstat这些小工具是很好用的,比jmx这种很重的东西好用很多,所以需要自己取舍。这里有个GC停顿的例子。
  • -Djava.net.preferIPv4Stack=true:这个参数是属于网络问题的一个参数,可以根据需要设置。在某些开启ipv6的机器中,通过InetAddress.getLocalHost().getHostName()可以获取完整的机器名,但是在ipv4的机器中,可能通过这个方法获取的机器名不完整,可以通过这个参数来获取完整机器名。

以下是对Java8官方文档 https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/sizing.html 这篇的翻译

=============

jvm初始化时, 会将-Xmx指定大小的内存空间预留, 如果-Xms小于-Xmx, 那么不会立即将所有空间给jvm, 未给予的部分为"virtual", 留给堆(heap)中的不同部分(老年代tenured generation和年轻代young generation)将来增长后在需要时使用.

参数NewRatio是老年代相对于年轻代的比例

整个堆(Total heap)

以下关于heap大小增长和收缩的讨论不适应于 parallel collector, 但是控制heap和各个generation的大小的参数, 对parallel collector是同样有效的

在默认情况下, jvm会在每次垃圾回收后增长或收缩heap大小, 以便保持合适比例的空闲空间. 这个百分比由-XX:MinHeapFreeRatio=<minimum> and -XX:MaxHeapFreeRatio=<maximum>来设定. 而整个heap的最小和最大尺寸, 由-Xms<min> 和 -Xmx<max>设定.
例如对于MinHeapFreeRatio = 40, MaxHeapFreeRatio = 70, 当一个generation的可用空间小于40%, 则这个generation会增长空间以保持40%的可用空间, 直到达到这个generation允许的最大空间. 同样的, 如果空闲空间大于70%, 那么这个generation会收缩以保持只有70%的可用空间, 直到这个generation允许的最小空间.

在未设定的情况下, 默认的最大heap空间是由jvm计算得到的, 用于parallel collector的java se和服务器jvm的所有的垃圾回收器都使用同样的算法. 对于上限大小的计算, 在32位和60位机器上不同, 而客户端jvm采用的算法是一样的, 其得到的最大heap大小要小于服务器jvm.

对于服务器应用的heap大小, 有以下原则
1. 除非你对暂停有问题, 给jvm设置尽可能多的内存, 默认的大小远远不够
2. 将-Xms和-Xmx设置为一样的可以避免jvm做heap大小决策, 当然如果你设置的不合适, jvm也不能自己再做调整
3. 一般来说, 在增加处理器核数时也增加内存, 内存分配是可以同步处理的

年轻代 The young generation

除了设置总内存大小外, 对垃圾回收性能影响最大的因素就是给年轻代配置的heap比例. 年轻代设置的越大, 小回收(minor collection)的频率就越低. 不过, 对于给定的heap尺寸, 一个大的年轻代意味着一个小的老年代(tenured generation), 这会增加主回收(major collection)的频率. 最优的配置取决于应用内对象的生命周期分布.

默认情况下, 年轻代的大小由NewRatio决定, 例如-XX:NewRatio=3意味着年轻代和老年代的比例为1:3, 换句话说, 伊甸区和幸存区(eden + survivor, 即e + s0 + s1)将占整个heap大小的1/4.

参数NewSize和MaxNewSize规定了年轻代大小的范围, 将其设置为同样的值, 就固定了年轻代的大小. 这对于调解年轻代的合适大小有帮助.

幸存区 Survivor Space Sizing

可以用SurvivorRatio来调解幸存区的大小, 但是一般来讲这个对性能没多大影响. 例如-XX:SurvivorRatio=6设置了幸存区和伊甸区的比例为1:6, 换句话说, 每个幸存区(有两个)大小都是伊甸区的1/6, 而占整个年轻代的1/8

如果幸存区的空间太小, 如果有一部分对象在YGC中幸存下来, 幸存区只有很少空间容纳这些对象, 结果大部分幸存对象在一次GC后,就会被转移到老年代, 这就不是我们期望的. 而如果幸存区的空间太大, 空闲的空间就是浪费. 在每次垃圾回收时, jvm会选择一个阈值, 即某个对象被移入老年代前要经历的回收次数. 这个阈值取决于是否可以将幸存区保持50%可用空间. 参数 -XX:+PrintTenuringDistribution 可以用于显示这个阈值以及各年轻代对象的年龄, 这对于获取应用的对象的生命周期分布特别有用.

对于默认的jvm设置, 年轻代的大小取决于整个heap大小和NewRatio的值, 而MaxNewRatio默认是not limited

对于服务器应用, 一般的设置原则是
1. 首先确定你能给jvm提供的最大的内存大小, 然后再配置年轻代的大小以得到最佳性能
   注意: 最大的内存大小不要超过机器实际安装的内存大小
2. 如果jvm的heap大小是固定的, 那么老年代和年轻代的内存大小是互相制约的, 需要保证老年代的内存大小足够用于容纳任何时候应用运行所需要的数据, 在这个基础上增加10%到20%的冗余度.
3. 基于上一条对老年代内存大小的要求
  3.1 尽可能给予年轻代足够的内存
  3.2 在增加cpu核数时增加内存, 因为内存分配是可以并行的

============

另外可以参考的文章: https://blog.codecentric.de/en/2012/08/useful-jvm-flags-part-5-young-generation-garbage-collection/   对应的中文翻译 http://weiboxie.iteye.com/blog/2085054

JVM的垃圾回收(GC)机制

参考这两篇就足够

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html Oracle官方的文章

https://www.baeldung.com/jvm-garbage-collectors Baeldung的JVM系列文章

原文地址:https://www.cnblogs.com/milton/p/6358524.html