共计 7780 个字符,预计需要花费 20 分钟才能阅读完成。
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 垃圾回收器
特点
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 一起回收
好啦,明天的文章就到这里,心愿能帮忙到屏幕前迷茫的你们!