IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 案例实战:每日百亿数据量的实时分析引擎,如何定位和解决频繁Full GC问题? -> 正文阅读

[大数据]案例实战:每日百亿数据量的实时分析引擎,如何定位和解决频繁Full GC问题?

添加VX:ruyuan0220,回复:CSDN,领取更多精品学习资料!

目录

一个日处理上亿数据的计算系统

这个系统到底多快会塞满新生代?

触发Minor GC的时候会有多少对象进入老年代?

系统运行多久,老年代大概就会填满?

这个系统运行多久,老年代会触发1次Full GC?

该案例应该如何进行JVM优化?

运行程序用的示例JVM参数

基于jstat分析程序运行的状态

对JVM性能进行优化

思考题


一个日处理上亿数据的计算系统

先给大家说一下这个系统的案例背景,当时我们团队里自己研发的一个数据计算系统,日处理数据量在上亿的规模。

为了方便大家集中注意力理解这个系统的生产环境的JVM相关的东西,所以对系统本身就简化说明了。

简单来说,这个系统就是会不停的从MySQL数据库以及其他数据源里提取大量的数据加载到自己的JVM内存里来进行计算处理,如下图所示。

这个数据计算系统会不停的通过SQL语句和其他方式从各种数据存储中提取数据到内存中来进行计算,大致当时的生产负载是每分钟大概需要执行500次数据提取和计算的任务。

但这是一套分布式运行的系统,所以生产环境部署了多台机器,每台机器大概每分钟负责执行100次数据提取和计算的任务。

每次会提取大概1万条左右的数据到内存里来计算,平均每次计算大概需要耗费10秒左右的时间

然后每台机器是4核8G的配置,JVM内存给了4G,其中新生代和老年代分别是1.5G的内存空间,大家看下图。

这个系统到底多快会塞满新生代?

现在明确了一些核心数据,接着我们来看看这个系统到底多快会塞满新生代的内存空间?

既然这个系统每台机器上部署的实例,每分钟会执行100次数据计算任务,每次是1万条数据需要计算10秒的时间,那么我们来看看每次1万条数据大概会占用多大的内存空间?

这里每条数据都是比较大的,大概每条数据包含了平均20个字段,可以认为平均每条数据在1KB左右的大小。

那么每次计算任务的1万条数据就对应了10MB的大小。所以大家此时可以思考一下,如果新生代是按照8:1:1的比例来分配Eden和两块Survivor的区域,那么大体上来说,Eden区就是1.2GB,每块Survivor区域在100MB左右,如下图。

基本上按照这个内存大小而言,大家会发现,每次执行一个计算任务,就会在Eden区里分配10MB左右的对象

一分钟大概对应100次计算任务,基本上一分钟过后,Eden区里就全是对象,基本就全满了。

所以说,回答这个小节的问题,新生代里的Eden区,基本上1分钟左右就迅速填满了

触发Minor GC的时候会有多少对象进入老年代?

此时假设新生代的Eden区在1分钟过后都塞满对象了,然后在接着继续执行计算任务的时候,势必会导致需要进行Minor GC回收一部分的垃圾对象。

这里在执行Minor GC之前会先进行的检查。

首先第一步,先看看老年代的可用内存空间是否大于新生代全部对象?

看下图,此时老年代是空的,大概有1.5G的可用内存空间,新生代的Eden区大概算他有1.2G的对象好了。

此时会发现老年代的可用内存空间有1.5GB,新生代的对象总共有1.2GB,即使一次Minor GC过后,全部对象都存活,老年代也能放的下的,那么此时就会直接执行Minor GC了。

那么此时Eden区里有多少对象还是存活的,无法被垃圾回收呢?

大家可以考虑一下之前说的那个点,每个计算任务1万条数据需要计算10秒钟,所以假设此时80个计算任务都执行结束了,但是还有20个计算任务共计200MB的数据,还在计算中,那么此时就是200MB的对象是存活的,不能被垃圾回收掉,然后有1GB的对象是可以垃圾回收的,大家看下图。

此时一次Minor GC就会回收掉1GB的对象,然后200MB的对象能放入Survivor区吗?

不能

因为任何一块Survivor区实际上就100MB的空间,此时就会通过空间担保机制,让这200MB对象直接进入老年代去,占用里面200MB内存空间,然后Eden区就清空了,大家看下图。

系统运行多久,老年代大概就会填满?

那么大家想一下,这个系统大概运行多久,老年代会填满呢?

按照上述计算,每分钟都是一个轮回,大概算下来是每分钟都会把新生代的Eden区填满,然后触发一次Minor GC,然后大概都会有200MB左右的数据进入老年代。

那么大家可以想一下,假设现在2分钟运行过去了,此时老年代已经有400MB内存被占用了,只有1.1GB的内存可用,此时如果第3分钟运行完毕,又要进行Minor GC会做什么检查呢?

如下图:

