方法的局部变量、类的静态变量。
(1)强引用(即最普通的对象引用)对象:在垃圾回收的时候是绝对不会被回收的;(2)软引用(SoftReference)对象:正常状况下垃圾回收是不会回收软引用对象的,可是若是进行垃圾回收以后,发现内存空间仍是不够存放新的对象,内存都快溢出了,此时就会把这些软引用对象给回收掉,哪怕他被变量引用了,可是由于它是软引用,因此仍是要回收;linux
(3)弱引用(WeakReference)对象:弱引用对象跟没引用似的,只要发生垃圾回收就会被回收掉;web
(1)躲过15次年轻代GC以后进入老年代:对象每次在新生代里躲过一次GC被转移到一块Survivor区域中,它的年龄就会增加一岁。默认的设置下,当对象的年龄达到15岁的时候,也就是躲过15次Young GC的时候,它就会被转移到老年代里面去。(2)动态对象年龄判断:年龄1+年龄2+年龄n(年龄从小到大进行累加)的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n及以上的对象都放入老年代。不管15岁的那个规则,仍是动态年龄判断的规则,都是但愿那些多是长期存活的对象,尽早进入老年代。算法
(3)大对象直接进入老年代:之因此这么作,就是为了不新生代里出现那种大对象,而后多次躲过GC,还得把它在两个Survivor区域里来回复制屡次以后才会进入老年代。数据库
(4)Young GC后的对象太多没法放入Survivor区:这个时候就必须得把这些对象直接转移到老年代去。浏览器
若是Young GC后新生代里有大量对象存活下来,确实是本身的Survivor区放不下了,必须转移到老年代去,那么若是老年代里空间也不够放这些对象呢?首先,在执行任何一次Young GC以前,JVM会先检查一下老年代里可用的内存空间(最大可用连续内存空间),是否大于新生代全部对象的总大小。为啥检查这个呢?由于最极端的状况下,可能新生代Young GC事后,全部对象都存活下来了,那岂不是新生代全部对象所有要进入老年代?性能优化
若是发现老年代的可用内存大小是大于新生代全部对象的,此时就能够放心大胆的对新生代发起一次Young GC了,由于即便Young GC以后全部对象都存活,Survivor区放不下了,也能够转移到老年代里去。服务器
可是假如执行Young GC以前,发现老年代的可用内存已经小于新生代的所有对象大小了,那么这个时候是否是有可能在Young GC以后新生代的对象所有存活下来,而后所有须要转移到老年代去,可是老年代空间又不够?理论上,是有这种可能的。并发
若是设置了虚拟机参数HandlePromotionFailure,则会继续尝试进行下一步判断。jvm
下一步判断,就是看看老年代的可用内存大小,是否大于以前每一次Young GC后进入老年代的对象的平均大小。高并发
若是上面那个步骤判断失败了,或者是"-XX:-HandlePromotionFailure"参数没设置,此时就会直接触发一次"Full GC",就是对老年代进行垃圾回收,尽可能腾出来一些内存空间,而后再执行Young GC。若是上面两个步骤都判断成功了,那么就是说能够冒点风险尝试一下Young GC。此时进行Young GC有几种可能结果:
第一种可能,Young GC事后,剩余的存活对象的大小,是小于Survivor区域的大小的,那么此时存活对象进入Survivor区域便可。
第二种可能,Young GC事后,剩余的存活对象的大小,是大于Survivor区域的大小的,可是是小于老年代可用内存大小的,此时就直接进入老年代便可。
第三种可能,很不幸,Young GC事后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,就会发生"Handle Promotion Failure"的状况,这个时候就会触发一次"Full GC"。
Full GC就是对老年代进行垃圾回收,同时也通常会对新生代进行垃圾回收。由于这个时候必须得把老年代里的没人引用的对象给回收掉,而后才可能让Young GC事后剩余的存活对象进入老年代里面。
若是Full GC事后,老年代仍是没有足够的空间存放Young GC事后的剩余存活对象,那么此时就会致使OOM内存溢出了,由于内存实在是不够了,你还要不停的往里面放对象,固然就崩溃了。
CMS执行一次垃圾回收的过程一共分为4个阶段:初始标记、并发标记、从新标记、并发清理。首先,CMS要进行垃圾回收时,会先执行初始标记阶段,这个阶段会让系统的工做线程所有中止,进入"Stop the World"状态。所谓的"初始标记"是说标记出来全部GC Roots直接引用的对象,即方法的局部变量和类的静态变量,而类的实例变量不是GC Roots。初始标记虽然要"Stop the World"暂停一切工做线程,但其实影响不大,由于它的速度很快,仅仅标记GC Roots直接引用的那些对象便可。
接着第二个阶段,是并发标记,这个阶段会让系统线程能够随意建立各类新对象,继续运行。在运行期间可能会建立新的存活对象,也可能会让部分存活对象失去引用,变成垃圾对象。在这个过程当中,垃圾回收线程,会尽量的对已有的对象进行GC Roots追踪。并发标记会对老年代全部对象进行GC Roots追踪,实际上是最耗时的,它须要追踪全部对象是否从根源上被GC Roots引用了,可是这个最耗时的阶段,是跟系统程序并发运行的,因此这个阶段不会对系统运行形成影响的。
接着会进入第三个阶段,从新标记阶段。由于在第二阶段里,你一边标记存活对象和垃圾对象,一边系统在不停运行建立新对象,或者让部分老对象变成垃圾对象,因此第二阶段结束以后,绝对会有不少存活对象和垃圾对象,是以前第二阶段没标记出来的。因此此时进入第三阶段,要继续让系统程序停下来,再次进入"Stop the World"状态。这个从新标记的阶段,是速度很快的,它其实就是对在第二阶段中被系统程序运行所变更过的少数对象进行标记,因此运行速度很快。
接着从新恢复系统程序的运行,进入第四阶段:并发清理。这个阶段就是让系统程序随意运行,而后它来清理掉以前标记为垃圾的对象便可。这个阶段其实也是很耗时的,由于须要进行对象的清理,可是它也是跟系统程序并发运行的,因此其实也不影响系统程序的运行。
CMS的垃圾回收机制已经尽量的对垃圾回收进行了性能优化。由于最耗时的,其实就是对老年代所有对象进行GC Roots追踪,标记出来到底哪些对象能够回收;而后是把各类垃圾对象从内存里清理掉,这两个过程是最耗时的。可是它的第二阶段和第四阶段,都是和系统程序并发执行的,因此基本这两个最耗时的阶段对性能影响不大。只有第一个阶段和第三个阶段是须要"Stop the World"的,可是这两个阶段都是简单的标记而已,速度很是的快,因此基本上对系统运行影响也不大。
CMS虽然能在垃圾回收的同时让系统同时工做,可是在并发标记和并发清理这两个最耗时的阶段,垃圾回收线程和系统工做线程同时工做,会致使有限的CPU资源被垃圾回收线程占用一部分。并发标记的时候,须要对GC Roots进行深度追踪,看老年代全部对象里面到底有多少是存活的,可是由于老年代里存活对象是比较多的,这个过程会追踪大量的对象,因此耗时较高。并发清理,又须要把垃圾对象从各类随机的内存位置清理掉,也是比较耗时的。所以在这两个阶段,CMS的垃圾回收线程是比较耗费CPU资源的。CMS默认启动的垃圾回收线程的数量是(CPU核数+3)/ 4。
在并发清理阶段,CMS只不过是回收以前标记好的垃圾对象,可是这个阶段系统一直在运行,伴随系统运行可能会产生新的垃圾对象,这种垃圾对象是"浮动垃圾"。虽然成了垃圾对象,可是CMS只能回收以前标记出来的垃圾对象,不会回收它们,须要等到下一次GC的时候才会回收它们。因此为了保证在CMS垃圾回收期间,还有必定的内存空间让一些新对象能够进入老年代,须要预留一些内存空间。CMS垃圾回收的触发时机,其中有一个就是当老年代内存占用达到必定比例了,就自动执行GC。"-XX:CMSInitiatingOccupancyFaction"参数能够用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是92%。也就是说,老年代占用了92%空间了,就自动进行CMS垃圾回收,预留8%的空间给并发回收期间,让系统程序把产生的一些新对象放入老年代中。那么若是CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,此时会如何?这个时候,就会发生所谓的Concurrent Mode Failure失败,意思是并发垃圾回收失败了,我一边回收,你一边把对象放入老年代中,内存已经不够用,系统线程运行产生的新对象已经放不下老年代了。
此时就会自动用"Serial Old"垃圾回收器替代CMS,就是直接强行把系统程序"Stop the World",从新进行长时间的GC Roots追踪,标记出来所有垃圾对象,不容许新的对象产生,而后一次性把垃圾对象都回收掉,完事以后才恢复系统线程。因此在生产实践中,这个自动触发CMS垃圾回收的比例须要合理优化一下,避免"Concurrent Mode Failure"问题。
CMS是一款基于"标记-清理"算法实现的老年代垃圾收集器,每次都是标记出来垃圾对象,而后一次性回收掉,这样会致使大量的内存碎片产生。若是内存碎片太多,就会致使后续对象进入老年代找不到可用的连续内存空间,而后不得不触发Full GC。因此CMS不是彻底就仅仅用"标记-清理"算法的,由于太多的内存碎片实际上会致使更加频繁的Full GC。CMS有一个参数是"-XX:+UseCMSCompactAtFullCollection",默认就打开了,意思是在Full GC以后要再次进行"Stop the World",中止工做线程,而后进行内存碎片整理,把存活对象挪到一块儿,空出来大片连续内存空间,避免内存碎片。还有一个参数是"-XX:CMSFullGCsBeforeCompaction",这个意思是执行多少次Full GC以后再执行一次内存碎片整理的工做,默认是0,意思是每次Full GC以后都会进行一次内存碎片整理。
其实缘由很简单,只要分析一下它们俩的执行过程就好了。新生代存活对象是不多的,从GC Roots出发不须要追踪多少对象就好了,因此速度是很快的,而后直接把存活对象放入Survivor中,就一次性直接回收到Eden区和以前使用的Survivor区。可是CMS的Full GC呢?在并发标记阶段,它须要去追踪老年代里全部存活对象,而老年代存活对象不少,这个过程就会很慢;其次并发清理阶段,它不是一次性回收一大片内存,而是找到零零散散在各个地方的垃圾对象,速度也很慢;最后还得执行一次内存碎片整理,把大量的存活对象给挪到一块儿,空出来连续内存空间,这个过程还得"Stop the World",那就更慢了。万一并发清理期间,剩余内存空间不足以存放要进入老年代的对象,引起了"Concurrent Mode Failure"问题,那更是麻烦,还得立马用"Serial Old"垃圾回收器,"Stop the World"以后慢慢从新来一遍回收的过程,这更是耗时。因此综上所述,老年代的垃圾回收,就是一个字:慢。
第一是老年代可用内存小于新生代所有对象的大小,若是没开启空间担保参数,会直接触发Full GC,因此通常空间担保参数都会打开;第二是老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提早Full GC;
第三是新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时若是老年代内存不足会触发Full GC;
第四是老年代可用内存大于历次新生代GC后进入老年代的平均对象大小,可是老年代已经使用的内存空间超过了"-XX:CMSInitiatingOccupancyFaction"参数指定的比例,也会自动触发Full GC。
G1垃圾回收器是能够同时回收新生代和老年代的对象的,不须要两个垃圾回收器配合起来运做,它一我的就能够搞定全部的垃圾回收。G1的一个显著特色,是把Java堆内存拆分红多个大小相等的Region。虽然它也有新生代和老年代的概念,可是只不过是逻辑上的概念,也就是说,新生代可能包含了某些Region,老年代可能包含了某些Region。
G1另一个显著的特色就是可让咱们设置一个垃圾回收的预期停顿时间。不少JVM优化的思路其实就是对内存合理分配,优化一些参数,尽量减小Minor GC和Full GC的次数,减小GC带来的系统停顿,避免影响系统处理请求。可是如今咱们直接能够给G1指定,在一段时间内,垃圾回收致使的系统停顿时间不能超过多久,而后G1全权给你负责,保证达到这个目标,这就至关于咱们能够直接控制垃圾回收对系统性能的影响了。
G1要作到垃圾回收对系统停顿可控,它就必需要追踪每一个Region里的回收价值。啥叫作回收价值呢?G1必须搞清楚每一个Region里的对象有多少是垃圾,若是对这个Region进行垃圾回收,须要耗费多长时间,能够回收掉多少垃圾。总结来讲,G1垃圾回收器的设计思想,主要是把内存拆分为不少个小的Region,而后新生代和老年代各自对应一些Region,追踪每一个Region中能够回收的对象大小和预估时间,回收的时候尽量挑选回收效率最高的Region,尽量保证达到咱们指定的垃圾回收时的系统停顿时间。
在G1中,每个Region是可能属于新生代,可是也可能属于老年代的。刚开始Region可能谁都不属于,而后接着就被分配给了新生代,而后放了不少属于新生代的对象,接着触发了GC回收了这个Region;而后下一次同一个Region可能又被分配给了老年代了,用来放老年代的长生存周期的对象。因此在G1对应的内存模型中,Region随时会属于新生代或老年代,没有所谓的新生代给多少内存,老年代给多少内存这一说。新生代和老年代各自的内存区域是不停在变更的,由G1自动控制。
首先思考两个问题:G1到底划分多少个Region?每一个Region的大小是多大?其实这个默认状况下是自动计算和设置的,咱们能够给整个堆内存设置一个大小,好比说用"-Xms"和"-Xmx"来设置堆内存的大小。而后JVM启动的时候一旦发现你使用的是G1垃圾回收器(可使用"-XX:+UseG1GC"来指定使用G1垃圾回收器),此时会自动用堆大小除以2048,由于JVM最多能够有2048个Region,而后Region的大小必须是2的倍数,好比说1MB、2MB、4MB之类的。好比说堆大小是4G,那么就是4096MB,此时除以2048个Region,每一个Region的大小就是2MB。大概就是这样子来决定Region的数量和大小的,通常保持默认的计算方式就能够。
刚开始的时候,默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,这个是能够经过"-XX:G1NewSizePercent"来设置新生代初始占比的,维持这个默认值便可。由于在系统运行中,JVM其实会不停的给新生代增长更多的Region,可是最多新生代的占比不会超过60%(能够经过"-XX:G1MaxNewSizePercent"设置)。并且一旦Region进行了垃圾回收,此时新生代的Region数量会减小,这些都是动态的。
G1虽然把内存划分红了不少的Region,可是其实仍是有新生代、老年代的区分的,并且新生代里仍是有Eden和Survivor的划分的,它们会各自占据不一样的Region,随着对象不停的在新生代里分配,属于新生代的Region会不断增长,Eden和Survivor对应的Region也会不断增长。
既然G1的新生代也有Eden和Survivor的区分,那么触发垃圾回收的机制都是相似的。随着不停的在新生代的Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%。一旦新生代达到了设定的占据堆内存的最大大小60%,好比都有1200个Region了,里面的Eden可能占据了1000个Region,每一个Survivor是100个Region,并且Eden区还占满了对象,这个时候仍是会触发新生代的GC的,G1会用复制算法来进行垃圾回收,进入一个"Stop the World"状态,而后把Eden对应的Region中的存活对象放入S1对应的Region中,接着回收掉Eden对应的Region中的垃圾对象。可是这个过程跟其余垃圾回收器是有区别的,由于G1是能够设定目标GC停顿时间的,也就是能够指定G1执行GC的时候最多可让系统停顿多长时间,能够经过"-XX:MaxGCPauseMills"参数来设定,默认值是200ms。那么G1就会经过对每一个Region追踪回收它须要多长时间,能够回收多少对象来选择回收一部分的Region,保证GC停顿时间控制在指定范围内,尽量多的回收掉一些对象。
在G1的内存模型下,新生代和老年代各自都会占据必定的Region,老年代也会有本身的Region,按照默认新生代最多只能占据堆内存60%的Region来推算,老年代最多能够占据40%的Region,大概就是800个左右的Region。那么对象何时重新生代进入老年代呢?
(1)对象在新生代躲过了不少次的垃圾回收,达到了必定的年龄了,"-XX:MaxTenuringThreshold"参数能够设置这个年龄,他就会进入老年代;
(2)动态年龄断定规则,若是一旦发现某次新生代GC事后,存活对象超过了Survivor的50%。好比年龄1岁、2岁、3岁、4岁的对象的大小总和超过了Survivor的50%,此时4岁以上的对象所有会进入老年代,这就是动态年龄断定规则。
通过一段时间的新生代使用和垃圾回收以后,总有一些对象会进入老年代中。
G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。在G1中,大对象的断定规则是一个大对象超过了一个Region大小的50%,好比每一个Region是2MB,只要一个大对象超过了1MB,就会被放入大对象专门的Region中,并且一个大对象若是太大,可能会横跨多个Region来存放。
那堆内存里哪些Region用来存放大对象呢?不是说60%的给新生代,40%的给老年代吗,那还有Region给大对象?很简单,在G1里,新生代和老年代的Region是不停的变化的,好比新生代如今占据了1200个Region,可是一次垃圾回收以后,就让里面1000个Region都空了,此时那1000个Region就能够不属于新生代了,里面不少Region能够用来存放大对象。
大对象既然不属于新生代和老年代,那何时会触发垃圾回收呢?也很简单,新生代、老年代在回收的时候,会顺带着大对象Region一块儿回收,因此这就是在G1内存模型下对大对象的分配和回收的策略。
G1有一个参数,是"-XX:InitiatingHeapOccupancyPercent",它的默认值是45%,意思是若是老年代占据了堆内存的45%的Region的时候,就会尝试触发一个新生代+老年代一块儿回收的混合回收阶段。好比堆内存有2048个Region,若是老年代占据了其中45%的Region,也就是接近1000个Region的时候,就会触发一个混合回收。
首先会触发一个"初始标记"的操做,这个过程是须要进入"Stop the World"的,仅仅只是标记一下GC Roots直接能引用的对象,这个过程速度是很快的。先中止系统程序的运行,而后对各个线程栈内存中的局部变量表明的GC Roots,以及方法区中的类静态变量表明的GC Roots,进行扫描,标记出来它们直接引用的那些对象。接着会进入"并发标记"的阶段,这个阶段会容许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪全部的存活对象。这个并发标记阶段是很耗时的,由于要追踪所有的存活对象。可是这个阶段是能够跟系统程序并发运行的,因此对系统程序的影响不太大。并且JVM会把并发标记阶段对对象作出的修改操做记录下来,好比哪一个对象被新建了,哪一个对象失去了引用。
接着是下一个阶段,最终标记阶段,这个阶段会进入"Stop the World",系统程序是禁止运行的,可是会根据并发标记阶段的对象修改操做记录,最终标记一下有哪些存活对象,哪些垃圾对象。
最后一个阶段,就是"混合回收"阶段,这个阶段会计算老年代中每一个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着会中止系统程序,而后尽心尽力尽快进行垃圾回收,此时会选择部分Region进行回收,由于必须让垃圾回收的停顿时间控制在咱们指定的范围内。好比老年代此时有1000个Region都满了,可是由于根据预约目标,本次垃圾回收可能只能停顿200毫秒,那么经过以前的计算得知,可能回收其中800个Region恰好须要200ms,那么就只会回收800个Region,把GC致使的停顿时间控制在咱们指定的范围内。
其实老年代对堆内存占比达到45%时触发的所谓"混合回收"不只仅回收老年代,还会回收新生代和大对象。那么,究竟是回收这些区域中的哪些Region呢?那就要看状况了,由于咱们设定了对GC停顿时间的目标,因此它会重新生代、老年代、大对象里各自挑选一些Region,保证用指定的时间(好比200ms)回收尽量多的垃圾对象。
G1在老年代的Region占据了堆内存的Region的45%以后,会触发一个混合回收的过程,也就是Mixed GC,分为了好几个阶段。其中最后一个阶段是执行混合回收,重新生代和老年代里都回收一些Region,但在最后一个阶段混合回收的时候,它其实会中止全部程序运行的,因此G1是容许执行屡次混合回收的。好比先中止工做,执行一次混合回收回收掉一些Region,接着恢复系统运行,而后再次中止系统运行,再执行一次混合回收回收掉一些Region。有一些参数能够控制其中的一些细节。好比"-XX:G1MixedGCCountTarget"参数,意思是在一次混合回收的过程当中,最后一个阶段执行几回混合回收,默认值是8次。意味着最后一个阶段,先中止系统运行,混合回收一些Region,再恢复系统运行,接着再次禁止系统 运行,混合回收一些Region,反复8次。假设一次混合回收预期要回收掉一共有160个Region,第一次混合回收回收掉了20个Region,接着恢复系统运行一下子,而后再执行一次"混合回收",再次回收掉20个Region,如此反复执行8次混合回收阶段,把预约的160个Region都回收掉,并且还把系统停顿时间控制在指定范围内。
那么为何要反复回收屡次呢?由于中止系统一下子,回收掉一些Region,再让系统运行一下子,而后再次中止系统一下子,再次回收掉一些Region,这样能够尽量让系统不要停顿时间过长,能够在屡次回收的间隙,也运行一下。
还有一个参数,"-XX:G1HeapWasterPercent",默认值是5%,意思是说,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其余Region,而后这个Region中的垃圾对象所有清理掉。这样的话在回收过程当中就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,就会当即中止混合回收,本次垃圾回收结束。从这里也能看出来G1总体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不须要像CMS那样标记-清理以后,再进行内存碎片的整理。
还有一个参数,"-XX:G1MixedGCLiveThresholdPercent",默认值是85%,意思是肯定要回收的Region的时候,必须是存活对象低于85%的Region才能够进行回收。不然要是一个Region的存活对象多余85%,你还要回收它干什么?这个时候要把85%的对象都拷贝到别的Region,成本是很高的。
若是在进行Mixed GC的时候,不管是年轻代仍是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去,此时万一出现拷贝的过程当中发现没有空闲的Region能够承载本身的存活对象了,就会触发一次失败。一旦失败,立马就会切换为中止系统程序,而后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。
当你的系统部署在大内存机器上的时候,好比说你的机器是32核64G的机器,此时你分配给系统的内存有几十个G,新生代的Eden区可能30G~40G的内存。相似Kafka、Elasticsearch之类的大数据相关系统,都是部署在大内存的机器上的,此时若是你的系统负载很是的高,极可能每秒几万的访问请求到Kafka、Elasticsearch上去,致使Eden区的几十个G内存频繁塞满要触发垃圾回收,假设1分钟会塞满一次。而后每次垃圾回收要停顿掉Kafka、Elasticsearch的运行,而后执行垃圾回收大概须要几秒钟,此时你发现,可能每过一分钟,你的系统就要卡顿几秒钟,有的请求一旦卡死几秒钟就会超时报错,致使你的系统频繁出错。
用G1垃圾回收器。G1垃圾回收器能够设置一个指望的每次GC停顿时间,G1基于它的Region内存划分原理,就能够在运行一段时间以后,只针对其中一部分的Region进行垃圾回收,腾出来部份内存,接着还能够继续让系统运行。G1天生就适合这种大内存机器的JVM运行,能够完美解决大内存垃圾回收时间过长的问题。
新生代gc通常问题不会太大,可是真正问题最大的地方,在于频繁触发老年代的GC。对象进入老年代的几个条件:年龄太大了、动态年龄断定规则、新生代gc后存活对象太多没法放入Survivor中。下面从新分析一下这几个条件:第一个,对象年龄太大了,这种对象通常不多,都是系统中确实须要长期存在的核心组件,它们通常不须要被回收掉,因此在新生代熬过默认15次垃圾回收以后就会进入老年代。
第二个,动态年龄断定规则,若是一次新生代gc事后,发现Survivor区域中的几个年龄的对象加起来超过了Survivor区域的50%,好比说年龄1+年龄2+年龄3的对象大小总和,超过了Survivor区域的50%,此时就会把年龄3以上的对象都放入老年代。
第三个,新生代垃圾回收事后,存活对象太多了,没法放入Survivor中,此时直接进入老年代。
其实在上述条件中,第二个和第三个都是很关键的,一般若是你的新生代中的Survivor区域内存太小,就会致使上述第二个和第三个条件频繁发生,而后致使大量对象快速进入老年代,进而频繁触发老年代的gc。
老年代gc一般来讲都很耗费时间,不管是CMS垃圾回收器仍是G1垃圾回收器,由于好比说CMS就要经历初始标记、并发标记、从新标记、并发清理、碎片整理几个环节,过程很是的复杂,G1一样也是如此。一般来讲,老年代gc至少比新生代gc慢10倍以上,好比新生代gc每次耗费200ms,其实对用户影响不大,可是老年代每次gc耗费2s,那可能就会致使老年代gc的时候用户发现页面上卡顿2s,影响就很大了。
因此一旦你由于jvm内存分配不合理,致使频繁进行老年代gc,好比几分钟就有一次老年代gc,每次gc系统都停顿几秒钟,那简直对你的系统就是致命的打击。此时用户会发现页面上或者APP上常常性的出现点击按钮以后卡顿几秒钟。
系统真正最大的问题,就是由于内存分配、参数设置不合理,致使你的对象频繁的进入老年代,而后频繁触发老年代gc,致使系统频繁的每隔几分钟就要卡死几秒钟。这就是所谓JVM的性能问题,也是JVM性能优化须要优化的东西。
(1)发生Young GC以前进行检查,若是"老年代可用的连续内存空间"<"新生代历次Young GC后升入老年代的对象总和的平均大小",说明可能本次Young GC后升入老年代的对象大小,超过了老年代当前可用内存空间。此时必须先触发一次Old GC给老年代腾出更多的空间,而后再执行Young GC。(2)执行Young GC以后有一批对象须要放入老年代,此时老年代没有足够的内存空间存放这些对象了,此时必须当即触发一次Old GC。
(3)老年代内存使用率超过了92%,也要直接触发Old GC,固然这个比例是能够经过参数调整的。
综上所述,总结成一句话,就是老年代空间不够了,无法放入更多对象了,这个时候必须执行Old GC对老年代进行垃圾回收。
Old GC执行的时候通常都会带上一次Young GC,通常Old GC极可能就是在Young GC以前触发或者在Young GC以后触发的,因此天然Old GC通常都会跟一次Young GC连带关联在一块儿。另一点,在不少JVM的实现机制里,其实在老年代达到GC条件的时候,它触发的实际上就是Full GC,这个Full GC会包含Young GC、Old GC和永久代的GC,因此不少时候咱们笼统的归纳为当上述条件知足时就会触发Full GC。
平时咱们对运行中的系统,若是要检查它的JVM总体运行状况,比较经常使用的工具之一就是jstat,它能够轻易的让你看到当前运行中的系统,它的JVM内的Eden、Survivor、老年代的内存使用状况,还有Young GC和Full GC的执行次数以及耗时。经过这些指标咱们能够轻松的分析出当前系统的运行状况,判断当前系统的内存使用压力以及GC压力,还有就是内存分配是否合理。
针对咱们的Java进程执行:jstat -gc PID,就能够看到这个Java进程(其实本质就是一个JVM)的内存和GC状况了,最完整、最经常使用、最实用仍是jstat -gc命令。运行这个命令以后会看到以下列:S0C:这是From Survivor的大小
S1C:这是To Survivor区的大小
S0U:这是From Survivor区当前使用的内存大小
S1U:这是To Survivor区当前使用的内存大小
EC:这是Eden区的大小
EU:这是Eden区当前使用的内存大小
OC:这是老年代的大小
OU:这是老年代当前使用的内存大小
MC:这是方法区(元数据区)的大小
MU:这是方法区(元数据区)的当前使用的内存大小
YGC:这是系统运行迄今为止的Young GC次数
YGCT:这是Young GC的耗时
FGC:这是系统运行迄今为止的Full GC次数
FGCT:这是Full GC的耗时
GCT:这是全部GC的总耗时
咱们分析线上的JVM进程,最想要知道的信息有哪些?包括以下:
新生代对象增加的速率,Young GC的触发频率,Young GC的耗时,每次Young GC后有多少对象是存活下来的,每次Young GC事后有多少对象进入了老年代,老年代对象增加的速率,Full GC的增加频率,Full GC的耗时。
只要知道了这些信息,结合不一样的垃圾回收器优化参数,合理分配内存空间,尽量让对象留在年轻代不进入老年代,避免发生频繁的Full GC。这就是对JVM最好的性能优化了!
咱们平时对jvm第一个要了解的事儿,就是随着系统运行,每秒钟会在年轻代的Eden区分配多少对象。要分析这东西,你只要在线上linux机器上运行以下命令:jstat -gc PID 1000 10。这行命令,它的意思是每隔1秒钟更新出来最新的一行jstat统计信息,一共执行10次jstat统计。经过这个命令,你能够很是灵活的对线上机器经过固定频率输出统计信息,观察每隔一段时间的jvm中的Eden区对象占用变化。举个例子,执行这个命令以后,第一秒先显示出来Eden区使用了200MB内存,第二秒显示出来的那行统计信息里,发现Eden区使用了205MB内存,第三秒显示出来的那行统计信息里,发现Eden区使用了209MB内存,以此类推。此时你能够轻易的推断出来,这个系统大概每秒钟会新增5MB左右的对象。并且这里能够根据本身系统的状况灵活多变的使用,好比你的系统负载很低,不必定每秒都有请求,那么能够把上面的1秒钟调整为1分钟,甚至10分钟,去看大家系统每隔1分钟或者10分钟大概增加多少对象。
还有就是通常系统都有高峰和平常两种状态,好比系统高峰期用的人不少,此时你就应该在系统高峰期去用上述命令看看高峰期的对象增加速率,而后再在非高峰的平常时间段内看看对象的增加速率。按照上述思路,基本上你能够对线上系统的高峰和平常两个时间段内的对象增加速率有很清晰的了解。
接着下一步咱们就想知道大概多久会触发一次Young GC,以及每次Young GC的耗时了。其实多久触发一次Young GC很容易推测出来,由于系统高峰和平常时候的对象增加速率你都知道了,那么很是简单就能够推测出来高峰期多久发生一次Young GC,平常期多久发生一次Young GC。好比你的Eden区有800MB内存,那么发现高峰期每秒新增5MB对象,大概高峰期就是3分钟会触发一次Young GC。平常期每秒新增0.5MB对象,那么平常期大概须要半个小时才会触发一次Young GC。
那么每次Young GC的平均耗时呢?
简单,jstat会告诉你迄今为止系统已经发生了多少次Young GC以及这些Young GC的总耗时。好比系统运行24小时后共发生了260次Young GC,总耗时为20s,那么平均下来每次Young GC大概就耗时几十毫秒的时间,你大概就知道每次Young GC的时候会致使系统停顿几十毫秒。
接着咱们想要知道,每次Young GC后有多少对象会存活下来,以及有多少对象会进入老年代。其实每次Young GC事后有多少对象会存活下来,这个无法直接看出来,可是有办法能够大体推测出来。以前咱们已经推算出来高峰期的时候多久发生一次Young GC,好比3分钟会有一次Young GC,那么此时咱们能够执行下述jstat命令:jstat -gc PID 180000 10。这就至关因而让它每隔三分钟执行一次统计,连续执行10次。此时能够观察一下,每隔三分钟以后发生了一次Young GC,Eden、Survivor、老年代的对象变化。
正常来讲,Eden区确定会在几乎放满以后从新变得里面对象不多,好比800MB的空间就使用了几十MB。Survivor区确定会放入一些存活对象,老年代可能会增加一些对象占用。因此这里的关键,就是观察老年代的对象增加速率。
从一个正常的角度来看,老年代的对象是不太可能不停的快速增加的,由于普通的系统其实没那么多长期存活的对象。若是你发现每次Young GC事后,老年代对象都要增加几十MB,那颇有可能就是你一次Young GC事后存活对象太多了。
存活对象太多,可能致使放入Survivor区域以后触发了动态年龄断定规则进入老年代,也多是Survivor区域放不下了,因此大部分存活对象进入老年代,最多见的就是这两种状况。若是你的老年代每次在Young GC事后就新增几百KB,或者几MB的对象,这个还算情有可原,可是若是老年代对象快速增加,那必定是不正常的。因此经过上述观察策略,你就能够知道每次Young GC事后多少对象是存活的,实际上Survivor区域里的和进入老年代的对象,都是存活的。
你也就能够知道老年代对象的增加速率了,好比每隔3分钟一次Young GC,每次会有50MB对象进入老年代,这就是老年代对象的增加速率,每隔3分钟增加50MB。
只要知道了老年代对象的增加速率,那么Full GC的触发时机就很清晰了,好比老年代总共有800MB的内存,每隔3分钟新增50MB对象,那么大概每小时就会触发一次Full GC。而后能够看到jstat打印出来的系统运行迄今为止的Full GC次数以及总耗时,好比一共执行了10次Full GC,共耗时30s,每次Full GC大概就是须要耗费3s左右。
若是单单只是要了解JVM的运行情况,而后去进行JVM GC优化,一般来讲jstat就彻底够用了。可是有的时候可能咱们会发现JVM新增对象的速度很快,而后就想要去看看,到底什么对象占据了那么多的内存。若是发现有的对象在代码中能够优化一下建立的时机,避免那种对象对内存占用过大,那么也许甚至能够去反过来优化一下代码。固然,其实若是不是出现OOM那种极端状况,也并无那么大的必要去着急优化代码。
jmap -histo PID这个命令会打印当前jvm中的对象对内存占用的状况,让你能够快速了解当前内存里究竟是哪一个对象占用了大量的内存空间。它会按照各类对象占用内存空间的大小降序排列,把占用内存最多的对象放在最上面。
jmap -dump:live,format=b,file=dump.hprof PID这个命令会在当前目录下生成一个dump.hprof文件,这里是二进制的格式,是不能直接打开看的,它会把这一时刻JVM堆内存里全部对象的快照放到文件里去,供你后续去分析。
jhat dump.hprof -port 7000接着就可使用jhat去分析堆快照了,jhat内置了web服务器,它支持你经过浏览器以图形化的方式分析堆转储快照,能够指定本身想要的http端口号,默认是7000端口。接着你就能够在浏览器上访问当前这台机器的7000端口,经过图形化的方式去分析堆内存里的对象分布状况了。
你们平时在开发一个新系统的时候,通常完成开发以后,要经历测试以及上线的过程。在系统开发完毕以后,实际上能够对系统进行预估性的优化。那什么叫作预估性的优化呢?就是本身估算系统每秒大概多少请求,没个请求会建立多少对象,占用多少内存,机器应该选用什么样的配置,年轻代应该给多少内存,Young GC触发的频率,对象进入老年代的速率,老年代应该给多少内存,Full GC触发的频率。这些东西实际上是能够根据你本身写的代码,大体合理的预估一下的。在预估完成以后,就能够结合各类优化思路,先给本身的系统设置一些初始化的JVM参数,好比堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阀值,大对象进入老年代的阀值,等等。
优化思路其实简单来讲就一句话:尽可能让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽可能别让对象进入老年代。尽可能减小Full GC的频率,避免频繁Full GC对JVM性能的影响。
一般一个新系统开发完毕以后,就会通过一连串的测试。从本地的单元测试,到系统集成测试,再到测试环境的功能测试,预发布环境的压力测试,要保证系统的功能所有正常,并且在必定压力下性能、稳定性和并发能力都正常,最后才会部署到生产环境运行。这里很是关键的一个环节就是预发布环境的压力测试,一般在这个环节,会使用一些压力测试工具模拟好比1000个用户同时访问系统形成每秒500个请求的压力,而后看系统可否支撑柱每秒500请求的压力。同时看系统各个接口的响应延时是否在好比200ms以内,也就是接口性能不能太慢,或者是在数据库中模拟出来百万级单表数据,而后看系统是否还能稳定运行。不少开源的压力测试工具均可以轻松模拟出N个用户同时访问你系统的场景,还能给你一份压力测试报告,告诉你系统能够支撑每秒多少请求,包括系统接口的响应延时。在这个环节,一般压测工具会对系统发起持续不断的请求,持续很长时间,好比几个小时,甚至几天时间。
因此此时,你们彻底就能够在这个环节,对测试机器运行的系统,采用jstat工具来分析在模拟真实环境的压力下,JVM的总体运行状态。具体如何使用jstat来进行分析,以前都讲的很详细了,包括如何借助jstat的各类功能分析出来如下JVM的关键运行指标:新生代对象增加的速率,Young GC的触发频率,Young GC的耗时,每次Young GC后有多少是存活下来的,每次Young GC事后有多少对象进入了老年代,老年代对象增加的速率,Full GC的触发频率,Full GC的耗时。
而后根据压测环境中的JVM运行情况,若是发现对象过快进入老年代,多是由于年轻代过小致使频繁Young GC,而后Young GC的时候不少对象仍是存活的,结果Survivor也过小,致使不少对象频繁进入老年代。固然也多是别的什么缘由。此时就须要结合各类优化思路,合理调整新生代、老年代、Eden、Survivor各个区域的内存大小,保证对象尽可能留在年轻代,不要过快进入老年代中。
不要去网上胡乱搜索JVM优化的博客,看到里面人家怎么优化,你就怎么优化,好比不少博客说年轻代和老年代的占比通常是3:8,其实彻底是片面的。每一个系统都是不同的,特色不一样,复杂度不一样。记住一点:真正的优化,必须是你根据本身的系统,实际观察以后,而后合理调整内存分布,根本没什么固定的JVM优化模板。当你对压测环境下的系统优化好JVM参数以后,观察Young GC和Full GC频率都很低,此时就能够部署系统上线了。
当你的系统上线以后,你就须要对线上系统的JVM进行监控,这个监控一般来讲有两种办法。第一种方法会low一点,其实就是天天在高峰期和低峰期都用jstat、jmap、jhat等工具去看看线上系统的JVM运行是否正常,有没有频繁Full GC的问题。若是有就优化,没有的话,平时天天都定时去看看,或者每周都去看看便可。
第二种方法在中大型公司里会多一些,不少中大型公司都会部署专门的监控系统,比较常见的有Zabbix、OpenFalcon、Ganglia,等等。而后你部署的系统均可以把JVM统计项发送到这些监控系统里去。此时你就能够在这些监控系统可视化的界面里,看到你须要的全部指标,包括你的各个内存区域的对象占用变化曲线,直接能够看到Eden区的对象增速,还会告诉你Young GC发生的频率以及耗时,包括老年代的对象增速以及Full GC的频率和耗时。并且这些工具还容许你设置监控。也就是说,你能够指定一个监控规则,好比线上系统的JVM,若是10分钟以内发生5次以上Full GC,就须要发送报警给你。好比发生到你的邮箱、短信或钉钉里,这样你就不用本身天天去看看了。
简单一句话总结:对线上运行的系统,要否则用命令行工具手动监控,发现问题就优化,要否则就是依托公司的监控系统进行自动监控,可视化查看平常系统的运行状态。
正常状况下的系统,会有必定频率的Young GC,通常在几分钟一次Young GC,或者几十分钟一次Young GC,一次耗时在几毫秒到几十毫秒的样子,都是正常的。正常的Full GC频率在几十分钟一次,或者几个小时一次,这个范围内都是正常的,一次耗时应该在几百毫秒的样子。
因此你们若是观察本身线上系统就是这个性能表现,基本上问题都不太大。固然,实际线上系统不少时候会遇到一些JVM性能问题,就是Full GC过于频繁,每次还耗时不少的状况,此时就须要一些优化了。
一旦系统发生频繁Full GC,大概看到的一些表象以下:机器CPU负载太高;
频繁Full GC报警;
系统没法处理请求或者处理过慢;
因此一旦发生上述几个状况,你们第一时间得想到是否是发生了频繁Full GC。
第一种,系统承载高并发请求,或者处理数据量过大,致使Young GC很频繁,并且每次Young GC事后存活对象太多,内存分配不合理,Survivor区域太小,致使对象频繁进入老年代,频繁触发Full GC。第二种,系统一次性加载过多数据进内存,搞出来不少大对象,致使频繁有大对象进入老年代,必然频繁触发Full GC。
第三种,系统发生了内存泄漏,莫名其妙建立了大量的对象,始终没法回收,一直占用在老年代里,必然频繁触发Full GC。
第四种,MetaSpace(永久代)由于加载类过多触发Full GC。
第五种,误调用System.gc()触发Full GC。
其实常见的频繁Full GC缘由无非就上述那几种,因此你们在线上处理Full GC的时候,就从这几个角度入手去分析便可,核心利器就是jstat。
若是jstat分析发现Full GC缘由是第一种,那么就合理分配内存,调大Survivor区域便可。
若是jstat分析发现是第二种或第三种缘由,也就是老年代一直有大量对象没法回收掉,年轻代升入老年代的对象并很少,那么就dump出来内存快照,而后用MAT工具进行分析便可。经过分析,找出来什么对象占用内存过多,而后经过一些对象的引用和线程执行堆栈的分析,找到哪块代码弄出来那么多的对象的,接着优化代码便可。
经过jstat分析发现内存使用很少,还频繁触发Full GC,必然是第四种和第五种,此时对应的进行优化便可。
为了简化JVM的参数设置和优化,建议各个公司和团队leader作一份JVM参数模板出来,设置一些常见参数便可。核心就是一些内存区域的分配、垃圾回收器的指定、CMS性能优化的一些参数(好比压缩、并发,等等),常见的一些参数,包括禁止System.gc(),打印出来gc日志,等等。
由于各类各样的状况,一旦出现了高并发场景,致使ygc后不少请求还没处理完毕,存活对象太多,可能就在Survivor区域放不下了,此时就只能进入到老年代里去了,老年代很快就会放满,一旦老年代放满了就会触发Full GC。咱们假设ygc事后有一批存活对象,Survivor放不下,此时就等着要进入老年代里,然而老年代也满了,那么就得等着老年代进行CMS GC回收掉一些对象,才能让年轻代里存活下来的对象放进去,可是这时不幸的事情发生了,老年代GC事后依然存活下来了不少的对象,没有足够的剩余空间来存放年轻代中的存活对象。这时候会发生什么?那就是内存溢出了!由于老年代都已经塞满了,你还要往里面放东西,并且触发了Full GC回收了老年代仍是没有足够内存空间,你坚持要放?那只能给你一个内存溢出的异常了!JVM跑不动了,崩溃掉。这就是堆内存实在放不下过多对象致使内存溢出的典型范例。
发生堆内存溢出的缘由其实总结下来,就一句话:有限的内存中放了过多的对象,并且大多数都是存活的,此时即便GC事后仍是大部分都存活,因此要继续放入更多对象已经不可能了,只能引起内存溢出问题。
因此通常来讲发生内存溢出有两种主要的场景:
系统承载高并发请求,由于请求量过大,致使大量对象都是存活的,因此要继续放入新的对象实在是不行了,此时就会引起OOM系统崩溃。
系统有内存泄漏的问题,就是莫名其妙弄了不少的对象,结果对象都是存活的,没有及时取消对他们的引用,致使触发GC仍是没法回收,此时只能引起内存溢出,由于内存实在放不下更多对象了。
所以总结起来,通常引起OOM,要否则是系统负载太高,要否则就是有内存泄漏的问题。
这个OOM问题,一旦你的代码写的不太好,或者设计有缺陷,仍是比较容易引起的。