背景###

最近发生了一起 Java 大对象引起的 FullGC 事件。记录一下。

有一位商家刷单,每单内有 50+ 商品。然后进行订单导出。订单导出每次会从订单详情服务取100条订单数据。由于 100 条订单数据对象很大,导致详情 FullGC ,影响了服务的稳定性。

本文借此来梳理下 Java 垃圾回收算法及分析 JVM 垃圾回收运行的方法。

案例分析###

如果对GC不太熟悉,可以先看看“GC姿势”部分,对 JVM 垃圾回收有一个比较清晰的理解。

测定大小####

回头看这个案例,显然它很可能触犯了“大对象容易触发 FullGC ” 的忌讳。先来测定下,这个大数据量的订单大小究竟有多少?

“HBase指定大量列集合的场景下并发拉取数据时卡住的问题排查” 有一段可以用来计算对象 deep-size 的方法。用法如下:


try {
ClassIntrospector.ObjectInfo objectInfo = new ClassIntrospector().introspect(orderDetailInfoList);
logger.info("object-deep-size: {} MB", (double)objectInfo.getDeepSize() / 1024.0 / 1024.0);
} catch (IllegalAccessException e) {
logger.warn("failed to introspect object size");
}

计算一个含有50个商品及优惠信息的订单,大小为 335KB,100 个就是 33M 这个商家导出了 4 次,每次有几百多单,会触发详情服务这边接受请求的几台服务器 FullGC ,进而影响详情服务的稳定性。

优化方法####

有两个方法可以组合使用:

  1. 检测这个订单是个大对象,将批量获取的条数改为更小,比如 10;

  2. 将大订单对象与小订单对象混合打散,降低大对象占用大量连续空间的概率。

可以做个问题抽象:有一个 0 与 1 组成的数组, 0 表示小对象, 1 表示大对象, 问题描述为:将一个 [0,1] 组成的数组打散,使得 1 的分布更加稀疏。 其中稀疏度可以如下衡量: 所有 1 之间的元素数目的平均值和方差。

这个问题看上去像洗牌,但实际是有区别的。洗牌是将有序的数排列打散变成无序,而现在是要使某些元素的分布更加均匀或稀疏。 一个简单的算法是:

STEP1: 遍历数组,将 0 和 1 分别放在列表 zeroList 和 oneList 里;

STEP2: 计算 0 与 1 的比值 ratio ; 创建一个结果列表 resultList ;

STEP3: 遍历 oneList ,对于每一个 1 , 将其加入 resultList ,同时加入 ratio 个 0 ;如果 0 不够,则仅返回剩余的 0 。

代码实现如下:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors; public class DistributedUtil { /**
* 一个列表,要求将满足条件 cond 的元素均匀分布到列表中。
*/
public static <T> List<T> even(List<T> alist, Predicate<T> cond) {
List<T> specialElements = alist.stream().filter(cond).collect(Collectors.toList());
List<T> normalElements = alist.stream().filter(e -> !cond.test(e)).collect(Collectors.toList()); int normalElemSize = normalElements.size();
int specialElemSize = specialElements.size(); if (normalElemSize == 0 || specialElemSize == 0) {
return alist;
} // 只要 normalElements 充足 , 每一个 specialElement 插入 ratio 个 normalElements
int ratio = normalElemSize % specialElemSize ==
0 ? (normalElemSize / specialElemSize) : (normalElemSize / specialElemSize + 1); List<T> finalList = new ArrayList<>();
int pos = 0;
for (T one: specialElements) {
finalList.add(one);
List<T> normalFetched = get(normalElements, ratio, pos);
pos += normalFetched.size();
finalList.addAll(normalFetched); }
return finalList;
} /**
* 从指定位置 position 取出 n 个元素 , 不足返回剩余元素或空元素
*/
public static <T> List<T> get(List<T> normalList, int n, int position) {
int size = normalList.size();
int num = size - position;
int realNum = Math.min(num, n);
return normalList.subList(position, position+realNum);
}
}

写个简单的单测验证下:

