参数汇总
一、java启动参数共分为三类:
其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
其三是非Stable参数(-XX),此类参数各个jvm实现会有所不同,这些都是不稳定的并且不推荐在生产环境中使用。将来可能会随时取消,需要慎重使用;上都被实现),而且如果在新版本有什么改动也不会发布通知。
对响应时间要求很高的系统来说,良好掌握JVM关于GC调优的参数是很重要的。比如一个高流量地延迟的电子交易平台,他要求的响应时间都是毫秒级的。要获得适合的参数组合需要大量的分析和不断的尝试,更依赖于交易系统的特性。
关于JVM选项的几点:
1) 布尔型参数选项:-XX:+ 打开, -XX:- 关闭。(比如-XX:+PrintGCDetails)
2) 数字型参数选项通过-XX:=设定。数字可以是m/M(兆字节),k/K(千字节),g/G(G字节)。
比如:32K表示32768字节。(比如-XX:HeapDumpPath=./java_pid.hprof)
3) 字符行参数选项通过-XX:=设定,通常用来指定一个文件,路径,或者一个命令列表。(比如-XX:+PrintGCDetails)
命令 java -help可以列出java 应用启动时标准选项(不同的JVM实现是不同的)。
java -X可以列出不标准的参数(这是JVM的扩展特性)。-X相关的选项不是标准的,被改变也不会通知。
如果你想查看当前应用使用的JVM参数,你可以使用:ManagementFactory.getRuntimeMXBean().getInputArguments()。
二、标准参数中比较有用的:
verbose
-verbose:class
输出jvm载入类的相关信息,当jvm报告说找不到类或者类冲突时可此进行诊断。
-verbose:gc
输出每次GC的相关情况。
-verbose:jni
输出native方法调用的相关情况,一般用于诊断jni调用错误信息。
命令查看java的class字节码文件、verbose、synchronize
三、非标准参数又称为扩展参数
非标准化的参数在将来的版本中可能会改变。所有的这类参数都以-X开始,并且可以用java -X来检索。注意,不能保证所有参数都可以被检索出来,其中就没有-Xcomp。例如:
常用的参数介绍:
-Xms512m 设置JVM促使内存为512m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmx512m ,设置JVM最大可用内存为512M。
-Xmn200m:设置年轻代大小为200M。此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是(eden+1 survivor space)不同的。
计算公式有:
年老代大小=-Xmx减去-Xmn
整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。
持久代一般固定大小为64m,所以增大年轻代(-Xmn)后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,默认比例为8(Eden):1(一个survivor),这个值也比较重要。
例如:-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-Xloggc:file
与-verbose:gc功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。
若与verbose命令同时出现在命令行中,则以-Xloggc为准。
-Xprof
跟踪正运行的程序,并将跟踪数据在标准输出输出;适合于开发环境调试。
-Xrunhprof
-Xdebug:JVM调试参数,用于远程调试
例如在tomcat中的远程调试设置方法为-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000
-Xbootclasspath:
-Xbootclasspath用来指定你需要加载,但不想通过校验的类路径。JVM 会对所有的类在加载前进行校验并为每个类通过一个int数值来应用。这个是保证 JVM稳定的必要过程,但比较耗时,如果你希望跳过这个过程,就把你的类通过这个参数来指定。-Xbootclasspath参数、java -jar参数运行应用时classpath的设置方法
-Xnoclassgc:
-Xnoclassgc 表示不对方法区进行垃圾回收。请谨慎使用。见GC 的算法分析、各类垃圾收集器介绍
-XX:MaxMetaspaceSize
java8中-XX:MaxMetaspaceSize=10M设置MetaSpace的最大值为10m。默认是Java的Metaspace空间:不受限制
四、非Stable参数
用-XX作为前缀的参数列表在jvm中可能是不健壮的,SUN也不推荐使用,后续可能会在没有通知的情况下就直接取消了;但是由于这些参数中的确有很多是对我们很有用的,比如我们经常会见到的-XX:PermSize、-XX:MaxPermSize等等;
首先来介绍行为参数:
参数及其默认值 | 描述 |
-XX:-DisableExplicitGC | 禁止调用System.gc();但jvm的gc仍然有效 |
-XX:+MaxFDLimit | 最大化文件描述符的数量限制 |
-XX:+ScavengeBeforeFullGC | 新生代GC优先于Full GC执行 |
-XX:+UseGCOverheadLimit | 在抛出OOM之前限制jvm耗费在GC上的时间比例 |
-XX:-UseConcMarkSweepGC | 对老生代采用并发标记交换算法进行GC |
-XX:-UseParallelGC | 启用并行GC |
-XX:-UseParallelOldGC | 对Full GC启用并行,当-XX:-UseParallelGC启用时该项自动启用 |
-XX:-UseSerialGC | 启用串行GC |
-XX:+UseThreadPriorities | 启用本地线程优先级 |
上面表格中黑体的三个参数代表着jvm中GC执行的三种方式,即串行、并行、并发;
串行(SerialGC)是jvm的默认GC方式,一般适用于小型应用和单处理器,算法比较简单,GC效率也较高,但可能会给应用带来停顿;
并行(ParallelGC)是指GC运行时,对应用程序运行没有影响,GC和app两者的线程在并发执行,这样可以最大限度不影响app的运行;
并发(ConcMarkSweepGC)是指多个线程并发执行GC,一般适用于多处理器系统中,可以提高GC的效率,但算法复杂,系统消耗较大;
性能调优参数列表:
参数及其默认值 | 描述 |
-XX:LargePageSizeInBytes=4m | 设置用于Java堆的大页面尺寸 |
-XX:MaxHeapFreeRatio=70 | GC后java堆中空闲量占的最大比例 |
-XX:MaxNewSize=size | 新生成对象能占用内存的最大值 |
-XX:MaxPermSize=64m | 老生代对象能占用内存的最大值 |
-XX:MinHeapFreeRatio=40 | GC后java堆中空闲量占的最小比例 |
-XX:NewRatio=2 | 新生代内存容量与老生代内存容量的比例 |
-XX:NewSize=2.125m | 新生代对象生成时占用内存的默认值 |
-XX:ReservedCodeCacheSize=32m | 保留代码占用的内存容量 |
-XX:ThreadStackSize=512 | 设置线程栈大小,若为0则使用系统默认值 |
-XX:+UseLargePages | 使用大页面内存 |
我们在日常性能调优中基本上都会用到以上黑体的这几个属性;
调试参数列表:
参数及其默认值 | 描述 |
-XX:-CITime | 打印消耗在JIT编译的时间 |
-XX:ErrorFile=./hs_err_pid<pid>.log | 保存错误日志或者数据到文件中 |
-XX:-ExtendedDTraceProbes | 开启solaris特有的dtrace探针 |
-XX:HeapDumpPath=./java_pid<pid>.hprof | 指定导出堆信息时的路径或文件名 |
-XX:-HeapDumpOnOutOfMemoryError | 当首次遭遇OOM时导出此时堆中相关信息 |
-XX:OnError="<cmd args>;<cmd args>" | 出现致命ERROR之后运行自定义命令 |
-XX:OnOutOfMemoryError="<cmd args>;<cmd args>" | 当首次遭遇OOM时执行自定义命令 |
-XX:-PrintClassHistogram | 遇到Ctrl-Break后打印类实例的柱状信息,与jmap -histo功能相同 |
-XX:-PrintConcurrentLocks | 遇到Ctrl-Break后打印并发锁的相关信息,与jstack -l功能相同 |
-XX:-PrintCommandLineFlags | 打印在命令行中出现过的标记 |
-XX:-PrintCompilation | 当一个方法被编译时打印相关信息 |
-XX:-PrintGC | 每次GC时打印相关信息 |
-XX:-PrintGCDetails | 每次GC时打印详细信息 |
-XX:-PrintGCTimeStamps | 打印每次GC的时间戳 |
-XX:-TraceClassLoading | 跟踪类的加载信息 |
-XX:-TraceClassLoadingPreorder | 跟踪被引用到的所有类的加载信息 |
-XX:-TraceClassResolution | 跟踪常量池 |
-XX:-TraceClassUnloading | 跟踪类的卸载信息 |
-XX:-TraceLoaderConstraints | 跟踪类加载器约束的相关信息 |
五、参数使用方法
1、eclipse中在VM arguments中配置
例如:项目-->properties-->run/debug setting-->选择目标类Test.java-->edit-->arguments-->VM arguments-->输入:-XX:+TraceClassLoading
将会打印class加载明细。
2、java命令中带参数
然后先用javac Test.java进行编译。然后用java -XX:+TraceClassLoading Test。然后就能在控制台打印出一大串的log。
3、启动脚本中增加
-XX:+TraceClassLoading
六、参数汇总
不管是YGC还是Full GC,GC过程中都会对导致程序运行中中断,正确的选择不同的GC策略,调整JVM、GC的参数,可以极大的减少由于GC工作,而导致的程序运行中断方面的问题,进而适当的提高Java程序的工作效率。但是调整GC是以个极为复杂的过程,由于各个程序具备不同的特点,如:web和GUI程序就有很大区别(Web可以适当的停顿,但GUI停顿是客户无法接受的),而且由于跑在各个机器上的配置不同(主要cup个数,内存不同),所以使用的GC种类也会不同(如何选择见GC种类及如何选择)。本文将注重介绍JVM、GC的一些重要参数的设置来提高系统的性能。
JVM内存组成及GC相关内容请见之前的文章:JVM内存组成 GC策略&内存申请。
JVM参数的含义 实例见实例分析
参数名称 | 含义 | 默认值 | |
-Xms | 初始堆大小 | 物理内存的1/64(<1GB) | 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制. |
-Xmx | 最大堆大小 | 物理内存的1/4(<1GB) | 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 |
-Xmn | 年轻代大小(1.4or lator) | 注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小. 增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 | |
-XX:NewSize | 设置年轻代大小(for 1.3/1.4) | ||
-XX:MaxNewSize | 年轻代最大值(for 1.3/1.4) | ||
-XX:PermSize | 设置持久代(perm gen)初始值 | 物理内存的1/64 | |
-XX:MaxPermSize | 设置持久代最大值 | 物理内存的1/4 | |
-Xss | 每个线程的堆栈大小 | JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右 一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长) 和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"” -Xss is translated in a VM flag named ThreadStackSize” 一般设置这个值就可以了。 | |
-XX:ThreadStackSize | Thread Stack Size | (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.] | |
-XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) | -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。 | |
-XX:SurvivorRatio | Eden区与Survivor区的大小比值 | 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 | |
-XX:LargePageSizeInBytes | 内存页的大小不可设置过大, 会影响Perm的大小 | =128m | |
-XX:+UseFastAccessorMethods | 原始类型的快速优化 | ||
-XX:+DisableExplicitGC | 关闭System.gc() | 这个参数需要严格的测试 | |
-XX:MaxTenuringThreshold | 垃圾最大年龄 | 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率 该参数只有在串行GC时才有效. | |
-XX:+AggressiveOpts | 加快编译 | ||
-XX:+UseBiasedLocking | 锁机制的性能改善 | ||
-Xnoclassgc | 禁用垃圾回收 | ||
-XX:SoftRefLRUPolicyMSPerMB | 每兆堆空闲空间中SoftReference的存活时间 | 1s | softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap |
-XX:PretenureSizeThreshold | 对象超过多大是直接在旧生代分配 | 0 | 单位字节 新生代采用Parallel Scavenge GC时无效 另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象. |
-XX:TLABWasteTargetPercent | TLAB占eden区的百分比 | 1% | |
-XX:+CollectGen0First | FullGC时是否先YGC | false |
并行收集器相关参数
-XX:+UseParallelGC | Full GC采用parallel MSC (此项待验证) | 选择垃圾收集器为并行收集器.此配置仅对年轻代有效.即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集.(此项待验证) | |
-XX:+UseParNewGC | 设置年轻代为并行收集 | 可与CMS收集同时使用 JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值 | |
-XX:ParallelGCThreads | 并行收集器的线程数 | 此值最好配置与处理器数目相等 同样适用于CMS | |
-XX:+UseParallelOldGC | 年老代垃圾收集方式为并行收集(Parallel Compacting) | 这个是JAVA 6出现的参数选项 | |
-XX:MaxGCPauseMillis | 每次年轻代垃圾回收的最长时间(最大暂停时间) | 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值. | |
-XX:+UseAdaptiveSizePolicy | 自动选择年轻代区大小和相应的Survivor区比例 | 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开. | |
-XX:GCTimeRatio | 设置垃圾回收时间占程序运行时间的百分比 | 公式为1/(1+n) | |
-XX:+ScavengeBeforeFullGC | Full GC前调用YGC | true | Do young generation GC prior to a full GC. (Introduced in 1.4.1.) |
CMS相关参数
-XX:+UseConcMarkSweepGC | 使用CMS内存收集 | 测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置.??? | |
-XX:+AggressiveHeap | 试图是使用大量的物理内存 长时间大内存使用的优化,能检查计算资源(内存, 处理器数量) 至少需要256MB内存 大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升) | ||
-XX:CMSFullGCsBeforeCompaction | 多少次后进行内存压缩 | 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理. | |
-XX:+CMSParallelRemarkEnabled | 降低标记停顿 | ||
-XX+UseCMSCompactAtFullCollection | 在FULL GC的时候, 对年老代的压缩 | CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。 可能会影响性能,但是可以消除碎片 | |
-XX:+UseCMSInitiatingOccupancyOnly | 使用手动定义初始化定义开始CMS收集 | 禁止hostspot自行触发CMS GC | |
-XX:CMSInitiatingOccupancyFraction=70 | 使用cms作为垃圾回收 使用70%后开始CMS收集 | 92 | 为了保证不出现promotion failed(见下面介绍)错误,该值的设置需要满足以下公式CMSInitiatingOccupancyFraction计算公式 |
-XX:CMSInitiatingPermOccupancyFraction | 设置Perm Gen使用到达多少比率时触发 | 92 | |
-XX:+CMSIncrementalMode | 设置为增量模式 | 用于单CPU情况 | |
-XX:+CMSClassUnloadingEnabled |
辅助信息
-XX:+PrintGC | 输出形式: [GC 118250K->113543K(130112K), 0.0094143 secs] | ||
-XX:+PrintGCDetails | 输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] | ||
-XX:+PrintGCTimeStamps | |||
-XX:+PrintGC:PrintGCTimeStamps | 可与-XX:+PrintGC -XX:+PrintGCDetails混合使用 输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs] | ||
-XX:+PrintGCApplicationStoppedTime | 打印垃圾回收期间程序暂停的时间.可与上面混合使用 | 输出形式:Total time for which application threads were stopped: 0.0468229 seconds | |
-XX:+PrintGCApplicationConcurrentTime | 打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用 | 输出形式:Application time: 0.5291524 seconds | |
-XX:+PrintHeapAtGC | 打印GC前后的详细堆栈信息 | ||
-Xloggc:filename | 把相关日志信息记录到文件以便分析. 与上面几个配合使用 | ||
-XX:+PrintClassHistogram | garbage collects before printing the histogram. | ||
-XX:+PrintTLAB | 查看TLAB空间的使用情况 | ||
XX:+PrintTenuringDistribution | 查看每次minor GC后新的存活周期的阈值 | Desired survivor size 1048576 bytes, new threshold 7 (max 15) |
GC性能方面的考虑
对于GC的性能主要有2个方面的指标:吞吐量throughput(工作时间不算gc的时间占总的时间比)和暂停pause(gc发生时app对外显示的无法响应)。
1. Total Heap
默认情况下,vm会增加/减少heap大小以维持free space在整个vm中占的比例,这个比例由MinHeapFreeRatio和MaxHeapFreeRatio指定。
一般而言,server端的app会有以下规则:
- 对vm分配尽可能多的memory;
- 将Xms和Xmx设为一样的值。如果虚拟机启动时设置使用的内存比较小,这个时候又需要初始化很多对象,虚拟机就必须重复地增加内存。
- 处理器核数增加,内存也跟着增大。
2. The Young Generation
另外一个对于app流畅性运行影响的因素是young generation的大小。young generation越大,minor collection越少;但是在固定heap size情况下,更大的young generation就意味着小的tenured generation,就意味着更多的major collection(major collection会引发minor collection)。
NewRatio反映的是young和tenured generation的大小比例。NewSize和MaxNewSize反映的是young generation大小的下限和上限,将这两个值设为一样就固定了young generation的大小(同Xms和Xmx设为一样)。
如果希望,SurvivorRatio也可以优化survivor的大小,不过这对于性能的影响不是很大。SurvivorRatio是eden和survior大小比例。
一般而言,server端的app会有以下规则:
- 首先决定能分配给vm的最大的heap size,然后设定最佳的young generation的大小;
- 如果heap size固定后,增加young generation的大小意味着减小tenured generation大小。让tenured generation在任何时候够大,能够容纳所有live的data(留10%-20%的空余)。
经验&&规则
- 年轻代大小选择
- 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,年轻代收集发生的频率也是最小的.同时,减少到达年老代的对象.
- 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用.
- 避免设置过小.当新生代设置过小时会导致:1.YGC次数更加频繁 2.可能导致YGC对象直接进入旧生代,如果此时旧生代满了,会触发FGC.
- 年老代大小选择
- 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:
并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在年轻代和年老代回收上的时间比例。 - 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象.
- 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:
- 较小堆引起的碎片问题
因为年老代的并发收集器使用标记,清除算法,所以不会对堆进行压缩.当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象.但是,当堆空间较小时,运行一段时间以后,就会出现"碎片",如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记,清除方式进行回收.如果出现"碎片",可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩.
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩 - 用64位操作系统,Linux下64位的jdk比32位jdk要慢一些,但是吃得内存更多,吞吐量更大
- XMX和XMS设置一样大,MaxPermSize和MinPermSize设置一样大,这样可以减轻伸缩堆大小带来的压力
- 使用CMS的好处是用尽量少的新生代,经验值是128M-256M, 然后老生代利用CMS并行收集, 这样能保证系统低延迟的吞吐效率。 实际上cms的收集停顿时间非常的短,2G的内存, 大约20-80ms的应用程序停顿时间
- 系统停顿的时候可能是GC的问题也可能是程序的问题,多用jmap和jstack查看,或者killall -3 java,然后查看java控制台日志,能看出很多问题。(相关工具的使用方法将在后面的blog中介绍)
- 仔细了解自己的应用,如果用了缓存,那么年老代应该大一些,缓存的HashMap不应该无限制长,建议采用LRU算法的Map做缓存,LRUMap的最大长度也要根据实际情况设定。
- 采用并发回收时,年轻代小一点,年老代要大,因为年老大用的是并发回收,即使时间长点也不会影响其他程序继续运行,网站不会停顿
- JVM参数的设置(特别是 –Xmx –Xms –Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold等参数的设置没有一个固定的公式,需要根据PV old区实际数据 YGC次数等多方面来衡量。为了避免promotion faild可能会导致xmn设置偏小,也意味着YGC的次数会增多,处理并发访问的能力下降等问题。每个参数的调整都需要经过详细的性能测试,才能找到特定应用的最佳配置。
promotion failed:
垃圾回收时promotion failed是个很头痛的问题,一般可能是两种原因产生,第一个原因是救助空间不够,救助空间里的对象还不应该被移动到年老代,但年轻代又有很多对象需要放入救助空间;第二个原因是年老代没有足够的空间接纳来自年轻代的对象;这两种情况都会转向Full GC,网站停顿时间较长。
解决方方案一:
第一个原因我的最终解决办法是去掉救助空间,设置-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0即可,第二个原因我的解决办法是设置CMSInitiatingOccupancyFraction为某个值(假设70),这样年老代空间到70%时就开始执行CMS,年老代有足够的空间接纳来自年轻代的对象。
解决方案一的改进方案:
又有改进了,上面方法不太好,因为没有用到救助空间,所以年老代容易满,CMS执行会比较频繁。我改善了一下,还是用救助空间,但是把救助空间加大,这样也不会有promotion failed。具体操作上,32位Linux和64位Linux好像不一样,64位系统似乎只要配置MaxTenuringThreshold参数,CMS还是有暂停。为了解决暂停问题和promotion failed问题,最后我设置-XX:SurvivorRatio=1 ,并把MaxTenuringThreshold去掉,这样即没有暂停又不会有promotoin failed,而且更重要的是,年老代和永久代上升非常慢(因为好多对象到不了年老代就被回收了),所以CMS执行频率非常低,好几个小时才执行一次,这样,服务器都不用重启了。
-Xmx4000M -Xms4000M -Xmn600M -XX:PermSize=500M -XX:MaxPermSize=500M -Xss256K -XX:+DisableExplicitGC -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log
CMSInitiatingOccupancyFraction值与Xmn的关系公式
上面介绍了promontion faild产生的原因是EDEN空间不足的情况下将EDEN与From survivor中的存活对象存入To survivor区时,To survivor区的空间不足,再次晋升到old gen区,而old gen区内存也不够的情况下产生了promontion faild从而导致full gc.那可以推断出:eden+from survivor < old gen区剩余内存时,不会出现promontion faild的情况,即:
(Xmx-Xmn)*(1-CMSInitiatingOccupancyFraction/100)>=(Xmn-Xmn/(SurvivorRatior+2)) 进而推断出:
CMSInitiatingOccupancyFraction <=((Xmx-Xmn)-(Xmn-Xmn/(SurvivorRatior+2)))/(Xmx-Xmn)*100
例如:
当xmx=128 xmn=36 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-36)-(36-36/(1+2)))/(128-36)*100 =73.913
当xmx=128 xmn=24 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-24)-(24-24/(1+2)))/(128-24)*100=84.615…
当xmx=3000 xmn=600 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((3000.0-600)-(600-600/(1+2)))/(3000-600)*100=83.33
CMSInitiatingOccupancyFraction低于70% 需要调整xmn或SurvivorRatior值。
令:
网上一童鞋推断出的公式是::(Xmx-Xmn)*(100-CMSInitiatingOccupancyFraction)/100>=Xmn 这个公式个人认为不是很严谨,在内存小的时候会影响xmn的计算。
关于实际环境的GC参数配置见:实例分析 监测工具见JVM监测
七、示例:
1、-Xbootclasspath示例:-Xbootclasspath参数、java -jar参数运行应用时classpath的设置方法
2、-XX:-HeapDumpOnOutOfMemoryError示例:JVM参数之-XX:+HeapDumpOnOutOfMemoryError(导出内存溢出的堆信息(hprof文件))
3、-verbose示例:命令查看java的class字节码文件、verbose、synchronize
4、-XX:MaxMetaspaceSize示例:mat之三--PermGen Metaspace永久代内存分析
5、-Xss示例:认识Java虚拟机的内部体系结构
6、-XX:+PrintGCDetails GC日志分析(jdk1.8)
7、gc中的参数示例:GC 的算法分析、各类垃圾收集器介绍
调优实践
堆内存大小配置
// 堆内存大小配置 -Xmx100m -Xms50m
public static void main(String[] args) {
System.out.print("最大内存: ");
System.out.println(Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
System.out.print("可用内存: ");
System.out.println(Runtime.getRuntime().freeMemory() / 1024 / 1024 + "MB");
System.out.print("已使用内存: ");
System.out.println(Runtime.getRuntime().totalMemory() / 1024 / 1024 + "MB");
}
参数配置前运行 ,以下是本机默认值。(每台机器配置不同值可能会不同)
最大内存: 2665MB
可用内存: 177MB
已使用内存: 180MB
配置 jvm 参数
main方法位置右击
使用示例: -Xmx100m -Xms50m
说明: 当前应用最大可用内存为100M,初始内存为50M
main 方法如果从建立一次都没运行过,下图中 左侧1 的位置不会发现程序,运行一下在打开下图配置就有了。
配置jvm参数
参数配置后运行
最大内存: 89MB
可用内存: 46MB
已使用内存: 48MB
参数配置信息最大可用内存为100M,初始内存为50M
按理这里计算出来的最大内存是100M,已使用内存50M,但结果却都小了一些
这是正常的误差,不去纠结少了的那一小部分内存
设置新生代比例参数
-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
说明:堆内存初始化值20m,堆内存最大值20m,新生代最大值可用1m,eden空间和from/to空间的比例为2/1,GC回收日志,使用串行回收
jvm 参数配置
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
byte[] b = new byte[1 * 1024 * 1024];
}
}
程序运行
运行结果
- 箭头所指就是 触发GC回收信息 - GC (Allocation Failure)
- 框起来的是新生代内存信息,我们设置了
-Xmn1m (eden+from+to <= 1m) - -XX:SurvivorRatio=2 ( eden:from:to = 2:1:1)
默认 eden:from:to = 8:1:1
设置新生代与老年代比例参数
使用示例: -Xms20m -Xmx20m -XX:SurvivorRatio=2 -XX:+PrintGCDetails
-XX:+UseSerialGC -XX:NewRatio=2
说明:堆内存初始化值20m,堆内存最大值20m,新生代最大值可用1m,eden空间和from/to空间的比例为2/1,新生代和老年代的占比为1/2
jvm 配置
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
byte[] b = new byte[1 * 1024 * 1024];
}
}
程序运行
运行结果
- -XX:SurvivorRatio=2,(上面框)新生代中 eden:from:to=2:1:1
- -XX:NewRatio=2, 新生代(eden+from+to) : 老年代 约等 1: 2,(下面框)老年代内存信息
默认 新生代(eden+from+to) : 老年代 = 1: 2
实战OutOfMemoryError
- 原因: 堆最大内存值小了,不能满足当前程序
- 解决: 增大堆最大内存空间
Java堆溢出 - OutOfMemoryError异常,在开发过程中很容易遇到,在没接触 JVM 之前遇到这样的错误可能会惊一下,莫名其妙。接触 JVM 之后你会发现也就这么回事。
jvm 配置
-Xms1m -Xmx10m -XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
说明:堆内存初始化值1m,堆内存最大值10m,GC回收日志
public static void main(String[] args) {
List<Object> listObject = new ArrayList<>();
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
Byte[] bytes = new Byte[1 * 1024 * 1024];
listObject.add(bytes);
}
System.out.println("添加成功...");
}
程序运行
运行结果
报错了!!OutOfMemoryError
解决
- 增大堆最大内存 -Xmx10m 更改为 -Xmx100m
再次运行
运行结果
添加成功,没有错误
实战StackOverflowError
栈溢出 - java.lang.StackOverflowError
- 栈溢出 产生于递归调用,循环遍历是不会的,但是循环方法里面产生递归调用, 也会发生栈溢出。
- 解决办法: -Xss5m 设置线程最大调用深度
private static int count;
public static void count() {
try {
count++;
count();
} catch (Throwable e) {
System.out.println("最大深度:" + count);
e.printStackTrace();
}
}
public static void main(String[] args) {
count();
}
main方法 里面进行递归调用,运行如下:
最大深度:10775
java.lang.StackOverflowError
at sun.misc.Unsafe.compareAndSwapLong(Native Method)
...
这里每次运行最大深度不同,但相差不大(本机默认深度10775左右)
如果是比较深的递归,就需要配置最大深度
比如现在有一个递归A方法执行报错StackOverflowError,那么A递归方法的访问的深度已经大于了默认的最大深度。
-
Xss5m 设置最大调用深度
jvm配置
再次运行
最大深度:257058
java.lang.StackOverflowError
at sun.misc.Unsafe.compareAndSwapLong(Native Method)
...
257058 对比之前 10775
修改 -Xss5m 为 - Xss1m , 再次运行 :
最大深度:10674
java.lang.StackOverflowError
at sun.misc.Unsafe.compareAndSwapLong(Native Method)
10674 对比之前 10775 , 好像差不多
可以推测默认的深度可能为 1m
- StackOverflowError 产生于递归调用
- 循环遍历不生效
误区
private static int count;
public static void count() {
try {
count++;
// count();
} catch (Throwable e) {
System.out.println("最大深度:" + count);
e.printStackTrace();
}
}
// 错误示例
public static void main(String[] args) {
for (int i = 0; i < 11000; i++) {
count();
}
}
内存溢出与内存泄漏区别
- java内存泄漏,使用过的内存空间没有被及时释放,长时间占用内存,导致系统无法再给你提供内存资源(内存资源耗尽)
- Java内存溢出,要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出(杯中的水满了溢出)
jvm调优以及调优监控工具实践
在遇到实际性能问题时,除了关注系统性能指标。还要结合应用程序的系统的日志、堆栈信息、GClog、threaddump等数据进行问题分析和定位。关于性能指标分析可以参考前一篇JVM性能调优实践——性能指标分析。
JVM的调优和故障处理可以使用JDK的几个常用命令工具。因为本文是基于Docker容器内部的Springboot服务。需要调整一下docker容器的启动参数,才可以使用jmap等工具。jmap命令需要使用Linux的Capability的PTRACE_ATTACH权限。而Docker自1.10在默认的seccomp配置文件中禁用了PTRACE_ATTACH。目前使用的Docker version是17.04.0-ce。支持的Capability列表可以详看runtime-privilege-and-linux-capabilities。
调整Capability的方式也比较方便。可以如下直接在运行参数后面加 cap_add,cap-drop
$docker run --cap-add=ALL --cap-drop=MKNOD ...
也可以在compose中增加:
cap_add:
- ALL
cap_drop:
- NET_ADMIN
- SYS_ADMIN
Docker容器中的服务进程
在排查问题时,一般是先通过JVM性能调优实践——性能指标分析中的几个命令来分析基础的服务器状态和信息。在微服务架构中,每台服务器部署着若干运行着服务的容器。在不能通过应用日志或者问题现象定位问题服务时,需要找到问题容器。
先通过TOP命令找到耗费关键资源的进程。
top - 11:45:13 up 318 days, 20:43, 2 users, load average: 0.15, 0.19, 0.18
Tasks: 172 total, 1 running, 171 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.1 us, 1.9 sy, 0.0 ni, 94.7 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
KiB Mem: 8175392 total, 7868636 used, 306756 free, 204400 buffers
KiB Swap: 0 total, 0 used, 0 free. 849564 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
31399 root 20 0 3585612 806804 12228 S 3.0 9.9 548:20.94 java
6331 root 20 0 3445612 925660 15784 S 2.7 11.3 41:40.29 java
31122 root 20 0 3460712 888776 11568 S 2.0 10.9 484:19.31 java
31147 root 20 0 3288180 811476 12748 S 1.3 9.9 263:44.73 java
8506 root 20 0 3254088 750880 6116 S 1.0 9.2 760:45.19 java
22940 root 20 0 1029012 70584 23396 S 0.7 0.9 0:10.68 node
24550 root 20 0 1229088 43096 8712 S 0.7 0.5 160:15.74 node
7 root 20 0 0 0 0 S 0.3 0.0 606:49.74 rcu_sched
454 sshd 20 0 32792 1924 188 S 0.3 0.0 29:15.40 nginx
13721 root 20 0 25396 1956 1324 S 0.3 0.0 56:29.17 AliYunDunUpdate
16225 root 20 0 3072752 429296 6848 S 0.3 5.3 42:51.01 java
20795 root 20 0 2408848 75344 3960 S 0.3 0.9 2361:22 java
23581 root 20 0 16736 2676 2196 R 0.3 0.0 0:00.01 top
31352 root 20 0 206920 1488 1024 S 0.3 0.0 1:20.48 docker-containe
32000 root 20 0 3061760 403708 6548 S 0.3 4.9 127:01.39 java
... 省略其他信息
因为Docker容器中还有java进程,所以需要找到具体的父子进程id.用ps -ef命令如下所示。第二列是PID(进程ID),第三列是PPID(父进程ID)。
$ps -ef |grep java
root 6310 6293 0 May21 ? 00:00:00 /bin/sh -c java -Dcontainer.host.ip=...
root 6331 6310 2 May21 ? 00:41:51 java -Dcontainer.host.ip= -server ...
root 8482 8465 0 Apr16 ? 00:00:00 /bin/sh -c java -Dcontainer.host.ip...
root 8506 8482 1 Apr16 ? 12:40:53 java -Dcontainer.host.ip= -server...
... 省略其他信息
可以使用docker inspect查看容器内部信息,找到对应的容器实例的进程信息。如下即可打印当前宿主机的所有运行的容器实例的PID,为了方便映射,可以打印对应容器名字,或者容器ID:
## 打印容器pid和容器id
$docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.ID}}' | grep "^${PID}"
## 打印容器pid和容器name
$ docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.Name}}' | grep "^${PID}"
6310, /service-item
31369, /gateway-api
31094, /service-resource
31025, /service-trade
30916, /service-user
16204, /service-analytics
8482, /service-financial
... 省略其他信息
如果要分析最消耗内存的进程,对应的pid= 6331,其所在的docker进程id也即父进程id= 6310,可以定位出service-item服务最消耗内存资源。定位到服务之后,即可使用docker exec -it service-item ‘/bin/sh’查看容器内部信息。
JVM调优基础命令
在容器内部,就可以进一步使用jdk提供的jps、jstack、jstat、jmap等工具来进行jvm问题排查和调优。
jps[options] [hostid]
jps主要用来输出JVM中运行的进程状态信息。
-q 输出类名、Jar名和传入main方法的参数
-m 输出传入main方法的参数
-l 输出main类或Jar的全限名
-v 输出传入JVM的参数
如下查看运行的java进程信息,打印jar名以及运行main方法传入的参数:
/opt/app # jps -l -m
6 /opt/app/app.jar --server.port=8080
327 sun.tools.jps.Jps -l -m
jstat
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]
jstat命令可以用于持续观察虚拟机内存中各个分区的使用率以及GC的统计数据。vmid是Java虚拟机ID,在Linux/Unix系统取进程ID。
如下面输出的信息,采样时间间隔为1000ms,采样5次:
/opt/app # jstat -gc 6 1000 5
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
1536.0 1536.0 1233.7 0.0 171520.0 169769.2 249344.0 57018.6 93912.0 91906.8 11264.0 10853.7 6224 47.439 5 3.423 50.863
1536.0 1536.0 1233.7 0.0 171520.0 169805.4 249344.0 57018.6 93912.0 91906.8 11264.0 10853.7 6224 47.439 5 3.423 50.863
1536.0 1536.0 0.0 1536.0 171520.0 3527.9 249344.0 60347.4 96728.0 94808.1 11520.0 11174.7 6225 47.453 5 3.423 50.876
1536.0 1536.0 0.0 1536.0 171520.0 4742.1 249344.0 60347.4 96728.0 94808.1 11520.0 11174.7 6225 47.453 5 3.423 50.876
1536.0 1536.0 0.0 1536.0 171520.0 7589.3 249344.0 60347.4 96728.0 94808.1 11520.0 11174.7 6225 47.453 5 3.423 50.876
上述各个列的含义:
S0C、S1C、S0U、S1U:young代的Survivor 0/1区容量(Capacity)和使用量(Used)。0是FromSurvivor,1是ToSurvivor。
EC、EU:Eden区容量和使用量
OC、OU:年老代容量和使用量
MC、MU:元数据区(Metaspace)已经committed的内存空间和使用量
CCSC、CCSU:压缩Class(Compressed class space)committed的内存空间和使用量。
YGC、YGT:young代GC次数和GC耗时
FGC、FGCT:Full GC次数和Full GC耗时
GCT:GC总耗时
可以通过分区占用量上看到,在第2-3秒之间发生了一次YGC。YGC次数+1,并且Survivor from区的内存空间从1233.7->0,Survivor from从0->1536。Eden区也释放了很多内存空间。其他变化的空间占用也有元数据区以及元数据区的压缩Class区。Compressed class space也是元数据区的一部分,默认是1G,也可以关闭。具体的jvm8内存分布不再详述。下一篇GC优化会再展开整理下。
如果只看gc的总统计信息,也可以用jstat -gcutil vmid查询:
/opt/app # jstat -gcutil 6
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 100.00 73.76 24.20 98.02 97.00 6225 47.453 5 3.423 50.876
jmap [option] pid
jmap可以用来查看堆内存的使用详情。内存各个分区可以通过jmap -heap pid来查看。得到的输出如下:
$jmap -heap 6
Attaching to process ID 6, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.121-b13
using thread-local object allocation.
Parallel GC with 2 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 536870912 (512.0MB)
NewSize = 44564480 (42.5MB)
MaxNewSize = 178782208 (170.5MB)
OldSize = 89653248 (85.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 170393600 (162.5MB)
used = 99020080 (94.43290710449219MB)
free = 71373520 (68.06709289550781MB)
58.11255821814904% used
From Space:
capacity = 4194304 (4.0MB)
used = 786432 (0.75MB)
free = 3407872 (3.25MB)
18.75% used
To Space:
capacity = 4194304 (4.0MB)
used = 0 (0.0MB)
free = 4194304 (4.0MB)
0.0% used
PS Old Generation
capacity = 255328256 (243.5MB)
used = 65264912 (62.24147033691406MB)
free = 190063344 (181.25852966308594MB)
25.561178783126927% used
39531 interned Strings occupying 4599760 bytes.
Heap Configuration是堆内存的配置信息。可以通过运行参数改变。一般通过分析内存分布和使用情况以及GC信息,可以针对不同的应用不断调整到合适的堆内存分区配置。
Heap Usage可以看堆内存实时的占用情况。
使用jmap -histo[:live] pid查看堆内存中的对象的数目,占用内存(单位是byte),如果带上live则只统计活对象,如下:
/opt/app/logs # jmap -histo:live 6 | more
num #instances #bytes class name
----------------------------------------------
1: 127610 19132008 [C
2: 6460 4074512 [B
3: 37041 3259608 java.lang.reflect.Method
4: 125182 3004368 java.lang.String
5: 86616 2771712 java.util.concurrent.ConcurrentHashMap$Node
6: 70783 2265056 java.util.HashMap$Node
7: 17686 1967496 java.lang.Class
8: 15834 1448440 [Ljava.util.HashMap$Node;
9: 35360 1414400 java.util.LinkedHashMap$Entry
10: 21948 1231624 [Ljava.lang.Object;
11: 9940 1165728 [I
12: 986 1064480 [Ljava.util.concurrent.ConcurrentHashMap$Node;
13: 18685 1046360 java.util.LinkedHashMap
14: 30351 971232 java.lang.ref.WeakReference
15: 50340 805440 java.lang.Object
16: 13490 539600 java.lang.ref.SoftReference
17: 17705 513768 [Ljava.lang.String;
18: 18781 450744 org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource$DefaultCacheKey
19: 20272 434456 [Ljava.lang.Class;
20: 17270 414480 java.beans.MethodRef
21: 23616 377856 java.lang.Integer
22: 11192 358144 java.util.LinkedList
23: 14911 357864 java.util.ArrayList
24: 5700 319200 java.beans.MethodDescriptor
以上示例的排序是按照占用内存字节数倒序的。class name列中”[C,[B,[I “是代表char,byte,int.”[L+类名”代表其他实例。这种写法跟Class文件的Java的类型表述含义是一致的。
在进行问题排查时,可以使用jmap把进程内存使用情况dump到文件中,或者dump**.hprof**文件,在本地使用MAT(Eclipse Memory Analyzer)进行分析。也可以直接用jhat分析查看。
/opt/app# jmap -dump:format=b,file=heapdump 6
Dumping heap to /opt/app/logs/heapdump ...
Heap dump file created
/opt/app# jmap -dump:live,format=b,file=heapLive.hprof 6
jstack [option] pid
jstack可以用来查看Java进程内的线程堆栈信息。
-l long listings,会打印出额外的锁信息,在发生死锁时可以用jstack -l pid来观察锁持有情况
-m mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native方法)
输出信息如下:
/opt/app # jstack -l 6
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.121-b13 mixed mode):
"elasticsearch[Oneg the Prober][listener][T#1]" #221 daemon prio=5 os_prio=0 tid=0x00007fc2a418a800 nid=0x195 waiting on condition [0x00007fc28318d000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000e29f88d0> (a java.util.concurrent.LinkedTransferQueue)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.LinkedTransferQueue.awaitMatch(LinkedTransferQueue.java:737)
at java.util.concurrent.LinkedTransferQueue.xfer(LinkedTransferQueue.java:647)
at java.util.concurrent.LinkedTransferQueue.take(LinkedTransferQueue.java:1269)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- None
在线上问题排查线程锁信息时,jstack是个非常好用的工具,结合应用日志可以迅速定位到问题线程。
Java性能分析工具
对于Java性能调优,以前一直比较好用的工具是JRockit,JProfile(商业)等工具,但随着JDK7 up40版本之后,jdk会自带JMC(JavaMissionControl)工具。可以分析本地应用以及连接远程ip使用。提供了实时分析线程、内存,CPU、GC等信息的可视化界面。从jdk8 up40开始,JMC还提供了在运行时创建JFR记录(飞行记录器)。如果是全面分析heap dump,再综合使用MAT(Eclipse Memory Analyzer)。基本就可以做很多日常的性能调优以及线上问题排查了。下文简单介绍一些JMC,基于java version “1.8.0_60”。
Java Mission Control
在Mac上使用的话,需要先找到jdk中的jmc路径。
$find /Library/Java -name missioncontrol
在我本地的目录是/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/lib/missioncontrol/。打开jmc之后,常用的话可以留在dock下面。
启动需要观察的应用,然后即可在JMC的MBean服务器中观察到综合信息如下:
进一步观察内存以及GC的情况,视图如下,可以观察到运行时内存各个分区的占用率。对于“堆直方图”默认是不开启的,可以通过右上角的刷新值来启用,会影响性能。一般用于排查内存中的大对象的回收问题以及OOM问题时可以开启观察。
对于线程死锁、线程池资源方面的分析,可以到线程视图中观察活动线程。
Java飞行记录器(Java Flight Recorder)
Java 8 up40开始,可以使用JMC创建JFR记录。JFR可以采样分析收集Java应用程序以及JVM的信息, 它的最小开销小于2%,不会影响其他JVM优化。JFR不会记录所有方法调用,只会探测热点方法,但不包含Native方法的线程采样。如果要开启JFR,需要应用启动参数中添加:
-XX:+UnlockCommertialFeatures -XX:+FlightRecorder
一般还是建议本地调优和分析时使用。JFR可以提供固定时间的采样(默认是1min),以及持续时间的记录。它们都会dump到一个“.jfr”的文件中。
分析内存信息如下,可以看到内存使用量,以及基础的GC配置和统计信息:
详细分析内存情况时,需要进一步查看“内存分配”以及“对象统计信息”。其中“对象统计信息”也是默认不开启的,需要在创建jfr时选择“启用”如下:
然后即可看到对象统计信息:
对于热点方法以及热点线程的采样分析图表也很直观,在分析一些循环调用时可以重点关注热点方法,对于有问题的热点方法可以进一步查看“堆栈跟踪”下的调用链:
总结
本文主要介绍了java常用的性能优化和排查问题的工具,以及JavaMissionControl工具的一些功能。JMC是官方提供的免费工具,结合MAT,基本可以处理性能优化的80%场景。JMC还可以链接远程ip进行分析。但对于线上问题排查,还是建议使用jstat,jstack,jmap工具等,结合top、vmstat等快速排查和定位问题。
性能排查一般问题都集中在cpu、内存。前者分析线程,后者分析具体出现问题的内存分区。对于磁盘、IO等资源瓶颈需要综合很多业务场景进行具体定位。
还有一部分比较大的话题,就是GC,会在下一篇进行总结和整理。
调优总结
#一些概念
##数据类型
Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。
####基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress
####引用类型包括:类类型,接口类型和数组。
##堆与栈
堆和栈是程序运行的关键,很有必要把他们的关系说清楚。
栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美。
在Java中,Main函数就是栈的起始点,也是程序的起始点。
程序要运行总是有一个起点的。同C语言一样,java中的Main就是那个起点。无论什么java程序,找到main就找到了程序执行的入口:)
堆中存什么?栈中存什么?
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4btye的引用(堆栈分离的好处:))。
为什么不把基本类型放堆中呢?因为其占用的空间一般是1~8个字节——需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有什么意义的(还会浪费空间,后面说明)。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是,Java中参数传递时的问题。
Java中的参数传递时传值呢?还是传引用?
要说明这个问题,先要明确两点:
####1. 不要试图与C进行类比,Java中没有指针的概念
####2. 程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。
明确以上两点后。Java在方法调用传递参数时,因为没有指针,所以它都是进行传值调用(这点可以参考C的传值调用)。因此,很多书里面都说Java是进行传值调用,这点没有问题,而且也简化的C中复杂性。
但是传引用的错觉是如何造成的呢?在运行栈中,基本类型和引用的处理是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是堆中的数据。所以这个修改是可以保持的了。
对象,从某种意义上说,是由基本类型组成的。可以把一个对象看作为一棵树,对象的属性如果还是对象,则还是一颗树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值本身都是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个对象引用),则可以修改这个节点下面的所有内容。
堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得Java的垃圾回收成为可能。
Java中,栈的大小通过-Xss来设置,当栈中存储数据比较多时,需要适当调大这个值,否则会出现java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息都是方法返回的记录点。
##Java对象的大小
基本数据的类型的大小是固定的,这里就不多说了。对于非基本类型的Java对象,其大小就值得商榷。
在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。看下面语句:
Object ob = new Object();
这样在程序中完成了一个Java对象的生命,但是它所占的空间为:4byte+8byte。4byte是上面部分所说的Java栈中保存引用的所需要的空间。而那8byte则是Java堆中对象的信息。因为所有的Java非基本类型的对象都需要默认继承Object对象,因此不论什么样的Java对象,其大小都必须是大于8byte。
有了Object对象的大小,我们就可以计算其他对象的大小了。
Class NewObject {
int count;
boolean flag;
Object ob;
}
其大小为:空对象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。但是因为Java在对对象内存分配时都是以8的整数倍来分,因此大于17byte的最接近8的整数倍的是24,因此此对象的大小为24byte。
这里需要注意一下基本类型的包装类型的大小。因为这种包装类型已经成为对象了,因此需要把他们作为对象来看待。包装类型的大小至少是12byte(声明一个空Object至少需要的空间),而且12byte没有包含任何有效信息,同时,因为Java对象大小是8的整数倍,因此一个基本类型包装类的大小至少是16byte。这个内存占用是很恐怖的,它是使用基本类型的N倍(N>2),有些类型的内存占用更是夸张(随便想下就知道了)。因此,可能的话应尽量少使用包装类。在JDK5.0以后,因为加入了自动类型装换,因此,Java虚拟机会在存储方面进行相应的优化。
##引用类型
对象引用类型分为强引用、软引用、弱引用和虚引用。
强引用:就是我们一般声明对象是时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收
软引用:软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。
弱引用:弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。
强引用不用说,我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们一般被作为缓存使用,而且一般是在内存大小比较受限的情况下做为缓存。因为如果内存足够大的话,可以直接使用强引用作为缓存即可,同时可控性更高。因而,他们常见的是被使用在桌面应用系统的缓存。
可以从不同的的角度去划分垃圾回收算法: ##按照基本回收策略分
引用计数(Reference Counting):
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
标记-清除(Mark-Sweep):
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
复制(Copying):
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
标记-整理(Mark-Compact):
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
##按分区对待的方式分
增量收集(Incremental Collecting):实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。
分代收集(Generational Collecting):基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
##按系统线程分
串行收集:串行收集使用单线程处理所有垃圾回收工作, 因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。
并行收集:并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。
并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。
##如何区分垃圾
上面说到的“引用计数”法,通过统计控制生成对象和删除对象时的引用数来判断。垃圾回收程序收集计数为0的对象即可。但是这种方法无法解决循环引用。所以,后来实现的垃圾判断算法中,都是从程序运行的根节点出发,遍历整个对象引用,查找存活的对象。那么在这种方式的实现中,垃圾回收从哪儿开始的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的。上面分析的堆和栈的区别,其中栈是真正进行程序执行地方,所以要获取哪些对象正在被使用,则需要从Java栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。
同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就形成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。
因此,垃圾回收的起点是一些根对象(java栈, 静态变量, 寄存器...)。而最简单的Java栈就是Java程序执行的main函数。这种回收方式,也是上面提到的“标记-清除”的回收方式
##如何处理碎片
由于不同Java对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中,“复制”方式和“标记-整理”方式,都可以解决碎片的问题。
##如何解决同时存在的对象创建和对象回收问题
垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的,一个回收内存,一个分配内存,从这点看,两者是矛盾的。因此,在现有的垃圾回收方式中,要进行垃圾回收前,一般都需要暂停整个应用(即:暂停内存的分配),然后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接,而且最有效的解决二者矛盾的方式。
但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,对应应用暂停的时间也会相应的增大。一些对相应时间要求很高的应用,比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就很有可能超过这个限制,在这种情况下,垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时,“碎片”问题将会比较难解决。
##为什么要分代
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
##如何分代
如图所示:
虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
年轻代:
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
年老代:
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代:
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
##什么情况下触发垃圾回收
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
· 年老代(Tenured)被写满
· 持久代(Perm)被写满
· System.gc()被显示调用
·上一次GC之后Heap的各域分配策略动态变化
##分代垃圾回收流程示意
##选择合适的垃圾收集算法
用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。可以使用-XX:+UseSerialGC打开。
对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC.打开。并行收集器在J2SE5.0第六6更新上引入,在Java SE6.0中进行了增强--可以对年老代进行并行收集。如果年老代不使用并发收集的话,默认是使用单线程进行垃圾回收,因此会制约扩展能力。使用-XX:+UseParallelOldGC打开。
使用-XX:ParallelGCThreads=设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。
此收集器可以进行如下配置:
最大垃圾回收暂停:指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis=<N><N>指定。>$$为毫秒.如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。
吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=<N><N>来设定,公>式为1/(1+N)。例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收。默认情况为99,即>1%的时间用于垃圾回收。
并发收集器
可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。
并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器 会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。
在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。
浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。
启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction=指定还有多少剩余堆时开始执行并发收集
##小结
串行处理器:
- 适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。
- 缺点:只能用于小型应用
并行处理器:
- 适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。
- 缺点:垃圾收集过程中应用响应时间可能加长
并发处理器:
- 适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。
以下配置主要针对分代垃圾回收算法而言。 ##堆大小设置
年轻代的设置很关键 JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。 典型设置:
java -Xmx3550m -Xms3550m -Xmn2g –Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
##回收器选择
JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。
吞吐量优先的并行收集器
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
####典型配置:
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
n java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
响应时间优先的并发收集器
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
####典型配置:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC: 设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
##辅助信息
JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
-XX:+PrintGC:输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails:输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用 输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用。输出形式:Application time: 0.5291524 seconds
-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用。输出形式:Total time for which application threads were stopped: 0.0468229 seconds
-XX:PrintHeapAtGC: 打印GC前后的详细堆栈信息。输出形式:
34.702: [GC {Heap before gc invocations=7: def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000) eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000) from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000) to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000) tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000) the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000) compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000) the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000) ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000) rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000) 34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8: def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000) eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000) from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000) to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000) tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000) the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000) compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000) the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000) ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000) rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000) } , 0.0757599 secs]
-Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
##常见配置汇总
堆设置 -Xms:初始堆大小 -Xmx:最大堆大小 -XX:NewSize=n:设置年轻代大小 -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 -XX:MaxPermSize=n:设置持久代大小
收集器设置 -XX:+UseSerialGC:设置串行收集器 -XX:+UseParallelGC:设置并行收集器 -XX:+UseParalledlOldGC:设置并行年老代收集器 -XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename
并行收集器设置 -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。 -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置 -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。 -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
##调优总结
年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
- 并发垃圾收集信息
- 持久代并发收集次数
- 传统GC信息
- 花在年轻代和年老代回收上的时间比例 减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用
一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置: 1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。 2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
##垃圾回收的瓶颈
传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限。但是他无法解决的一个问题,就是Full GC所带来的应用暂停。在一些对实时性要求很高的应用场景下,GC暂停所带来的请求堆积和请求失败是无法接受的。这类应用可能要求请求的返回时间在几百甚至几十毫秒以内,如果分代垃圾回收方式要达到这个指标,只能把最大堆的设置限制在一个相对较小范围内,但是这样有限制了应用本身的处理能力,同样也是不可接收的。
分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器,支持最大暂停时间的设置,但是受限于分代垃圾回收的内存划分模型,其效果也不是很理想。
为了达到实时性的要求(其实Java语言最初的设计也是在嵌入式系统上的),一种新垃圾回收方式呼之欲出,它既支持短的暂停时间,又支持大的内存空间分配。可以很好的解决传统分代方式带来的问题。
##增量收集的演进
增量收集的方式在理论上可以解决传统分代方式带来的问题。增量收集把对堆空间划分成一系列内存块,使用时,先使用其中一部分(不会全部用完),垃圾收集时把之前用掉的部分中的存活对象再放到后面没有用的空间中,这样可以实现一直边使用边收集的效果,避免了传统分代方式整个使用完了再暂停的回收的情况。
当然,传统分代收集方式也提供了并发收集,但是他有一个很致命的地方,就是把整个堆做为一个内存块,这样一方面会造成碎片(无法压缩),另一方面他的每次收集都是对整个堆的收集,无法进行选择,在暂停时间的控制上还是很弱。而增量方式,通过内存空间的分块,恰恰可以解决上面问题。
##Garbage Firest(G1)
这部分的内容主要参考这里,这篇文章算是对G1算法论文的解读。我也没加什么东西了。
目标
从设计目标看G1完全是为了大型应用而准备的。
支持很大的堆
高吞吐量
--支持多CPU和垃圾回收线程
--在主线程暂停的情况下,使用并行收集
--在主线程运行的情况下,使用并发收集
实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收
当然G1要达到实时性的要求,相对传统的分代回收算法,在性能上会有一些损失。
算法详解
G1可谓博采众家之长,力求到达一种完美。他吸取了增量收集优点,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以region为单位;同时,他也吸取了CMS的特点,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程;而且,G1也认同分代垃圾回收的思想,认为不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。为了达到对回收时间的可预计性,G1在扫描了region以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种方式被称为Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。
####回收步骤:
####初始标记(Initial Marking)
G1对于每个region都保存了两个标识用的bitmap,一个为previous marking bitmap,一个为next marking bitmap,bitmap中包含了一个bit的地址信息来指向对象的起始点。
开始Initial Marking之前,首先并发的清空next marking bitmap,然后停止所有应用线程,并扫描标识出每个region中root可直接访问到的对象,将region中top的值放入next top at mark start(TAMS)中,之后恢复所有应用线程。
触发这个步骤执行的条件为:
G1定义了一个JVM Heap大小的百分比的阀值,称为h,另外还有一个H,H的值为(1-h)*Heap Size,目前这个h的值是固定的,后续G1也许会将其改为动态的,根据jvm的运行情况来动态的调整,在分代方式下,G1还定义了一个u以及soft limit,soft limit的值为H-u*Heap Size,当Heap中使用的内存超过了soft limit值时,就会在一次clean up执行完毕后在应用允许的GC暂停时间范围内尽快的执行此步骤;
在pure方式下,G1将marking与clean up组成一个环,以便clean up能充分的使用marking的信息,当clean up开始回收时,首先回收能够带来最多内存空间的regions,当经过多次的clean up,回收到没多少空间的regions时,G1重新初始化一个新的marking与clean up构成的环。
####并发标记(Concurrent Marking)
按照之前Initial Marking扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的以来关系则记录到remembered set logs中,新创建的对象则放入比top值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改top值。
####最终标记暂停(Final Marking Pause)
当应用线程的remembered set logs未满时,是不会放入filled RS buffers中的,在这样的情况下,这些remebered set logs中记录的card的修改就会被更新了,因此需要这一步,这一步要做的就是把应用线程中存在的remembered set logs的内容进行处理,并相应的修改remembered sets,这一步需要暂停应用,并行的运行。
####存活对象计算及清除(Live Data Counting and Cleanup)
值得注意的是,在G1中,并不是说Final Marking Pause执行完了,就肯定执行Cleanup这步的,由于这步需要暂停应用,G1为了能够达到准实时的要求,需要根据用户指定的最大的GC造成的暂停时间来合理的规划什么时候执行Cleanup,另外还有几种情况也是会触发这个步骤的执行的:
G1采用的是复制方法来进行收集,必须保证每次的”to space”的空间都是够的,因此G1采取的策略是当已经使用的内存空间达到了H时,就执行Cleanup这个步骤;
对于full-young和partially-young的分代模式的G1而言,则还有情况会触发Cleanup的执行,full-young模式下,G1根据应用可接受的暂停时间、回收young regions需要消耗的时间来估算出一个yound regions的数量值,当JVM中分配对象的young regions的数量达到此值时,Cleanup就会执行;partially-young模式下,则会尽量频繁的在应用可接受的暂停时间范围内执行Cleanup,并最大限度的去执行non-young regions的Cleanup。
##展望
以后JVM的调优或许跟多需要针对G1算法进行调优了。
##JVM调优工具
####Jconsole,jProfile,VisualVM ####Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。
####JProfiler:商业软件,需要付费。功能强大。
####VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。
##如何调优
观察内存释放情况、集合类检查、对象树
上面这些调优工具都提供了强大的功能,但是总的来说一般分为以下几类功能
堆信息查看
可查看堆空间大小分配(年轻代、年老代、持久代分配)
提供即时的垃圾回收功能
垃圾监控(长时间监控回收情况)
查看堆内类、对象信息查看:数量、类型等
对象引用情况查看
有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:
- 年老代年轻代大小划分是否合理
- 内存泄漏
- 垃圾回收算法设置是否合理
##线程监控
线程信息监控:系统线程数量。
线程状态监控:各个线程都处在什么样的状态下
Dump线程详细信息:查看线程内部运行情况
死锁检查
热点分析
####CPU热点:检查系统哪些方法占用的大量CPU时间 ####内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)
这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。
快照 快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时刻,对象(或类、线程等)的不同,以便快速找到问题
举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下来的了。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后对比两次快照的对象情况。
##内存泄漏检查
内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一下,而线程、热点方面的问题则是具体问题具体分析了。
内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。
内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。
需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。
年老代堆空间被占满 ####异常: java.lang.OutOfMemoryError: Java heap space ####说明:
这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。 如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小时或者几天)
####解决:
这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
持久代被占满 ####异常:java.lang.OutOfMemoryError: PermGen space ####说明: Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。
更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。 解决:
- -XX:MaxPermSize=16m
- 换用JDK。比如JRocket。
堆栈溢出 ####异常:java.lang.StackOverflowError ####说明:这个就不多说了,一般就是递归没返回,或者循环调用造成
线程堆栈满 ####异常:Fatal: Stack size too small ####说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。 ####解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
系统内存被占满 ####异常:java.lang.OutOfMemoryError: unable to create new native thread ####说明: 这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。
分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。 ####解决:
- 重新设计系统减少线程数量。
- 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。
##垃圾回收的悖论
所谓“成也萧何败萧何”。Java的垃圾回收确实带来了很多好处,为开发带来了便利。但是在一些高性能、高并发的情况下,垃圾回收确成为了制约Java应用的瓶颈。目前JDK的垃圾回收算法,始终无法解决垃圾回收时的暂停问题,因为这个暂停严重影响了程序的相应时间,造成拥塞或堆积。这也是后续JDK增加G1算法的一个重要原因。
当然,上面是从技术角度出发解决垃圾回收带来的问题,但是从系统设计方面我们就需要问一下了:
我们需要分配如此大的内存空间给应用吗?
我们是否能够通过有效使用内存而不是通过扩大内存的方式来设计我们的系统呢?
##我们的内存中都放了什么
内存中需要放什么呢?个人认为,内存中需要放的是你的应用需要在不久的将来再次用到到的东西。想想看,如果你在将来不用这些东西,何必放内存呢?放文件、数据库不是更好?这些东西一般包括:
- 系统运行时业务相关的数据。比如web应用中的session、即时消息的session等。这些数据一般在一个用户访问周期或者一个使用过程中都需要存在。
- 缓存。缓存就比较多了,你所要快速访问的都可以放这里面。其实上面的业务数据也可以理解为一种缓存。
- 线程。
因此,我们是不是可以这么认为,如果我们不把业务数据和缓存放在JVM中,或者把他们独立出来,那么Java应用使用时所需的内存将会大大减少,同时垃圾回收时间也会相应减少。
我认为这是可能的。
##解决之道
数据库、文件系统
把所有数据都放入数据库或者文件系统,这是一种最为简单的方式。在这种方式下,Java应用的内存基本上等于处理一次峰值并发请求所需的内存。数据的获取都在每次请求时从数据库和文件系统中获取。也可以理解为,一次业务访问以后,所有对象都可以进行回收了。
这是一种内存使用最有效的方式,但是从应用角度来说,这种方式很低效。
内存-硬盘映射
上面的问题是因为我们使用了文件系统带来了低效。但是如果我们不是读写硬盘,而是写内存的话效率将会提高很多。
数据库和文件系统都是实实在在进行了持久化,但是当我们并不需要这样持久化的时候,我们可以做一些变通——把内存当硬盘使。
内存-硬盘映射很好很强大,既用了缓存又对Java应用的内存使用又没有影响。Java应用还是Java应用,他只知道读写的还是文件,但是实际上是内存。
这种方式兼得的Java应用与缓存两方面的好处。memcached的广泛使用也正是这一类的代表。
同一机器部署多个JVM
这也是一种很好的方式,可以分为纵拆和横拆。纵拆可以理解为把Java应用划分为不同模块,各个模块使用一个独立的Java进程。而横拆则是同样功能的应用部署多个JVM。
通过部署多个JVM,可以把每个JVM的内存控制一个垃圾回收可以忍受的范围内即可。但是这相当于进行了分布式的处理,其额外带来的复杂性也是需要评估的。另外,也有支持分布式的这种JVM可以考虑,不要要钱哦:)
程序控制的对象生命周期 这种方式是理想当中的方式,目前的虚拟机还没有,纯属假设。即:考虑由编程方式配置哪些对象在垃圾收集过程中可以直接跳过,减少垃圾回收线程遍历标记的时间。
这种方式相当于在编程的时候告诉虚拟机某些对象你可以在*时间后在进行收集或者由代码标识可以收集了(类似C、C++),在这之前你即便去遍历他也是没有效果的,他肯定是还在被引用的。
这种方式如果JVM可以实现,个人认为将是一个飞跃,Java即有了垃圾回收的优势,又有了C、C++对内存的可控性。
线程分配
Java的阻塞式的线程模型基本上可以抛弃了,目前成熟的NIO框架也比较多了。阻塞式IO带来的问题是线程数量的线性增长,而NIO则可以转换成为常数线程。因此,对于服务端的应用而言,NIO还是唯一选择。不过,JDK7中为我们带来的AIO是否能让人眼前一亮呢?我们拭目以待。
其他的JDK
本文说的都是Sun的JDK,目前常见的JDK还有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高很多,不过Sun JDK6.0以后提高也很大。而且JRocket在垃圾回收方面,也具有优势,其可设置垃圾回收的最大暂停时间也是很吸引人的。不过,系统Sun的G1实现以后,在这方面会有一个质的飞跃。
能整理出上面一些东西,也是因为站在巨人的肩上。下面是一些参考资料,供大家学习,大家有更好的,可以继续完善:)
· 《深入Java虚拟机》。虽然过去了很多年,但这本书依旧是经典。
这里是本系列的最后一篇了,很高兴大家能够喜欢这系列的文章。期间也提了很多问题,其中有些是我之前没有想到的或者考虑欠妥的,感谢提出这些问题的朋友,我也学到的不少东西。
性能调优监控工具
企业级应用开发中经常会遇到以下问题,可以使用工具对JVM进行监管,以便及时查找问题所在。
内存不足OutOfMemory(大对象没有gc等),内存泄露;
线程死锁,线程数过多;
锁争用(Lock Contention),资源未及时释放(数据库);
Java进程CPU消耗过高.
一、Java自带工具
Java安装目录的bin文件加下有一些工具可以用来监控JVM性能,如jconsole、jvisualvm、jmap、jps、jstack、jhat、jstat等。
1. jconsole
jconsole可以监控Java应用程序(如jar应用、tomcat等),但被监视的应用程序必须和jconsole是用同一个用户运行的。jvisualvm的使用和jconsole类似。
- 本地监控: jconsole pid
- 远程监控: jconsole [ hostname:portNum ]
使用远程监控需要配置jmx代理信息,修改Tomcat的bin目录下的catalina.bat。
set JAVA_OPTS= %JAVA_OPTS% -Djava.rmi.server.hostname=HostIP
set JAVA_OPTS= %JAVA_OPTS% -Dcom.sun.management.jmxremote.port=8888
set JAVA_OPTS= %JAVA_OPTS% -Dcom.sun.management.jmxremote.ssl=false
set JAVA_OPTS= %JAVA_OPTS% -Dcom.sun.management.jmxremote.authenticate=false
连接成功后可以在overview中查看内存、线程、类及CPU使用情况:
2. probe
Lambda Probe 是基于 Web + AJAX 的强大的免费开源工具,可以用来实时管理一个单独的host。LambdaProbe拥有几乎所有Tomcat Manager的功能,还包括应用程序、数据源、发布、日志、线程、集群、系统信息、状态、连接器状态这些功能,并能显示 Server 的详细内存占用状态。
3. jmap
打印java进程的堆内存信息。
- jmap -heap pid 查看heap的概要信息,GC使用的算法、heap的配置及wise heap的使用情况.
- jmap -histo[:live] pid 查看堆内存中的每个类的类名、实例数量、内存占用大小
- jmap -dump:live, format=b, file=fileName pid 将内存使用情况导出到文件中,再用jhat、MAT、VisualVM分析查看,以便查找内存溢出原因
4. jhat
jhat可以对JVM中导出的文件进行分析,使用命令 jhat fileName 即可以在浏览器中输入http://localhost:7000查看内存信息。如果Dump文件太大需要加上-J-Xmx512m指定最大堆内存,如 jhat -J-Xmx512m [-port 9998] tmp.bin
分析内存还可以使用Eclipse的Memory Analyzer,插件地址http://download.eclipse.org/releases/juno,找到General Purpose Tools底下的Memory Analyzer并安装。
5. jstat
查看classloader,compiler,gc相关信息,实时监控资源和性能 。jstat工具特别强大,可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量。
语法结构:
jstat -<option> [-t] [-h<lines>] <pid> [<interval> [<count>]]
Options — 通常使用 -gcutil 查看gc情况
interval – 间隔时间,单位为秒或者毫秒
count — 打印次数,如果缺省则打印无数次
结果说明:
S0 — Heap上的 Survivor space 0 区已使用空间的百分比
S1 — Heap上的 Survivor space 1 区已使用空间的百分比
E — Heap上的 Eden space 区已使用空间的百分比
O — Heap上的 Old space 区已使用空间的百分比
P — Perm space 区已使用空间的百分比
YGC — 从应用程序启动到采样时发生 Young GC 的次数
YGCT— 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
FGC — 从应用程序启动到采样时发生 Full GC 的次数
FGCT— 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT — 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)
- jstat –class<pid> : 显示加载class的数量,及所占空间等信息。
- jstat -compiler <pid>: 显示VM实时编译的数量等信息。
- jstat -gc <pid>: 显示gc的信息,查看gc的次数,及时间。
- jstat -gccapacity <pid>: 显示VM内存中三代(young,old,perm)对象的使用和占用大小
- jstat -gcutil <pid>: 统计gc信息
- jstat -gcnew / gcnewcapacity<pid>: 年轻代对象的信息(及其占用量)。
- jstat -gcold / gcoldcapacity <pid> :old代对象的信息(及其占用量)。
- jstat -gcpermcapacity <pid>: perm对象的信息及其占用量。
- jstat -printcompilation <pid>: 当前VM执行的信息。
6. jstack
jstack用于生成java虚拟机当前时刻的线程快照,主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
语法: jstack [ option ] pid
-F 当 jstack [-l] pid 没有响应的时候强制打印栈信息
-l 长列表. 打印关于锁的附加信息,例如属于java.util.concurrent的ownable synchronizers列表.
jstack应用一: JVM调优之jstack找出最耗cpu的线程并定位代码
jstack应用二: 三个实例演示 Java Thread Dump 日志分析
7. jps
用来查看基于HotSpot JVM里面所有进程的具体状态, 包括进程ID,进程启动的路径等等。
命令格式:jps [options ] [ hostid ]
[options]选项 :
-q: 输出VM标识符pid
-m:输出main method的参数
-l: 输出完全的包名,应用主类名,jar的完全路径名
-v: 输出jvm参数
-V: 输出通过flag文件传递到JVM中的参数
-Joption:传递参数到vm,例如:-J-Xms512m
[hostid]:[protocol:][[//]hostname][:port][/servername]
参考:https://www.cnblogs.com/duanxz/p/3482366.html、https://www.jianshu.com/p/06da907556c9、https://blog.csdn.net/lijingyao8206/article/details/80432402、https://www.cnblogs.com/andy-zhou/p/5327288.html、https://baijiahao.baidu.com/s?id=1639024706303844305&wfr=spider&for=pc、https://www.cnblogs.com/anxiao/p/6796644.html?utm_source=itdadao&utm_medium=referral