此时会先检查老年代可用空间是否大于新生代全部对象,此时老年代可用空间1.1GB,新生代对象有1.2GB,那么此时假设一次Minor GC过后新生代对象全部存活,老年代是放不下的,那么此时就得看看一个参数是否打开了 。

如果“-XX:-HandlePromotionFailure”参数被打开了,当然一般都会打开,此时会进入第二步检查,就是看看老年代可用空间是否大于历次Minor GC过后进入老年代的对象的平均大小。

我们已经计算过了,大概每分钟会执行一次Minor GC,每次大概200MB对象会进入老年代。

那么此时发现老年代的1.1GB空间,是大于每次Minor GC后平均200MB对象进入老年代的大小的,所以基本可以推测,本次Minor GC后大概率还是有200MB对象进入老年代,1.1G可用空间是足够的。

所以此时就会放心执行一次Minor GC,然后又是200MB对象进入老年代。

转折点大概在运行了7分钟过后,7次Minor GC执行过后,大概1.4G对象进入老年代,老年代剩余空间就不到100MB了,几乎快满了,如下图。

这个系统运行多久,老年代会触发1次Full GC?

大概在第8分钟运行结束的时候,新生代又满了,执行Minor GC之前进行检查,此时发现老年代只有100MB内存空间了,比之前每次Minor GC后进入老年代的200MB对象要小,此时就会直接触发一次Full GC。

Full GC会把老年代的垃圾对象都给回收了,假设此时老年代被占据的1.4G空间里,全部都是可以回收的对象,那么此时一次性就会把这些对象都给回收了,如下图。

然后接着就会执行Minor GC,此时Eden区情况,200MB对象再次进入老年代,之前的Full GC就是为这些新生代本次Minor GC要进入老年代的对象准备的,如下图。

按照这个运行模型,基本上平均就是七八分钟一次Full GC,这个频率就相当高了。因为每次Full GC速度都是很慢的,性能很差

该案例应该如何进行JVM优化?

相信通过这个案例,大家结合图一路看下来,对新生代和老年代如何配合使用,然后什么情况下触发Minor GC和Full GC,什么情况下会导致频繁的Minor GC和Full GC,大家都有了更加深层次和透彻的理解了。

对这个系统,其实要优化也是很简单的,因为这个系统是数据计算系统,每次Minor GC的时候,必然会有一批数据没计算完毕,但是按照现有的内存模型,最大的问题,其实就是 每次Survivor区域放不下存活对象。

所以当时我们就是对生产系统进行了调整,增加了新生代的内存比例,3GB左右的堆内存,其中2GB分配给新生代,1GB留给老年代

这样Survivor区大概就是200MB,每次刚好能放得下Minor GC过后存活的对象了,如下图所示。

只要每次Minor GC过后200MB存活对象可以放Survivor区域,那么等下一次Minor GC的时候,这个Survivor区的对象对应的计算任务早就结束了,都是可以回收的了,此时比如Eden区里1.6GB空间被占满了,然后Survivor1区里有200MB上一轮 Minor GC后存活的对象,如下图。

然后此时执行Minor GC,就会把Eden区里1.4GB对象回收掉,Survivor1区里的200MB对象也会回收掉,然后Eden区里剩余的200MB存活对象会放入Survivor2区里,如下图。

以此类推,基本上就很少对象会进入老年代中,老年代里的对象也不会太多的。

通过这个分析和优化,定时我们成功的把生产系统的老年代Full GC的频率从几分钟一次降低到了几个小时一次,大幅度提升了系统的性能,避免了频繁Full GC对系统运行的影响。

运行程序用的示例JVM参数

下面的参数唯一修改的就是“-XX:PretenureSizeThreshold,把大对象阈值修改为了20MB,避免我们程序里分配的大对象直接进入老年代。

-XX:NewSize=104857600?-XX:MaxNewSize=104857600?-XX:InitialHeapSize=209715200?-XX:MaxHeapSize=209715200?-XX:SurvivorRatio=8??-XX:MaxTenuringThreshold=15?-XX:PretenureSizeThreshold=20971520?-XX:+UseParNewGC?-XX:+UseConcMarkSweepGC?-XX:+PrintGCDetails?-XX:+PrintGCTimeStamps?-Xloggc:gc.log

示例程序

简单给大家解释一下上面的程序

大概意思其实就是,每秒钟都会执行一次loadData()方法,他会分配4个10MB的数组,但是都立马成为垃圾,但是会有data1和data2两个10MB的数组是被变量引用必须存活的,此时Eden区已经占用了六七十MB空间了,接着是data3变量依次指向了两个10MB的数组,这是为了在1s内触发Young GC的。

基于jstat分析程序运行的状态

接着我们基于jstat分析程序运行的状态,启动程序后立马采用jstat监控其运行状态可以看到如下的信息:

接着我们一点点来分析这个jvm的运行状态。首先我们先看如下一行截图:

