本文主要是基于Sun JDK 1.6 Garbage Collector(作者:毕玄)的整理与总结,原文请读者在网上搜索。 1、Java虚拟机运行时的数据区 2、常用的内存区域调节参数 -Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制 -Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 -Xmn:新生代的内存空间大小,注意:此处的大小是(eden+ 2 survivor space)。与jmap -heap中显示的New gen是不同的。整个堆大小=新生代大小 + 老生代大小 + 永久代大小。 -XX:SurvivorRatio:新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。 -Xss:每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。应根据应用的线程所需内存大小进行适当调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。一般小的应用, 如果栈不是很深, 应该是128k够用的,大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了。 -XX:PermSize:设置永久代(perm gen)初始值。默认值为物理内存的1/64。 -XX:MaxPermSize:设置持久代最大值。物理内存的1/4。 3、内存分配方法 1)堆上分配 2)栈上分配 3)堆外分配(DirectByteBuffer或直接使用Unsafe.allocateMemory,但不推荐这种方式) 4、监控方法 1)系统程序运行时可通过jstat –gcutil来查看堆中各个内存区域的变化以及GC的工作状态; 5)断代法可用GC汇总 一、新生代可用GC 1)串行GC(Serial Copying):client模式下默认GC方式,也可通过-XX:+UseSerialGC来强制指定;默认情况下 eden、s0、s1的大小通过-XX:SurvivorRatio来控制,默认为8,含义 默认情况下,仅在TLAB或eden上分配,只有两种情况下会在老生代分配: 默认情况下,触发Minor GC时: 默认情况下,新生代对象晋升到老生代的规则: 1、经历多次minor gc仍存活的对象,可通过以下参数来控制:以MaxTenuringThreshold值为准,默认为15。 2)并行GC(ParNew):CMS GC时默认采用,也可采用-XX:+UseParNewGC强制指定;垃圾回收的时候采用多线程的方式。 3)并行回收GC(Parallel Scavenge):server模式下默认的GC方式,也可采用-XX:+UseParallelGC强制指定;eden、s0、s1的大小可通过-XX:SurvivorRatio来控制,但默认情况下 默认情况下,当TLAB、eden上分配都失败时,判断需要分配的内存大小是否 >= eden space的一半大小,如是就直接在老生代上分配; 默认情况下的垃圾回收规则: 1、在回收前PS GC会先检测之前每次PS GC时,晋升到老生代的平均大小是否大于老生代的剩余空间,如大于则直接触发full GC; 默认情况下的新生代对象晋升到老生代的规则: 在回收后,如UseAdaptiveSizePolicy,PS GC会根据运行状态动态调整eden、to以及TenuringThreshold的大小。如果不希望动态调整可设置-XX:-UseAdaptiveSizePolicy。如希望跟踪每次的变化情况,可在启劢参数上增加: PrintAdaptiveSizePolicy。 二、老生代可用GC 1、串行GC(Serial Copying):client方式下默认GC方式,可通过-XX:+UseSerialGC强制指定。 触发机制汇总: 2、并行回收GC(Parallel Scavenge): server模式下默认GC方式,可通过-XX:+UseParallelGC强制指定; 并行的线程数为当cpu core<=8 ? cpu core : 3+(cpu core*5)/8或通过-XX:ParallelGCThreads=x来强制指定。如ScavengeBeforeFullGC为true(默认值),则先执行minor GC。 3、并行Compacting:可通过-XX:+UseParallelOldGC强制指定。 4、并发CMS:可通过-XX:+UseConcMarkSweepGC来强制指定。并发的线程数默认为:( 并行GC线程数+3)/4,也可通过ParallelCMSThreads指定。 触发机制: Hotspot V 1.6中默认为65%,可通过PrintCMSInitiationStatistics(此参数在V 1.5中不能用)来查看这个值到底是多少;可通过CMSInitiatingOccupancyFraction来强制指定,默认值并不是赋值在了这个值上,是根据如下公式计算出来的: ((100 - MinHeapFreeRatio) +(double)(CMSTriggerRatio * MinHeapFreeRatio) / 100.0)/ 100.0; 其中,MinHeapFreeRatio默认值: 40 CMSTriggerRatio默认值: 80。 2、当perm gen采用CMS收集且空间使用到一定比率时触发; perm gen采用CMS收集需设置:-XX:+CMSClassUnloadingEnabled Hotspot V 1.6中默认为65%;可通过CMSInitiatingPermOccupancyFraction来强制指定,同样,它是根据如下公式计算出来的:((100 - MinHeapFreeRatio) +(double)(CMSTriggerPermRatio* MinHeapFreeRatio) / 100.0)/ 100.0; 其中,MinHeapFreeRatio默认值: 40 CMSTriggerPermRatio默认值: 80。 3、Hotspot根据成本计算决定是否需要执行CMS GC;可通过-XX:+UseCMSInitiatingOccupancyOnly来去掉这个动态执行的策略。 6、GC组合 1)默认GC组合 2)可选的GC组合 7、GC监测 1)jstat –gcutil [pid] [intervel] [count] 4)查看内存的消耗状况 (1)长期消耗,可以直接dump,然后MAT(内存分析工具)查看即可 (2)短期消耗,图形界面情况下,可使用jvisualvm的memory profiler或jprofiler。 8、系统调优方法 步骤:1、评估现状 2、设定目标 3、尝试调优 4、衡量调优 5、细微调整 设定目标: 1)降低Full GC的执行频率? 衡量调优: 1、衡量工具 2、应收集的信息 QPS每秒查询率:是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。在因特网上,作为域名服务器的机器性能经常用每秒查询率来衡量。对应fetches/sec,即每秒的响应请求数,也即是最大吞吐能力。 尝试调优: 注意Java RMI的定时GC触发机制,可通过:-XX:+DisableExplicitGC来禁止或通过 -Dsun.rmi.dgc.server.gcInterval=3600000来控制触发的时间。 1)降低Full GC执行频率 – 通常瓶颈 (1)Minor GC后总是有对象不断的进入老生代,导致老生代不断的满 (2)老生代的内存占用一直偏高 (3)Minor GC后总是有对象不断的进入老生代 (4)降低单次Full GC的执行时间 (5)降低Minor GC执行频率 (6)降低Minor GC执行时间 细微调整: 首先需要了解以下情况: ① 当响应速度下降到多少或请求量上涨到多少时,系统会宕掉? ② 参数调整后系统多久会执行一次Minor GC,多久会执行一次Full GC,高峰期会如何? 需要计算的量: ①每次请求平均需要分配多少内存?系统的平均响应时间是多少呢?请求量是多少、多常时间执行一次Minor GC、Full GC? ②现有参数下,应该是多久一次Minor GC、Full GC,对比真实状况,做一定的调整; 必杀技:提升响应速度、降低每次请求分配的内存? 9、系统调优举例 现象:1、系统响应速度大概为100ms;2、当系统QPS增长到40时,机器每隔5秒就执行一次minor gc,每隔3分钟就执行一次full gc,并且很快就一直full GC了;4、每次Full gc后旧生代大概会消耗400M,有点多了。 解决方案:解决Full GC次数过多的问题 (1)降低响应时间或请求次数,这个需要重构,比较麻烦;——这个是终极方法,往往能够顺利的解决问题,因为大部分的问题均是由程序自身造成的。 (2)减少老生代内存的消耗,比较靠谱;——可以通过分析Dump文件(jmap dump),并利用MAT查找内存消耗的原因,从而发现程序中造成老生代内存消耗的原因。 (3)减少每次请求的内存的消耗,貌似比较靠谱;——这个是海市蜃楼,没有太好的办法。 (4)降低GC造成的应用暂停的时间——可以采用CMS GS垃圾回收器。参数设置如下: -Xms1536m -Xmx1536m -Xmn700m -XX:SurvivorRatio=7 -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSMaxAbortablePrecleanTime=1000 -XX:+CMSClassUnloadingEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:+DisableExplicitGC (5)减少每次minor gc晋升到old的对象。可选方法:1) 调大新生代。2)调大Survivor。3)调大TenuringThreshold。 调大Survivor:当前采用PS GC,Survivor space会被动态调整。由于调整幅度很小,导致了经常有对象直接转移到了老生代;于是禁止Survivor区的动态调整了,-XX:-UseAdaptiveSizePolicy,并计算Survivor Space需要的大小,于是继续观察,并做微调…。最终将Full GC推迟到2小时1次。 10、垃圾回收的实现原理 内存回收的实现方法:1)引用计数:不适合复杂对象的引用关系,尤其是循环依赖的场景。2)有向图Tracing:适合于复杂对象的引用关系场景,Hotspot采用这种。常用算法:Copying、Mark-Sweep、Mark-Compact。 Hotspot从root set开始扫描有引用的对象并对Reference类型的对象进行特殊处理。 另外:minor GC只扫描新生代,当老生代的对象引用了新生代的对象时,会采用如下的处理方式:在给对象赋引用时,会经过一个write barrier的过程,以便检查是否有老生代引用新生代对象的情况,如有则记录到remember set中。并在minor gc时,remember set指向的新生代对象也作为root set。 新生代串行GC(Serial Copying): 新生代串行GC(Serial Copying)完整内存的分配策略: 1)首先在TLAB(本地线程分配缓冲区)上尝试分配; 新生代串行GC(Serial Copying)完整内存回收策略 新生代可用GC-PS 完整内存分配策略 最悲惨的情况,分配触发多次PS GC和多次Full GC,直到OOM。 完整内存回收策略 老生代并行CMS GC: 优缺点: 1) 大部分时候和应用并发进行,因此只会造成很短的暂停时间; 11、TLAB的解释 堆内的对象数据是各个线程所共享的,所以当在堆内创建新的对象时,就需要进行锁操作。锁操作是比较耗时,因此JVM为每个线在堆上分配了一块“自留地”——TLAB(全称是Thread Local Allocation Buffer),位于堆内存的新生代,也就是Eden区。每个线程在创建新的对象时,会首先尝试在自己的TLAB里进行分配,如果成功就返回,失败了再到共享的Eden区里去申请空间。在线程自己的TLAB区域创建对象失败一般有两个原因:一是对象太大,二是自己的TLAB区剩余空间不够。通常默认的TLAB区域大小是Eden区域的1%,当然也可以手工进行调整,对应的JVM参数是-XX:TLABWasteTargetPercent。 参考文献: 1、Sun JDK 1.6 GC(Garbage Collector) 作者:毕玄 |
|