import org.junit.Test
import spock.lang.Specification
import spock.lang.Unroll import java.util.function.Predicate class DistributedUtilTest extends Specification { @Unroll
@Test
def "testEven"() {
expect:
result == DistributedUtil.even(originList, { it == 1 } as Predicate) where:
originList | result
[1, 1, 1, 1, 1] | [1, 1, 1, 1, 1]
[0, 0, 0, 0, 0] | [0, 0, 0, 0, 0]
[1, 0, 0, 0, 0, 0] | [1, 0, 0, 0, 0, 0]
[1, 0, 1, 0, 0, 0, 0] | [1, 0, 0, 0, 1, 0, 0]
[1, 0, 1, 1, 0, 0, 0, 0] | [1, 0, 0, 1, 0, 0, 1, 0]
[1, 0, 1, 1, 1, 0, 0, 0, 0] | [1, 0, 0, 1, 0, 0, 1, 0, 1]
[1, 0, 1, 1, 1, 1, 0, 0, 0, 0] | [1, 0, 1, 0, 1, 0, 1, 0, 1, 0]
[1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0] | [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1] }
}

GC姿势###

Java 垃圾回收采用的算法主要是:分代垃圾回收。垃圾回收算法简称 GC ,下文将以 GC 代之。

分代 GC 的主要理念是:大部分生成的对象都是生命周期短暂的对象,可以被很快回收掉;很少的对象能活动比较久。因此,分代回收算法,将垃圾回收分为两个阶段:新生代 GC 和 老年代 GC。

新生代 GC 采用算法基于 GC 复制算法,老年代 GC 采用的算法基于 标记-清除算法。

基础概念####

变量的分配

栈与堆。

栈:临时变量,作用域结束或函数执行完成后即被释放;

堆: 数组与对象的存储,不会随函数执行完成而释放。

栈的变量引用堆中的数组与对象。栈的变量就是根引用。引用通过指针来实现。

根引用与活动对象

从根引用出发,遍历所能引用和抵达的所有对象,这些对象都是活动对象。而其他则是非活动对象。

GC 的目标就是销毁非活动对象,腾出内存空间分配给新的对象和活动对象。

根引用(引用自 MAT 工具的文档):

  • Class loaded by bootstrap/system class loader

  • Object referred to from a currently active thread block.

  • A started, but not stopped, thread.

  • Everything that has called wait() or notify() or that is synchronized. For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object.

  • Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.

  • A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects.

NOTE ! GC 不仅仅是GC,还要与内存分配综合考虑。

四种引用

  • 强引用: 有强引用的对象不会被回收。

  • 软引用: 在空间不足时抛出OOM前会回收软引用的对象。内存敏感的缓存对象,比如cache的value对象

  • 弱引用: 当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。比如canonicalizing mappings

  • 虚引用:often used for scheduling pre-mortem cleanup actions in a more flexible way than is possible with the Java finalization mechanism . get 总是返回 null

算法指标

吞吐量: HEAP_SIZE / Cost(GCa+GCb+...+GCx)

最大暂停时间: max(GCi)

堆使用效率:HEAP_SIZE / Heap(GC)

分代回收算法####

  • 不同对象的活动周期不同;年轻代更快地回收,老年代回收频率相对少。分代回收 = YoungGC + OldGC

  • YoungGC: GC 复制算法。 比较频繁;

  • OldGC: GC 标记-清除算法。 频度低,回收慢。

GC复制算法

基本思路:

  1. 复制活动对象从From空间到To空间;复制活动对象也包括该活动对象引用所抵达的所有对象,是递归的。

  2. 吞吐量优秀(只需复制活动对象),堆利用率比较低。高速分配、无碎片化。

局部优化:

  • 迭代复制:避免栈溢出

  • 近似深度搜索复制

  • 多空间复制

GC标记-清除算法

就像插入排序,优点是:简单而且适合小数据量。

基本流程:

  1. 标记阶段: 从根引用出发,将所有可抵达的对象打上标记;

  2. 清理阶段: 遍历堆,将没有标记的对象清理回收。