在这里最后一行,可以清晰看到,程序运行起来之后,突然在一秒内就发生了一次Young GC,这是为什么呢?

很简单,按照我们上述的代码,他一定会在一秒内触发一次Young GC的。

Young GC过后,我们发现S1U,也就是一个Survivor区中有587KB的存活对象,这应该就是那些未知对象了。

然后我们明显看到在OU中多出来了30MB左右的对象,因此可以确定,在这次Young GC的时候,有30MB的对象存活了,此时因为Survivor区域放不下,所以直接进入老年代了。

我们接着看下面的截图

大家看上图中红圈的部分,很明显每秒会发生一次Young GC,都会导致20MB~30MB左右的对象进入老年代

因为每次Young GC都会存活下来这么多对象,但是Survivor区域是放不下的,所以都会直接进入老年代。

此时看到老年代的对象占用从30MB一路到60MB左右,此时突然在60MB之后下一秒,明显发生了一次Full GC,对老年代进行了垃圾回收,因为此时老年代重新变成30MB了。

为啥会这样?

很简单,老年代总共就100MB左右,已经占用了60MB了,此时如果发生一次Young GC,有30MB存活对象要放入老年代的话,你还要放30MB对象,明显老年代就要不够了,此时必须会进行Full GC,回收掉之前那60MB对象,然后再放入进去新的30MB对象。

所以大家可以看到,按照我们的这段代码,几乎是每秒新增80MB左右,触发每秒1次Young GC,每次Young GC后存活下来20MB~30MB的对象,老年代每秒新增20MB~30MB的对象,触发老年代几乎三秒一次Full GC,是不是跟我们上面的案例中分析的场景很类似?Young GC太频繁,而且每次GC后存活对象太多,频繁进入老年代,频繁触发老年代的GC。

那么Young GC和Full GC的耗时呢?看下图:

大家看上图,有没有发现Young GC特别坑爹,28次Young GC,结果耗费了180毫秒,平均下来一次Young GC要6毫秒左右。但是14次Full GC才耗费34毫秒,平均下来一次Full GC才耗费两三毫秒。这是为什么呢?

很简单,按照上述程序,每次Full GC都是由Young GC触发的,因为Young GC过后存活对象太多要放入老年代,老年代内存不够了触发Full GC,所以必须得等Full GC执行完毕了,Young GC才能把存活对象放入老年代,才算结束。这就导致Young GC也是速度非常慢。

对JVM性能进行优化

接着我们按照之前学习的思路对JVM进行优化,很简单,他最大的问题就是每次Young GC过后存活对象太多了,导致频繁进入老年代,频繁触发Full GC

我们只需要调大年轻代的内存空间,增加Survivor的内存即可,看如下JVM参数:

-XX:NewSize=209715200?-XX:MaxNewSize=209715200?-XX:InitialHeapSize=314572800?-XX:MaxHeapSize=314572800?-XX:SurvivorRatio=2??-XX:MaxTenuringThreshold=15?-XX:PretenureSizeThreshold=20971520?-XX:+UseParNewGC?-XX:+UseConcMarkSweepGC?-XX:+PrintGCDetails?-XX:+PrintGCTimeStamps?-Xloggc:gc.log

我们把堆大小调大为了300MB,年轻代给了200MB,同时“-XX:SurvivorRatio=2”表明,Eden:Survivor:Survivor的比例为2:1:1,所以Eden区是100MB,每个Survivor区是50MB,老年代也是100MB。

接着我们用这个JVM参数运行程序,用jstat来监控其运行状态如下:

在上述截图里,大家可以清晰看到,每秒的Young gC过后,都会有20MB左右的存活对象进入Survivor,但是每个Survivor区都是50MB的大小,因此可以轻松容纳,而且一般不会过50%的动态年龄判定的阈值。

我们可以清晰看到每秒触发Yuong GC过后,几乎就没有对象会进入老年代,最终就600KB的对象进入了老年代里,其他就没有对象进入老年代了。

再看下面的截图:

我们可以看到,只有Young GC,没有Full GC,而且11次Young GC才不过91毫秒,平均一次GC1毫秒都不到,没有Full GC干扰之后,Young GC的性能极高。

所以,其实这个案例就优化成功了,同样的程序,仅仅是调整了内存分配比例,立马就大幅度提升了JVM的性能,几乎把Full GC给消灭掉了。

思考题

自己按照文章的思路,写出来模拟代码,尝试用jstat去观察一下运行状态

尤其是今天的文章,需要你去亲自动手写代码,观察优化前频繁full gc的问题,然后优化jvm参数之后,再看看优化后的效果,相信小伙伴基本从实操层面就完全明白jvm的优化方法了。

----------------------- END?-----------------------

添加VX:ruyuan0220?或扫描下方二维码,回复:CSDN,领取更多精品学习资料!

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章           查看所有文章
加:2022-02-27 11:00:44  更:2022-02-27 11:01:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 12:11:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码