大纲
1.JVM GC导致系统突然卡死无法访问
2.什么是Young GC什么是Full GC
3.Young GC、Old GC和Full GC的发生情况
4.频繁YGC的案例(G1解决大内存YGC过慢)
5.频繁FGC的案例(YGC存活对象S区放不下)
6.问题汇总
1.JVM GC导致系统突然卡死无法访问
(1)基于JVM运行的系统最怕什么
(2)新生代GC多久一次才对系统影响不大
(3)什么时候新生代GC对系统影响很大
(4)如何处理大内存机器新生代GC过慢
(5)要命的频繁老年代GC问题
(6)JVM性能优化到底在优化什么
(1)基于JVM运行的系统最怕什么
一.JVM运行时最核心的内存区域其实是堆内存
在堆内存中会放业务系统创建出来的各种对象,而且通常都会将堆内存划分为新生代和老年代两个内存区域。对象一般都会优先放在新生代中,如下图示:
二.随着系统不断地运行,会有越来越多的对象放入新生代中
然后新生代快占满,放不下更多对象时,就要清理新生代的垃圾对象了。所谓的垃圾对象就是没有被GC Roots引用的对象,所谓的GC Roots就是类的静态变量、方法的局部变量等。
代码中最常创建对象的地方,就是在方法里。而一个方法一旦执行完毕,那么该方法里的局部变量就没了。之前在方法里创建出来的对象就会成为垃圾对象,因为没有再被引用了。在新生代中,99%的对象都是这种没有再被引用的垃圾对象。
三.在新生代快要占满时,就会触发新生代GC,对新生代进行垃圾回收
新生代会通过复制算法进行回收。通常来说新生代会有一块Eden区用来创建对象,默认占据80%的内存。还有两块Survivor区用来放垃圾回收后存活的对象,分别占10%的内存。如下图示:
需要注意的是:对新生代进行垃圾回收时,需要停止系统程序的运行,不要让系统程序执行任何代码逻辑,也就是所谓的Stop the World。此时只允许垃圾回收器的多个垃圾回收线程去进行垃圾回收,如下图示:
新生代通过复制算法进行回收时:
首先会对所有的GC Roots进行追踪,标记出所有被GC Roots直接或者间接引用的对象。被标记成GC Roots引用的对象就是存活对象,比如上图有个类的静态变量就引用了一个对象,那个对象就是存活对象。然后就会把存活对象都转移到一块Survivor区域里去,比如上图中,就把存活的对象转移到一块Survivor区里了。接着就会直接把Eden区里的对象全部回收掉,释放其内存空间,然后就可以恢复系统程序的运行。如下图示:
四.由此可见,这里有一个很大的问题------停止系统程序的问题
当新生代被占满,需要进行垃圾回收时,就要停止系统程序的运行。这就是基于JVM运行的系统最害怕的问题:系统卡顿问题。假设一次新生代垃圾回收要20ms,那么在这20ms内系统是无法工作的。此时用户对系统发送的请求,在这20ms内是无法处理的,会卡住20ms。
(2)新生代GC多久一次才对系统影响不大
新生代GC对系统的性能影响到底大不大?通常来说是不大的,而且新生代GC也几乎没什么好调优的。因为它的运行逻辑非常简单,就是Eden区一旦满了就会触发一次GC。
一.如果非要对新生代GC进行调优,只要给系统分配足够内存即可
所以核心点还是在于堆内存的分配、新生代内存的分配。如果内存足够的话,系统可能在低峰期几个小时才会有一次新生代GC,而在高峰期最多也就几分钟一次新生代GC。
一般的业务系统都会部署在2核4G或4核8G的机器上,此时分配给堆的内存不会超3G,给新生代中的Eden区的内存大概是1G。
二.而且新生代采用的复制算法效率极高
因为新生代里存活的对象很少,所以可以迅速标记出这少量存活对象。然后再将存活对象移动到Survivor区,接着回收全部垃圾对象。整个标记 -> 移动 -> 回收的速度很快(小内存ParNew + 大内存G1)。
通常来说,一次新生代GC可能也就耗费几毫秒~几十毫秒。假如系统运行时每隔几分或几十分执行一次新生代GC,卡顿几十毫秒。尽管这几十毫秒期间的请求会出现卡顿,但此时用户几乎是无感知的,所以新生代GC一般对系统性能影响不大。
(3)什么时候新生代GC对系统影响很大
当系统部署在大内存机器上时,新生代GC会对系统影响会很大。比如系统部署在32核64G的机器上,分配给JVM的内存就有几十G,那么新生代的Eden区可能就会有30~40G的内存。
Kafka、Elasticsearch等大数据相关系统,会部署在大内存的机器上。如果系统负载非常高,比如每秒几万的访问请求发到Kafka、ES上,那么就可能会导致Eden区几十G内存迅速被占满而触发垃圾回收。
假设1分钟就会占满新生代,然后需要执行GC,每次GC需要几秒钟。那么由于每次进行垃圾回收时都要暂停Kafka、ElasticSearch的运行,所以会发现每隔一分钟,Kafka、ElasticSearch系统就要卡顿几秒钟。甚至有的请求一旦卡住几秒就会超时报错,从而导致系统频繁出错。
(4)如何处理大内存机器新生代GC过慢
那么应该如何解决这种几十G大内存机器的新生代GC过慢的问题呢?答案是使用G1垃圾回收器。
可以对G1垃圾回收器设置一个期望的每次GC的停顿时间,比如20ms。那么G1就会基于它的Region内存划分原理,在运行一段时间后,就只针对比如2G内存的Region进行垃圾回收,此时只需停顿20ms,然后回收掉2G内存空间,腾出内存后,接着继续让系统运行。
G1天生就适合这种大内存JVM的运行,G1能完美解决大内存垃圾回收时间过长的问题。
(5)要命的频繁老年代GC问题
综上所述,新生代GC一般不会有太大问题。真正有问题的是,频繁触发老年代GC。
一.对象进入老年代的条件
年龄太大 + 动态年龄判断规则 + 新生代GC后存活对象太多无法放入S区。
第一:对象年龄太大了
这种对象一般很少,基本是系统中要长期存在的核心组件,不需要回收。这些核心组件对象,在新生代熬过默认15次垃圾回收后就会进入老年代。
第二:动态年龄判定规则
如果一次新生代GC后,发现S区的几个年龄对象加起来超过了S区50%。比如年龄1 + 年龄2 + 年龄3的对象大小总和,超过了S区的50%,此时就会把年龄3及以上的对象都放入老年代。
动态年龄判断规则有个推论:如果S区中的同龄对象大小超过S区内存的一半,就要直接升入老年代。
第三:新生代垃圾回收后,存活对象太多无法放入S区,直接进入老年代
其实上述条件中,第二个和第三个都是很关键的。如果新生代的S区内存过小,就会导致上述第二个第三个条件频繁发生。然后导致大量对象快速进入老年代,从而频繁触发老年代GC。
二.老年代GC都很耗费时间
无论是CMS垃圾回收器还是G1垃圾回收器,老年代GC都很耗费时间。如CMS有初始标记->并发标记->重新标记->并发清理->碎片整理环节,这几个环节的过程非常的复杂,对于G1也同样如此。
通常来说,老年代GC至少比新生代GC慢10倍以上。比如新生代GC每次耗费200ms其实对用户影响不大,但是老年代GC每次耗费2s就可能导致老年代GC时的用户请求卡顿2s,这时老年代GC对用户的影响就会很大。
所以一旦JVM内存分配不合理,导致老年代频繁GC,就会影响系统性能。比如几分钟就有一次老年代GC,每次老年代GC时系统都停顿几秒,那用户可能就会发现他发起的请求也会每几分钟卡顿几秒。
(6)JVM性能优化到底在优化什么
基于JVM运行的系统最大的问题其实就是:因为内存分配、参数设置不合理,导致对象频繁进入老年代。然后频繁触发老年代GC,导致系统每隔几分钟就要卡顿几秒钟。这就是所谓的JVM性能问题,也是JVM性能优化时需要优化的地方。
2.什么是Young GC什么是Full GC
(1)Minor GC / Young GC
(2)Full GC / Old GC
(3)Full GC
(4)Major GC
(5)Mixed GC
(1)Minor GC / Young GC
新生代也可以称为年轻代,这两个名词是等价的。当新生代的Eden内存区被占满后,就需要触发新生代GC(年轻代GC)。此时这个新生代GC就是所谓的Minor GC,也可以称为Young GC。所以Minor GC和Young GC这两个名词,就是专门针对新生代GC的。
(2)Full GC / Old GC
老年代被占满后就会触发老年代的GC,也会把这种GC也称为Full GC,但有人会觉得老年代的GC不能叫Full GC。《深入理解Java虚拟机》中Full GC指收集整个Java堆和方法区的垃圾。所以所谓老年代GC,称Old GC可能会更加合适,字面意义上比较符合。但有时候如果把老年代GC称为Full GC,其实也是可以的。老年代GC,还有一种叫法是Major GC。
(3)Full GC
Full GC指的是针对新生代、老年代、永久代的全体内存空间的垃圾回收。从字面意思上也可以理解,Full就是整体的意思。所以Full GC就是对JVM的一次整体的垃圾回收,把各区的垃圾都回收掉。
(4)Major GC
Major GC其实一般用的比较少,它是一个非常容易混淆的概念。有些人把Major GC跟Old GC等价,认为它就是针对老年代的GC。也有人把Major GC和Full GC等价,认为它是针对JVM全部区域的GC。
(5)Mixed GC
Mixed GC是G1中特有的概念,主要是指在G1中,一旦老年代占堆内存45%了,就要触发Mixed GC,此时年轻代和老年代都会进行回收。
3.Young GC、Old GC和Full GC的发生情况
(1)名词解释
(2)Young GC的触发时机
(3)Old GC和Full GC的触发时机
(4)永久代填满了之后怎么办
(1)名词解释
可以认为Young GC就是年轻代的GC,Old GC就是老年代的GC,Full GC是针对年轻代、老年代、永久代进行的整体的GC。
Minor GC也可以称之为Young GC,Major GC也可以称之为Old GC,有的人也把Major GC和Full GC划等号,也有人把Full GC和Old GC划等号。
这里用Young GC指代年轻代GC,用Old GC指代老年代GC,用Full GC指代年轻代、老年代、永久代共同的GC。
(2)Young GC的触发时机
Young GC是在新生代Eden区域满了之后就会触发,采用复制算法来回收新生代垃圾。
(3)Old GC和Full GC的触发时机
一.Old GC的触发时机
情况一:发生Young GC之前进行检查
如果老年代可用空间 < 新生代历次GC后入进老年代的对象的平均大小,说明本次YGC后升入老年代的对象大小,可能超过老年代当前可用空间。此时就要先触发一次Old GC给老年代腾出更多空间,然后再执行YGC。
情况二:执行Young GC之后有一大批对象需要放入老年代
此时老年代已经没有足够的内存空间存放这些对象了,因此必须立即触发一次Old GC。
情况三:老年代内存使用率已超过了92%,也要直接触发Old GC
当然这个比例是可以通过参数调整的。
上述三个条件可以概括成一句话:由于老年代空间不够 + 没法放入更多对象,于是就要执行Old GC回收老年代垃圾。注意:执行Old GC时一般都会带上一次Young GC。
二.Full GC的触发时机
在很多JVM实现里,其实达到上述几种条件时,触发的就是Full GC。Full GC会包含Young GC、Old GC和永久代(方法区/元数据区)的GC,也就是触发Full GC时,会回收新生代、老年代和永久代里面的垃圾对象。
(4)永久代填满了之后怎么办
当存放类信息、常量池的永久代满了之后,就会触发一次Full GC。因为Full GC执行时会顺带把永久代中的垃圾给回收了,但是永久代中的垃圾一般是很少的。永久代里存放的都是一些类、常量池等,这些信息一般是不需要回收的。如果永久代真的满了,回收后发现没腾出更多空间,则只能抛出OOM。
4.频繁YGC的案例(G1解决大内存YGC过慢)
(1)服务于百万级商家的BI系统是什么
(2)刚开始上线BI系统时的部署架构
(3)技术痛点:实时刷新报表+大数据量报表
(4)没什么大影响的频繁Young GC
(5)提升机器配置:运用大内存机器
(6)用G1来优化大内存机器的Young GC性能
(7)总结
(1)服务于百万级商家的BI系统是什么
作为一个电商平台,可能会有数十万到百万的商家在平台上做生意。电商平台每天会产生大量数据,需要基于这些数据为商家提供数据报表。比如:每个商家每天有多少访客、有多少交易、付费转化率是多少。
BI系统其实就是把商家日常经营的数据收集起来进行分析,然后提供各种数据报表给商家的一套系统。
这样的一个BI系统,其运行逻辑如下:
首先电商平台会提供一个业务平台给商家进行日常使用交互。该业务平台会采集到商家的很多日常经营数据。然后根据这些日常经营数据,通过Hadoop、Spark等技术计算各种数据报表。这些数据报表会被放入存储到MySQL、Elastcisearch、HBase中。最后基于MySQL、HBase、ES中存储的数据报表,开发出一个BI系统。通过这个BI系统就能把各种存储好的数据展示给商家进行筛选和分析。
(2)刚开始上线BI系统时的部署架构
刚开始系统上线时,这个BI系统使用的商家是不多的,比如几千个商家。所以刚开始系统部署得非常简单,就是用几台机器来部署上述BI系统。机器都是普通的4核8G配置,在这个配置下,会给堆内存新生代分配1.5G内存,Eden区大概1G左右。如下图示:
(3)技术痛点:实时刷新报表+大数据量报表
刚开始在少数商家的情况下,这个系统是没多大问题的,运行非常良好。但使用系统的商家开始越来越多,商家的数量级达到几万时就有问题了。
首先说明一下此类BI系统的特点,就是在BI系统中有一种实时数据报表,它支持前端页面运行一个JS脚本,该JS脚本每隔几秒就会自动发送请求到后台刷新一下数据。如下图示:
虽然只有几万商家使用该系统,但可能同时打开实时报表的商家有几千。每个商家打开报表后,前端都会每隔几秒发送请求到后台加载最新数据。于是部署BI系统的每台机器每秒请求就已达几百个,假设每秒500请求。
然后每个请求会加载出一张报表所需要的大量数据,BI系统可能还要针对这些数据在内存中进行计算加工,才能返回。根据测算,每个请求大概会从MySQL中加载出100K的数据进行计算。因此每秒500个请求,就需要加载50M的数据到内存中进行计算。如下图示:
(4)没什么大影响的频繁Young GC
在上述系统运行模型下,由于每秒会加载50M的数据到Eden区中。所以只要200s就会填满Eden区,然后就会触发一次YGC。1G左右的Eden区进行YGC的速度也是比较快的,可能几十ms就搞定了。所以每200s频繁执行一次YGC其实对系统性能影响并不大,而且上述场景下,基本上每次YGC后存活对象可能会有几十M。
因此可能会看到如下场景:BI系统每运行几分钟就会突然卡顿10ms,但几乎不影响用户和系统性能。
(5)提升机器配置:运用大内存机器
针对这样的一套BI系统:当越来越多商家使用,并发压力越来越大,甚至高峰期QPS达每秒10万。如果还用4核8G机器,则要部署上百台机器来抗每秒10万的并发压力。
所以针对这种情况,假设决定通过提升机器配置去处理。那么由于BI系统非常吃内存,所以将机器配置全面提升到16核32G。这样每台机器可以抗每秒几千请求,此时只要部署二三十台机器即可。
此时问题就来了:如果用大内存机器,则新生代内存至少会分配20G,Eden区也会占16G。由于每个请求加载100K数据,故每秒几千请求会加载几百M数据到内存。那么大概1分钟左右就会填满Eden区,需要执行YGC。此时YGC要回收那么大的内存,速度会慢很多,也许会导致系统卡顿个几百毫秒或者1秒。
如果系统卡顿时间过长,必然会导致很多请求积压排队,严重时会导致线上系统时不时出现前端请求超时的问题。
(6)用G1来优化大内存机器的Young GC性能
所以对这个系统的优化,就是采用G1来应对大内存下YGC过慢的问题。可以对G1设置一个预期的GC停顿时间,比如100ms。让G1保证每次Young GC时最多停顿100ms,避免影响终端用户的使用。
此时效果是非常显著的。G1在每次YGC时会回收一部分Region,确保GC停顿时间在100ms内。这样也许YGC频率高一些,但由于每次停顿时间很小,对系统影响不大。
(7)总结
通常来说,YGC即使发生比较频繁,其实对系统也造成不了太大影响。只有在机器内存特别大时,才要注意YGC可能会导致比较长时间的停顿。所以针对大内存机器通常建议采用G1垃圾回收器。
5.频繁FGC的案例(YGC存活对象S区放不下)
(1)一个日处理上亿数据的计算系统
(2)这个系统多久会塞满新生代
(3)触发YGC时会有多少对象进入老年代
(4)系统运行多久老年代就会被填满
(5)这个系统运行多久,老年代会触发1次FGC
(6)该案例应该如何进行JVM优化
(7)如果该系统的工作负载再次扩大10倍
(8)使用大内存机器来优化上述场景
(1)一个日处理上亿数据的计算系统
当时团队里自研的一个数据计算系统,日处理数据量在上亿的规模。这个系统会不停的从MySQL数据库以及其他数据源里提取大量的数据,然后加载到自己的JVM内存里来进行计算处理。如下图示:
这个数据计算系统会不停的通过SQL语句和其他方式,从各种数据存储中提取数据到内存中来进行计算,大致当时的生产负载是每分钟需要执行500次数据提取和计算的任务。
由于这是一套分布式运行的系统,所以生产环境部署了多台机器。每台机器大概每分钟负责执行100次数据提取和计算的任务(15个线程)。每次会提取大概1万条数据到内存计算,平均每次计算大概耗费10秒。然后每台机器4核8G,新生代和老年代分别是1.5G和1.5G的内存空间。如下图示:
(2)这个系统多久会塞满新生代
现在明确了一些核心数据,那么该系统多久会塞满新生代的内存空间?既然每台机器上部署的该系统实例,每分钟会执行100次数据计算任务。每次1万条数据需要计算10秒时间,该台机器大概开启15个线程去执行。
那么先来看看每次1万条数据大概会占用多大的内存空间。这里每条数据都是比较大的,每条数据大概包含20个字段。可认为平均每条数据的大小在1K左右,那么每次计算任务提取的1万条数据就对应了10M大小。
所以如果新生代按照8:1:1的比例来分配Eden和两块Survivor的区域,那么Eden区就是1.2G,每块Survivor区在100M左右。如下图示:
由于每次执行一个计算任务,就要提取1万条数据到内存,每条数据1K。所以每次执行一个计算任务,JVM会在Eden区里分配10M的对象。那么由于一分钟需要执行大概100次计算任务,所以新生代里的Eden区,基本上1分钟左右就会被迅速填满。
(3)触发YGC时会有多少对象进入老年代
假设新生代的Eden区在1分钟后都塞满对象了,在继续执行计算任务时,必然会导致需要进行YGC回收部分垃圾对象。
一.在执行YGC前会先进行检查
首先会看老年代的可用内存空间是否大于新生代全部对象。此时老年代是空的,大概有1.5G的可用内存空间,而新生代的Eden区大概有1.2G对象。
于是会发现老年代的可用内存空间有1.5G,新生代的对象总共有1.2G。即使一次YGC过后,即时全部对象都存活,老年代也能放的下的。所以此时会直接执行YGC。
二.执行YGC后,Eden区里有多少对象是存活的无法被垃圾回收的
由于新生代的Eden区在1分钟就塞满对象需要YGC了,而1分钟内会执行100次任务,每个计算任务处理1万条数据需要10秒钟。
假设执行YGC时,有80个计算任务都执行结束了。但还有20个计算任务共计200M的数据还在计算中,那么此时就有200M的对象是存活的,不能被垃圾回收掉。所以总共有1G对象可以进行垃圾回收,200M对象存活无法被垃圾回收。如下图示:
三.此时执行一次YGC会回收1G对象,然后出现200M的存活对象
这200M的存活对象并不能放入S区,因为一块S区只有100M的大小。此时老年代会通过空间担保机制,让这200M的对象直接进入老年代中。于是需要占用老年代里的200M内存空间,然后对Eden区进行清空。
(4)系统运行多久老年代就会被填满
按照上述计算,每分钟都是一个轮回。大概算下来是每分钟都会把新生代的Eden区填满。然后触发一次YGC,接着大概会有200M左右的数据进入老年代。
假设2分钟过去了,老年代已有400M内存被占用,只有1.1G内存可用。此时老年代的可用内存空间已经少于新生代的内存大小了。所以如果第3分钟运行完毕,又要进行YGC,会做什么检查呢?如下图示:
一.首先检查老年代的可用空间是否大于新生代全部对象
此时老年代可用空间1.1G,新生代对象有1.2G。如果这次YGC过后新生代里的对象全部都存活,那么老年代是放不下它们的。
二.接着就得检查HandlePromotionFailure参数是否打开
如果"-XX:-HandlePromotionFailure"参数被打开了,一般都会打开,此时会进入下一个检查:老年代可用空间是否大于历次YGC过后进入老年代的对象的平均大小。
前面已计算出大概每分钟会执行一次YGC,每次200M对象进入老年代。此时老年代可用1.1G,大于每次YGC进入老年代的对象平均大小200M。所以可推测本次YGC后大概率还是有200M对象进入老年代,1.1G足够。因此这时就可以放心执行一次YGC,然后又有200M对象进入老年代。
三.转折点大概在运行了7分钟后
执行了7次YGC后,大概1.4G对象进入老年代,老年代剩余空间就不到100M,几乎满了。如下图示:
(5)这个系统运行多久,老年代会触发1次FGC
大概在第8分钟运行结束时,新生代又满了。执行YGC之前进行检查,发现老年代此时只有100M的可用内存空间,小于历次YGC后进入老年代的200M对象,于是就会直接触发一次FGC。FGC会把老年代的垃圾对象都给回收掉。
假设此时老年代被占据的1.4G空间里,全部都是可以回收的对象,那么此时就会一次性把这些对象都给回收掉。如下图示:
然后执行完FGC后,还会继续执行YGC,又有200M对象进入老年代,之前的FGC就是为这次新生代YGC后要进入老年代的对象准备的。如下图示:
所以按照这个运行模型进行分析:平均八分钟会发生一次FGC,这个频率就很高了,而且每次FGC速度都是很慢的、性能很差。
(6)该案例应该如何进行JVM优化
通过上述案例可以清楚看到:新生代和老年代应该如何配合使用,什么情况会触发Young GC和Full GC,什么情况会导致频繁Young GC和Full GC。
如果要对这个系统进行优化:由于该系统是数据计算系统,每次YGC时都会有一批数据没计算完毕。所以按现有的内存模型,最大问题就是每次YGC后S区放不下存活对象。
所以可以对生产系统进行调整:增加新生代的内存比例,3G堆内存的2G给新生代,1G给老年代。这样S区大概就是200M,每次刚好能放得下YGC过后存活的对象。如下图示:
只要每次YGC过后200M存活对象可以放进Survivor区域,那么等下次YGC时,这个S区的对象对应的计算任务早就结束可回收了。比如此时Eden区里1.6G空间被占满了,然后S1区里有200M上一轮YGC后存活的对象。如下图示:
此时执行YGC后:就会把Eden区里1.6G对象回收掉,S1区里的200M对象也会回收掉。然后Eden区里剩余的200M存活对象会放入S2区里,如下图示:
以此类推,基本就很少有对象进入老年代了,老年代的对象也不会太多。这样就把生产系统老年代FGC的频率从几分钟一次降低到几小时一次。大幅度提升了系统的性能,避免了频繁FGC对系统运行的影响。
前面说过一个动态年龄判定升入老年代的规则:如果S区中的同龄对象大小超过S区内存的一半,就要直接升入老年代。
所以这里的优化仅仅是做一个示例说明而已,实际S区200M还是不够。但表达的是要增加S区大小,让YGC后的对象进入S区,避免进入老年代。
实际上为了避免由于动态年龄判定规则而把S区中的对象直接升入老年代,如果新生代内存有限,那么可以调整"-XX:SurvivorRatio=8"参数。比如降低Eden区的比例(默认80%),给两块S区更多的内存空间。让每次YGC后的对象进入S区,避免动态年龄规则把它们升入老年代。
(7)如果该系统的工作负载扩大10倍
一段时间过后,该系统工作负载扩大10倍,每日需处理10亿数据的计算。根据上图,此时会导致每秒要加载100M的数据到内存里。对于1.6G的Eden区而言,10多秒就会迅速塞满,此时就会触发YGC。
但由于每次加载一批数据到内存里,一般要处理10秒以上才可以计算完毕,而在计算完毕之前这些数据都是不能被回收的。
所以如果10多秒就触发一次YGC,导致的后果就是:可能可以回收掉的垃圾也就几百M,有1G的对象可能都是无法回收的。于是就会导致每隔10多秒,就有1G对象进入老年代,而老年代也就1G。即使勉强能放下,那么10多秒过后下一次YGC又会放1G对象到老年代。此时必然会提前触发FGC去回收老年代里的1G对象,然后再把这次YGC后存活的1G对象放入老年代。
这就是当时遇到的生产场景:一台4核8G的机器,每分钟要触发几次FGC,对系统性能造成巨大影响。
(8)使用大内存机器来优化上述场景
针对负载扩大10倍导致的问题,因为计算类的系统非常吃内存,所以将数据计算系统的每台机器都更换成了16核32G的高配置机器。这样Eden的空间就会扩大10倍,比如有16G,Survivor区就会有2G。
由于每秒会加载100M数据到内存计算,所以2分钟才会触发一次YGC。因为降低了YGC的频率,所以每次YGC时存活的对象大概也就几百M。因S区有2G,故每次YGC后的存活对象可轻松放入S区,不会进老年代。这就完美地通过提升机器配置的方式,解决了频繁YGC和FGC的问题。
那么针对大内存机器,此时是否需要用G1来减少每次YGC的停顿时间?不需要,因为数据计算系统是一个后台自动计算系统,它不面向用户。所以哪怕每隔2分钟一次YGC,一次停顿几秒钟,也没任何影响。因此如果大内存机器不是面向用户的,其实也可以不用G1垃圾回收器。
6.问题汇总
问题一:
G1和ParNew + CMS调优原则都是尽可能YGC,不做老年代GC。G1相对而言更加智能,这也意味着JVM会用更多资源去判断每个Region的使用情况。
ParNew + CMS则更加纯粹和直接。虽然G1不会产生碎片,但由于Region存活率大于85%会不清理,所以G1会导致内存没有充分释放。
因此,对于CPU性能高的 + 内存容量大的 + 用户响应敏感的系统推荐使用G1,对于内存小的 + CPU性能比较低的系统使用ParNew + CMS会更合适。
问题二:
为什么说G1适合大堆的情况?
答:假设有32G内存。如果用ParNew+CMS,必须内存填满了才会触发GC。此时进行新生代GC就会回收几十G垃圾,那么速度就会很慢。从而可能导致系统停顿时间多达几秒甚至更高都有可能。但是用了G1后,它会频繁地回收Region,且每次只回收一部分Region。从而保证停机时间不会太长,所以G1其实更加适合大内存的机器。
问题三:
一.YGC和OGC一样都是追踪GC Roots,为什么后者的追踪更慢呢?
二.从GC Roots追踪,指的是GC线程会从扫描栈的局部变量开始吗?
答:一.老年代GC,从GC Roots开始追踪。但老年代的存活对象多,所以追踪速度慢。新生代存活对象少,所以追踪速度快。二.两种典型的GC Roots:方法里的局部变量、类的静态变量,初始标记的时候会从这两个地方开始追踪扫描。
问题四:
系统创建的对象被分配到Java堆内存中,要计算对象内所占的内存大小,就要计算对象的每个部分所占内存大小。Java对象包括:对象头、实例数据以及对象填充。对象头包括对象的基本信息以及Class的指针类等相关信息占用64bit。实例数据包括八种数据基本类型以及引用类型。将所用的进行全部相加,并且使用8的最低倍数进行对象填充。最后得出的结果就是所占用的内存空间bit。
问题五:
G1回收器的垃圾回收原理总结如下:
一.如果新生代未达60%,老年代未达45%,系统照常运行,不会触发回收
二.如果新生代达60%,此后如果有新对象生成放入新生代,则会触发YGC
三.如果老年代达到45%,则触发混合回收
混合回收时:
一.首先通过GC Roots初始标记存活对象,此过程会STW不过很快
二.然后并发标记,用户线程和标记线程并行
三.接着最终标记,会STW,标记并发标记过程中可能新产生的垃圾对象
四.最后混合回收,采用复制算法,不会产生垃圾碎片,不用整理碎片
G1会按照给定的时间去STW并回收,争取回收性价比高的Region。默认下,如果回收次数少于8次,则再次混合回收。不过在回收过程中,如果发现空闲Region大小达到堆5%,会提前结束。如果Region回收失败,则会转换采用Serial Old回收器。
问题六:
长生命周期的对象有哪些?
答:长生命周期的对象有:
一.Spring的Bean
二.线程池的核心线程及其引用的对象,其中包括ThreadLocal引用的对象
三.Tomcat组件:Connector和Container(如Filter,Servlet,Listener)
四.Classloader、Class对象也是长生命周期
五.各类池化技术,比如线程池,连接池等