耗费时间与堆大小成正比,堆使用效率最高。

就地回收 -> 碎片化问题 -> 分配效率问题

局部优化:

  • 多空闲链表: 不同分块,方便不同大小的分配。空间回收时创建和更新。

  • BiBOP:将堆分为相同大小的块【跳过】

  • 位图标记: 活动对象标记采用位图技术来标记

  • 延迟清除法: 分配空间时进行清除操作,减少最大暂停时间。

现实GC####

垃圾收集器#####

选择垃圾收集器时,需要考虑 新生代收集器与老生代收集的配合使用。

新生代收集器

  • Serial : 单线程, stop the world ; 简单高效,桌面应用场景下,停顿时间可控制在几十毫秒不超过一百毫秒, Client 模式下的默认;

  • ParNew: Serial 的多线程版本,Server 模式下的首选,可以与 CMS 收集器配合使用;

  • Parallel Scavenge: 基于复制算法,多线程; 其目标是达到好的吞吐量,即使“用户代码CPU时间/CPU总耗时”比值更大,吞吐量优先的收集器,适合后台任务。具有自适应调节参数控制,适合新用户使用。

老生代收集器

  • SerialOld: 单线程,基于 标记-清理 算法,Client 模式下的默认。若用于 Server 模式,可以与 收集Parallel Scavenge 搭配使用,以及作为 CMS 的预备(在并发收集发生 Concurrent Mode Failure 时使用)。

  • ParallalOld: 多线程,基于 标记-清理 算法,Server 模式, 可以与 Parallel Scavenge 配合使用,吞吐量及CPU时间敏感型应用。

  • CMS : 并发,基于 标记-清理 算法,目标是获取最短停顿时间,可以与用户线程同时工作;

  • G1:并发,基于 标记-整理 算法,可预测的停顿时间模型,“隐藏级收集器”。

摘录自《深入理解Java虚拟机》(周志明著)

运行参数#####

堆内存

  • -Xms 初始堆大小 ; -Xmx 初始堆大小最大值;

  • -Xmn 新生代(包括Eden和两个Surivior)的堆大小 ;-XX:SurvivorRation=N来调整Eden Space及SurvivorSpace的大小,表示 Eden 与一个 SurvivorSpace 的比值是 N:1

  • -XX:NewRatio=N : 新生代与老年代的比值 1: N , 年轻代的空间占 1/(N+1)

  • -Xss : 每个线程的栈大小

收集器

  • -XX:+UseParNewGC : 使用 ParNew 收集器 ; -XX:+UseParallelOldGC 使用 ParallalOld 收集器;

  • -XX:MaxGCPauseMillis=N : 可接受最大停顿时间,毫秒数 ;-XX:GCTimeRatio=N : 可接受GC时间占比(目标吞吐量), 1 / (N+1), 吞吐量=1-1/(1+N)

  • -XX:+UseConcMarkSweepGC : 使用 CMS 收集器 ; -XX:+UseCMSCompactAtFullCollection :FullGC 后对老年代进行压缩整理,减少碎片化;-XX:+CMSInitiatingOccupancyFraction=80 老年代占用内存 80% 以上时,触发 FullGC。

  • -XX:+UseParallelGC : 并行收集器的线程数

  • -XX:+ DisableExplicitGC : 禁止RMI调用System.gc

  • -XX:PretenureSizeThreshold :大于这个设置值的大对象将直接进入老年代。

    -XX:MaxTenuringThreshold=15 :在 Eden 区出生的对象,经过第一次 MinorGC 之后仍然存活,且被 Surivior 容纳,则年龄记为 1 ; 每经过一次依然能在 Surivior 年龄增长一 ;当到达 XX:MaxTenuringThreshold 指定的值时,就会进入老年代空间。

