重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
本篇内容介绍了“JVM有哪些常用的功能”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
创新互联公司是一家专注于网站建设、网站设计与策划设计,浦城网站建设哪家好?创新互联公司做网站,专注于网站建设10年,网设计领域的专业建站公司;建站业务涵盖:浦城等地区。浦城做网站价格咨询:18980820575
Hello,今天给各位童鞋们分享JVM,赶紧拿出小本子记下来吧!
新生代GC场景
在jvm内存模型中,新生代的内存分为为Eden和两个Survivor
在系统不停的运行过程中,Eden区会被塞满,这个时候就会触发Minor GC,进行垃圾回收有专门的垃圾回收线程,不同的内存区域会有不同的垃圾回收器,相当于垃圾回收线程和垃圾回收器配合起来,使用自己的垃圾回收算法,对指定的内存区域进行垃圾回收,如下图所示:
针对新生代采用ParNew垃圾回收器来进行回收,然后ParNew垃圾回收器针对新生代采用的就是复制算法来垃圾回收
这个时候垃圾回收器,就会把Eden区中的存活对象都标记出来,然后全部转移到Survivor1去,接着一次性清空掉Eden中的垃圾对象
当Eden再次塞满的时候,就又要触发Minor GC了,此时已然是垃圾回收线程运行垃圾回收器中的算法逻辑,也就是采用复制算法逻辑,去标记出来Eden和Survivor1中的存活对象,然后一次性把存活对象转移到Survivor2中去,接着把Eden和Survivor1中的垃圾对象都回收掉
在发生GC的时候,我们写好的JAVA系统在运行期间还能不能继续在新生代里创建新的对象?
假如在GC期间,允许创建新的对象,那么垃圾回收器在把Eden和Survivor1里的存活对象标记转移到Survivor2去,然后还在想办法把Eden和Survivor1里的垃圾对象都清理掉,结果这个时候系统程序还在不停的在Eden里创建新的对象,那么这些新对象很快就成了垃圾对象,有的还有人引用是存活对象,这对垃圾回收器完全乱套,一边回收一边还在创建新的对象。
Stop the World
JVM最大的痛点,就是垃圾回收的过程,在垃圾回收的时候,尽可能让垃圾回收器专心的工作,不能随便让我们的Java应用继续创建对象,所以此时JVM会在后台进入“入“Stop the World”状态,也就是说会直接停止我们的Java系统的所有工作线程,让我们的代码不再运行
这样的话,就可以让我们的系统暂停运行,然后不再创建新的对象,同时让垃圾回收线程尽快完成垃圾回收的工作,就是标记和转移Eden以及Survivor1的存活对象到Survivor2中去,然后尽快一次性回收掉Eden和Survivor1中的垃圾对象,等垃圾回收完毕后,继续恢复我们写的Java系统的工作线程,然后继续运行我们的代码逻辑,继续在Eden区创建新的对象
Stop the World造成的系统停顿
在运行GC的时候会无法创建新的对象,则会造车系统停顿,如果Minor GC要运行50ms,则可能会导致我们的系统在50ms内不能接受任何请求,在这50ms期间用户发起的所有请求都会出现短暂的卡顿,因为系统的工作线程不在运行,不能处理请求
可能由于内存分配不合理,导致对象频繁进入老年代,平均七八分钟一次Full GC,而Full GC比较慢,一次回收可能需要几秒甚至几十秒,所以一旦频繁的Full GC,就会造成系统每隔几分钟卡死个几十秒,让用户体验极差
所以说,无论是新生代GC还是老年代GC,都尽量不要让频率过高,也避免持续时间过长,避免影响系统正常运行,这也是使用JVM过程中一个最需要优化的地方,也是最大的一个痛点。
不同的垃圾回收器的不同的影响
Serial垃圾回收器(新生代)
用一个线程进行垃圾回收,然后此时暂停系统工作线程
一般我们在服务器程序中很少用这种方式
ParNew垃圾回收器(新生代)
常用的新生代垃圾回收器
针对服务器一般都是多核CPU做了优化,他是支持多线程个垃圾回收的,可以大幅度提升回收的性能,缩短回收的时间
Serial和Serial Old垃圾回收器
分别用来回收新生代和老年代的垃圾对象
工作原理就是单线程运行,垃圾回收的时候会停止我们自己写的系统的其他工作线程,让我们系统直接卡死不动,然后让他们垃圾回收,这个现在一般写后台Java系统几乎不用。
ParNew和CMS垃圾回收器
ParNew现在一般都是用在新生代的垃圾回收器,采用的就是复制算法来垃圾回收
CMS是用在老年代的垃圾回收器
都是多线程并发的机制,性能更好,现在一般是线上生产系统的标配组合
ParNew
理论
没有最新的G1垃圾回收器的话,通常大家线上系统都是ParNew垃圾回收器作为新生代的垃圾回收器当然现在即使有了G1,其实很多线上系统还是用的ParNew
通常运行在服务器上Java系统,都可以充分利用服务器的多核CPU优势,如果对新生代回收的时候,仅仅使用单线程进行垃圾回收,会导致浪费CPU的资源
新生代的ParNew垃圾回收器主打的就是多线程垃圾回收机制,另外一种Serial垃圾回收器主打的是单线程垃圾回收,他们俩都是回收新生代的,唯一的区别就是单线程和多线程的区别,但是垃圾回收算法是完全一样
ParNew垃圾回收器如果一旦在合适的时机执行Minor GC的时候,就会把系统程序的工作线程全部停掉,禁止程序继续运行创建新的对象,然后自己就用多个垃圾回收线程去进行垃圾回收,回收的机制和算法都是一样的
参数设置
部署到Tomcat时可以在Tomcat的catalina.sh中设置Tomcat的JVM参数,使用Spring Boot也可以在启动时指定JVM参数。
指定使用ParNew垃圾回收器
使用“-XX:+UseParNewGC”选项,只要加入这个选项,JVM启动之后对新生代进行垃圾回收的,就是ParNew垃圾回收器
ParNew垃圾回收器默认情况下的线程数量
一旦我们指定了使用ParNew垃圾回收器之后,他默认给自己设置的垃圾回收线程的数量就是跟CPU的核数是一样的
如果你一定要自己调节ParNew的垃圾回收线程数量,也是可以的,使用“-XX:ParallelGCThreads”参数即可,通过他可以设置线程的数量
CMS
理论
老年代选择的垃圾回收器是CMS,他采用的是标记清理算法
标记清理算法:先通过GC Roots的方法,看各个对象是否被GC Roots给引用,如果是的话,那就是存活对象,否则就是垃圾对象。先将垃圾对象都标记出来,然后一次性把垃圾对象都回收掉,这种方法最大问题:就是会造成很多内存碎片,这种内存碎片不大不小,可能放不下任何一个对象,则会造成内存浪费
CMS的STW(Stop the World)问题:如果停止一切工作线程,然后慢慢的执行“标记-清理”算法,会导致系统卡死时间过长,很多响应无法处理。所以CMS垃圾回收器采取的是:垃圾回收线程和系统工作线程尽量同时执行的模式来处理
如何实现系统一边工作的同时进行垃圾回收?
CMS在执行一次垃圾回收的过程共分为4个阶段:
初始标记
并发标记
重新标记
并发清理
1、初始标记
CMS在进行垃圾回收时,会先执行初始标记阶段。这个阶段会让系统的工作线程全部停止,进入“Stop The World”状态,初始标记执行STW影响不大,因为他的速度比较快,只是标记出GC Roots直接应用的对象
2、并发标记
这个阶段会让系统可以随意创建各种新对象,继续运行,在运行期间可能会创建新的存活对象,也可能会让部分存活对象失去引用,变成垃圾对象。在这个过程中,垃圾回收线程,会尽可能的对已有的对象进行GC Roots追踪,但是这个过程中,在进行并发标记的时候,系统程序会不停的工作,他可能会各种创建出来新的对象,部分对象可能成为垃圾
这个阶段就是对老年代所有对象进行GC Roots追踪,其实是最耗时的,需要追踪所有对象是否从根源上被GC Roots引用了,但是这个最耗时的阶段,是跟系统程序并发运行的,所以其实这个阶段不会对系统运行造成影响。
3、重新标记
因为第二阶段里,你一边标记存活对象和垃圾对象,一边系统在不停运行创建新对象,让老对象变成垃圾,所以第二阶段结束之后,绝对会有很多存活对象和垃圾对象,是之前第二阶段没标记出来的。所以此时进入第三阶段,要继续让系统程序停下来,**再次进入“Stop the World”阶段。**然后重新标记下在第二阶段里新创建的一些对象,还有一些已有对象可能失去引用变成垃圾的情况
这个重新标记的阶段,是速度很快的,他其实就是对在第二阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快,接着重新恢复系统程序的运行。
4、并发清理
让系统程序随意运行,然后他来清理掉之前标记为垃圾的对象,这个阶段比较耗时,需要进行对象的清理,但是他是跟着系统程序并发运行的,所以也不影响系统程序的执行
CMS垃圾回收器问题
1、并发回收导致CPU资源紧张
CMS垃圾回收器有一个最大的问题,虽然能在垃圾回收的同时让系统同时工作,在并发标记和并发清理两个最耗时的阶段,垃圾回收线程和系统工作线程同时工作,会导致有限的CPU资源被垃圾回收线程占用了一部分
并发标记的时候,需要对GC Roots进行深度追踪,看所有对象里面到底有多少人是存活的但是因为老年代里存活对象是比较多的,这个过程会追踪大量的对象,所以耗时较高。并发清理,又需要把垃圾对象从各种随机的内存位置清理掉,也是比较耗时的
所以在这两个阶段,CMS的垃圾回收线程是比较耗费CPU资源的。CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4
2、Concurrent Mode Failure问题
在并发清理阶段,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追踪,标记出来全部垃圾对象,不允许新的对象产生,然后一次性把垃圾对象都回收掉,完事后再恢复系统线程
3、内存碎片问题
老年代的CMS采用“标记-清理”算法,每次都是标记出来垃圾对象,然后一次性回收掉,这样会导致大量的内存碎片产生。如果内存碎片太多,会导致后续对象进入老年代找不到可用的连续内存空间了,然后触发Full GC
所以CMS不是完全就仅仅用“标记-清理”算法的,因为太多的内存碎片实际上会导致更加频繁的Full GC
CMS有一个参数是“-XX:+UseCMSCompactAtFullCollection”,默认就打开,意思是在Full GC之后要再次进行“Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片
还有一个参数是“-XX:CMSFullGCsBeforeCompaction”,这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0,意思就是每次Full GC之后都会进行一次内存整理
触发老年代GC的时机
1、老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开;
2、老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
3、新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足;
4、-XX:CMSInitiatingOccupancyFaction:老年代的已用内存大于设定的阀值,就会触发Full GC;
5、显示调用System.gc
ParNew + CMS带给我们的痛点是什么
Stop the World,这个是大家最痛的一个点
无论是新生代垃圾回收,还是老年代垃圾回收,都会或多或少产生“Stop the World”现象,对系统的运行是有一定影响的。所以其实之后对垃圾回收器的优化,都是朝着减少“Stop the World”的目标去做的。
在这个基础之上,G1垃圾回收器就应运而生了,他可以提供比“ParNew + CMS”组合更好的垃圾回收的性能
特点
G1垃圾回收器是可以同时回收新生代和老年代的对象的,不需要两个垃圾回收器配合起来运作,他一个人就可以搞定所有的垃圾回收。
1、把Java堆内存拆分为多个大小相等的Region
G1也会有新生代和老年代的概念,但是只不过是**逻辑上的概念**
也就是说新生代可能包含了某些Region,老年代可能包含了某些Region。
2、可以设置一个垃圾回收的预期停顿时间
也就是说比如我们可以指定:希望G1在垃圾回收的时候,可以保证,在1小时内由G1垃圾回收导致的“Stop the World”时间,也就是系统停顿的时间,不能超过1分钟,这样相当于我们就可以直接控制垃圾回收对系统性能的影响
3、Region可能属于新生代也可能属于老年代
刚开始Region可能谁都不属于,然后接着就分配给了新生代,然后放了很多属于新生代的对象,接着就触发了垃圾回收这个Region,下一次同一个Region可能又被分配了老年代了,用来放老年代的长生存周期的对象,所以其实在G1对应的内存模型中,Region随时会属于新生代也会属于老年代,所以没有所谓新生代给多少内存,老年代给多少内存这一说
实际上新生代和老年代各自的内存区域是不停的变动的,由G1自动控制
G1是如何做到对垃圾回收导致的系统停顿可控的?
其实G1如果要做到这一点,他就必须要追踪每个Region里的回收价值,啥叫做回收价值呢?
他必须搞清楚每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾?G1通过追踪发现,1个Region中的垃圾对象有10MB,回收他们需要耗费1秒钟,另外一个Region中的垃圾对象有20MB,回收他们需要耗费200毫秒。
然后在垃圾回收的时候,G1会发现在最近一个时间段内,比如1小时内,垃圾回收已经导致了几百毫秒的系统停顿了,现在又要执行一次垃圾回收,那么必须是回收上图中那个只需要200ms就能回收掉20MB垃圾的Region;于是G1触发一次垃圾回收,虽然可能导致系统停顿了200ms,但是一下子回收了更多的垃圾,就是20MB的垃圾
所以简单来说,G1可以做到让你来设定垃圾回收对系统的影响,他自己通过把内存拆分为大量小Region,以及追踪每个Region中可以回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象。这就是G1的核心设计思路
如何设定G1对应的内存大小?
G1对应的是一大堆的Region内存区域,每个Region的大小是一致的,默认情况下自动计算和设置的,可以给整个堆内存设置一个大小,比如说用“-Xms”和“-Xmx”来设置堆内存的大小
JVM启动的时候,发现使用的是G1垃圾回收器(通过:用“-XX:+UseG1GC”来指定使用G1垃圾回收器),此时会自动用堆大小除以2048,JVM最多可以有2048个Region,然后Region的大小必须是2的倍数,比如说1MB、2MB、4MB之类,可以通过手动方式来指定,则是**“-XX:G1HeapRegionSize“**
刚开始的时候,默认新生代对堆内存的占比是5%,这个是可以通过“-XX:G1NewSizePercent”来设置新生代初始占比的,其实维持这个默认值即可
在系统运行中,JVM其实会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”,而且一旦Region进行了垃圾回收,此时新生代的Region数量还会减少,这些其实都是动态
新生代还有Eden和Survivor的概念?
G1中虽然把内存划分为很多的 Region,但是其实还是有新生代、老年代的区分,而且新生代里还是有Eden和Survivor的划分
通过参数,“-XX:SurvivorRatio=8”,可以设置新生代中80%的Region属于Eden,两个Survivor各自占10%
随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加
G1的新生代垃圾回收触发机制?
既然G1的新生代也有Eden和Survivor的区分,那么触发垃圾回收的机制都是类似的,随着不停的在新生代的Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%。
一旦新生代达到了设定的占据堆内存的最大大小60%,这个时候还是会触发新生代的GC,G1就会用之前说过的复制算法来进行垃圾回收,进入一个“Stop the World”状态,然后把Eden对应的Region中的存活对象放入S1对应的Region中,接着回收掉Eden对应的Region中的垃圾对象,但是这个过程跟之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms。
那么G1就会通过之前说的,对每个Region追踪回收他需要多少时间,可以回收多少对象来选择回收一部分的Region,保证GC停顿时间控制在指定范围内,尽可能多的回收掉一些对象。
对象什么时候进入老年代?
可以说跟之前几乎是一样的,还是这么几个条件:
1、对象在新生代躲过了很多次的垃圾回收,达到了一定的年龄了,“-XX:MaxTenuringThreshold”参数可以设置这个年龄,就会进入老年代
2、 动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%
大对象Region
在之前,大对象是直接进入老年代,在G1的内存模型中,G1提供了专门的Region来存放大对象,而不是让大对象直接进入老年的Region中。
在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,如果每个Region是2MB,只要一个大对象超过了1MB,就会被放入大对象专门的Region中,而且一个大对象如果太大,可能会横跨多个Region来存放在新生代、老年代回收的时候,会顺带带着大对象Region一起回收
“JVM有哪些常用的功能”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注创新互联网站,小编将为大家输出更多高质量的实用文章!