JVM 实战—频繁 YGC 和频繁 FGC 的后果
1.JVM GC 导致系统突然卡死无法访问
(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
新生代也可以称为年轻代,这两个名词是等价的。当新生代的 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)名词解释
可以认为 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 系统是什么
作为一个电商平台,可能会有数十万到百万的商家在平台上做生意。电商平台每天会产生大量数据,需要基于这些数据为商家提供数据报表。比如:每个商家每天有多少访客、有多少交易、付费转化率是多少。
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)一个日处理上亿数据的计算系统
当时团队里自研的一个数据计算系统,日处理数据量在上亿的规模。这个系统会不停的从 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 对象也是长生命周期
五.各类池化技术,比如线程池,连接池等
文章转载自:东阳马生架构
评论