GC事件#####
  • MinorGC : 大多数情况,新生代对象直接分配在 Eden 区。 当 Eden 区没有足够空间分配时,将发生一次 MinorGC 。 特点是: 频繁,回收快。

  • MajorGC / FullGC: 老年代GC,特点是:很少, 慢。 FullGC 指 MajorGC 中 stop the world 的部分,是需要尽量避免的事件。

  • 大对象触发的 FullGC :大对象,是指需要大量连续内存空间的java对象,例如很长的对象列表。此类对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。

  • promotion failed和concurrent mode failure 触发 FullGC : 采用 CMS 进行老年代 GC,尤其要注意 GC 日志中是否有 promotion failed 和 concurrent mode failure 两种状况,当这两种状况出现时可能会触发 Full GC。promotion failed 是在进行 Minor GC 时,survivor space 放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是 CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。

  • 空间分配担保触发 FullGC: 在进行 MinorGC 之前,虚拟机会检查老年代连续最大可用空间是否大于新生代所有活动对象总大小。如果大于,则可以保证 MinorGC 是安全的;如果不成立,会查看 HandlePromotionFailure 是否允许担保失败;如果可以,则会检查老年代连续最大可用空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则会进行有风险的 MinorGC ;否则,会进行一次 FullGC 。

  • System.gc()方法的调用来建议触发 FullGC 。

GC日志#####
  • GC (Allocaion Failure) : 当在新生代中没有足够空间分配对象时会发生 Allocaion Failure,触发Young GC。 [ParNew: 1887487K->209664K(1887488K), 0.0814271 secs]表示 新生代 ParNew 收集器,GC 前该内存区域使用了 1887487K ,GC 后该内存区域使用了 209664K ,回收了 1677823K , 总容量 1887488K ; 该内存区域 GC 耗时 0.0814271 secs 。 3579779K->2056421K(3984640K), 0.0822273 secs 表示 堆区 GC 前 3579779K, GC 后 2056421K ,回收了 1523358K,GC 耗时 0.0822273 secs 。

  • concurrent mode failure : 一个是在老年代被用完之前不能完成对非活动对象的回收;一个是当新空间分配请求在老年代的剩余空间中不能得到满足。

小结###

线上的服务运行,会遇到各种的突发情况。比如大流量导出,多个大数据对象的订单导出,对于通用的处理措施来说,常常会触发一些潜在的问题,亦能引导人收获一些新知。仅仅是满足功能服务要求是远远不够的。

然而, 反过来思考,为什么总是要到问题发生的时候,才会意识到和去处理呢 ? 是否可以预知和处理问题呢 ? 这涉及到参悟本质: 事物的原理及关联。冥冥之中,因果早已注定,只是很多情况没有达到临界阈值,没有达到诱发条件。

深入理解原理,审视现有的架构设计和实现,预知和解决问题,才是更上一层楼的方式。

参考资料###

记一起Java大对象引起的FullGC事件及GC知识梳理的更多相关文章

  1. [原创]java WEB学习笔记81:Hibernate学习之路--- 对象关系映射文件(.hbm.xml):hibernate-mapping 节点,class节点,id节点(主键生成策略),property节点,在hibernate 中 java类型 与sql类型之间的对应关系,Java 时间和日期类型的映射,Java 大对象类型 的 映射 (了解),映射组成关系

    本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱 ...

  2. 【java】对象变成垃圾被垃圾回收器gc收回前执行的操作:Object类的protected void finalize() throws Throwable

    package 对象被回收前执行的操作; class A{ @Override protected void finalize() throws Throwable { System.out.prin ...

  3. MAT工具分析Dump文件(大对象定位)

    前段时间线上服务经常发生卡顿,经过排查发现是大对象引起的Fullgc问题,特此记录排查逻辑. 目录 目的 一.获得服务进程 二.生成dump文件 三.下载mat工具 四.使用mat工具导入第二步生成的 ...

  4. 如何成为一个牛掰的Java大神?

    一.基础篇 1.1 JVM 1.1.1. Java内存模型,Java内存管理,Java堆和栈,垃圾回收 http://www.jcp.org/en/jsr/detail?id=133http://if ...

  5. jvm对大对象分配内存的特殊处理(转)

    前段日子在和leader交流技术的时候,偶然听到jvm在分配内存空间给大对象时,如果young区空间不足会直接在old区切一块过去.对于这个结论很好奇,也比较怀疑,所以就上网搜了下,发现还真有这么回事 ...

  6. BLOB:大数据,大对象,在数据库中用来存储超长文本的数据,例如图片等

    将一张图片存储在mysql中,并读取出来(BLOB数据:插入BLOB类型的数据必须使用PreparedStatement,因为插入BLOB类型的数据无法使用字符串拼写): -------------- ...

  7. Statement和PreparedStatement的特点 MySQL数据库分页 存取大对象 批处理 获取数据库主键值

    1 Statement和PreparedStatement的特点   a)对于创建和删除表或数据库,我们可以使用executeUpdate(),该方法返回0,表示未影向表中任何记录   b)对于创建和 ...

  8. java 大数据处理之内存溢出解决办法(一)

    http://my.oschina.net/songhongxu/blog/209951 一.内存溢出类型 1.java.lang.OutOfMemoryError: PermGen space JV ...

  9. hibernate 大对象类型hibernate制图

    基础知识: 在 Java 在, java.lang.String 它可以用来表示长串(超过长度 255), 字节数组 byte[] 可用于存放图片或文件的二进制数据. 此外, 在 JDBC API 中 ...

随机推荐

  1. textarea增加字数监听且高度自适应(兼容IE8)

    1.封装方法: var textareaListener = { /*事件监听器兼容 * *attachEvent——兼容:IE7.IE8:不兼容firefox.chrome.IE9.IE10.IE1 ...

  2. pv操作与信号量详解

    对于信号量,可以认为是一个仓库,有两个概念,容量和当前的货物个数. P操作从仓库拿货,如果仓库中没有货,线程一直等待,直到V操作,往仓库里添加了货物,为了避免P操作一直等待下去,会有一个超时时间. V ...

  3. linus 命令

    系统信息 arch 显示机器的处理器架构uname -m 显示机器的处理器架构uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 - (SMBIOS / DMI) h ...

  4. 《Java练习题》进阶练习题(五)

    编程合集: https://www.cnblogs.com/jssj/p/12002760.html 前言:不仅仅要实现,更要提升性能,精益求精,用尽量少的时间复杂度和空间复杂度解决问题. [程序88 ...

  5. minicom配置1500000波特率

    背景 项目需求,得用1500000波特率进行,即1.5M的波特率进行串口通信. 最开始以为minicom不支持,因为第一眼在配置界面的选项中没看见.后来发现其实是支持的 方式一 启动时带参数 -b 1 ...

  6. Ubuntu Server 16.04 LTS上怎样安装下载安装Nginx并启动

    场景 Linux-安装 Ubuntu Server 16.04 X64(图文教程详细版): https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/deta ...

  7. Mapbox轨迹回放

        轨迹回放是webgis中的常见功能,是一种被客户喜闻乐见的GIS动画.     动画是一种短时间内不停重绘达到不断运动的效果.本文中轨迹回放就是事先计算好所需要的点,后面再进行播放.      ...

  8. AbstractMethodError: abstract method "androidx.databinding.ViewDataBinding androidx.databinding.DataBinderMapper.getDataBinder(androidx.databinding.DataBindingComponent, android.view.View, int)"

    混淆导致的数据绑定库错误 问题摘要 AbstractMethodError: abstract method "androidx.databinding.ViewDataBinding an ...

  9. 如何编写一个工程文件夹下通用的Makefile

    新建工程文件夹,在里面新建 bsp.imx6ul.obj 和project 这 3 个文件夹,完成以后如图所示: 新建的工程根目录文件夹 其中 bsp 用来存放驱动文件:imx6ul 用来存放跟芯片有 ...

  10. zuul网关

    Zuul路由网关简介及基本使用 简介 Zuul API路由网关服务简介 请看上图,这里的API 路由网关服务 由Zuul实现,主要就是对外提供服务接口的时候,起到了请求的路由和过滤作用,也因此能够隐藏 ...