You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

161 KiB

JVM

Introduction收纳技术相关的JVM知识等总结

[TOC]

https://mp.weixin.qq.com/s?__biz=MzI4NjI1OTI4Nw==&mid=2247489183&idx=1&sn=02ab3551c473bd2c8429862e3689a94b&chksm=ebdef7a7dca97eb17194c3d935c86ade240d3d96bbeaf036233a712832fb94af07adeafa098b&mpshare=1&scene=23&srcid=0812OQ78gD47QJEguFGixUVa&sharer_sharetime=1628761112690&sharer_shareid=0f9991a2eb945ab493c13ed9bfb8bf4b#rd

Classloader

类加载机制

JVM把描述类的数据加载到内存里面并对数据进行校验、解析和初始化最终变成可以被虚拟机直接使用的class对象。整个生命周期包括加载Loading、验证Verification、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接Linking

JVM类加载机制

类加载过程如下:

  • 加载。加载分为三步:
    • 通过类的全限定性类名获取该类的二进制流
    • 将该二进制流的静态存储结构转为方法区的运行时数据结构
    • 在堆中为该类生成一个class对象
  • 验证验证该class文件中的字节流信息复合虚拟机的要求不会威胁到jvm的安全
  • 准备为class对象的静态变量分配内存初始化其初始值
  • 解析:该阶段主要完成符号引用转化成直接引用
  • 初始化到了初始化阶段才开始执行类中定义的java代码初始化阶段是调用类构造器的过程

类加载器

虚拟机设计团队把加载动作放到 JVM 外部实现以便让应用程序决定如何获取所需的类JVM 提供了 3 种类加载器:

Classloader

  • 启动类加载器(Bootstrap ClassLoader)

    用来加载java核心类库无法被java程序直接引用。负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar的类。

  • 扩展类加载器(Extension ClassLoader)

    用来加载java的扩展库java的虚拟机实现会提供一个扩展库目录该类加载器在扩展库目录里面查找并加载java类。负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。

  • 应用程序类加载器(Application ClassLoader)

    它根据java的类路径来加载类一般来说java应用的类都是通过它来加载的。负责加载用户路径classpath上的类库。JVM 通过双亲委派模型进行类的加载当然我们也可以通过继承java.lang.ClassLoader 实现自定义的类加载器。

  • 自定义类加载器(User ClassLoader)

    由JAVA语言实现继承自ClassLoader。

此外我们比较需要知道的几点:

  • 一个类是由 jvm 加载是通过类加载器+全限定类名确定唯一性的
  • 双亲委派,众所周知,子加载器会尽量委托给父加载器进行加载,父加载器找不到再自己加载
  • 线程上下文类加载,为了满足 spi 等需求突破双亲委派机制,当高层类加载器想加载底层类时通过 Thread.contextClassLoader 来获取当前线程的类加载器(往往是底层类加载器)去加载类

双亲委派

当一个类加载器收到一个类加载的请求,他首先不会尝试自己去加载,而是将这个请求委派给父类加载器去加载,只有父类加载器在自己的搜索范围类查找不到给类时,子加载器才会尝试自己去加载该类。

所有的加载请求都会传送到根加载器去加载,只有当父加载器无法加载时,子类加载器才会去加载:

双亲委派

为什么需要双亲委派模型?

为了防止内存中出现多个相同的字节码。因为如果没有双亲委派的话用户就可以自己定义一个java.lang.String类那么就无法保证类的唯一性。

那怎么打破双亲委派模型?

自定义类加载器继承ClassLoader类重写loadClass方法和findClass方法。

双亲委派模型的作用

  • 避免类的重复加载
  • 保证Java核心类库的安全

JVM

JAVA内存模型

Java虚拟机规范中试图定义一种Java内存模型Java Memory ModelJMM屏蔽掉各种硬件和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。

JAVA内存模型

JMM描述了Java程序中各种变量线程共享变量的访问规则以及在JVM中将变量存储到内存中读取出变量这样的底层细节。所有的变量都存储在主内存中每个线程都有自己独立的工作内存里面保存该线程使用到的变量的副本主内存中变量的一份拷贝。JMM的两条规定

  • 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
  • 不同的线程之间无法直接访问其他线程工作内存中的变量,线程变量值的传递需要通过主内存来完成

Java 内存模型(下文简称 JMM)就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性。这一组规则被称为 Happens-Before, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系

  • 单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作
  • 监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作
  • volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作
  • 线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作
  • 线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前
  • 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C

JMM

JVM内存结构

JVM包含元空间Java虚拟机栈本地方法栈程序计数器等内存区域,其中是占用内存最大的,如下图所示:

JVM架构

JVM常量池

JVM常量池主要分为Class文件常量池、运行时常量池、全局字符串常量池、以及基本类型包装类对象常量池

  • Class文件常量池class文件是一组以字节为单位的二进制数据流在java代码的编译期间我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中其中就包括class文件常量池。
  • 运行时常量池运行时常量池相对于class常量池一大特征就是具有动态性java规范并不要求常量只能在运行时才产生也就是说运行时常量池的内容并不全部来自class常量池在运行时可以通过代码生成常量并将其放入运行时常量池中这种特性被用的最多的就是String.intern()。
  • 全局字符串常量池字符串常量池是JVM所维护的一个字符串实例的引用表在HotSpot VM中它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
  • 基本类型包装类对象常量池java中基本类型的包装类的大部分都实现了常量池技术这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池也即对象不负责创建和管理大于127的这些类的对象。

JVM内存模型

JVM试图定义一种统一的内存模型能将各种底层硬件以及操作系统的内存访问差异进行封装使Java程序在不同硬件以及操作系统上都能达到相同的并发效果。它分为工作内存和主内存,线程无法对主存储器直接进行操作,如果一个线程要和另外一个线程通信,那么只能通过主存进行交换。如下图所示:

JVM内存结构(JDK1.6)

JVM内存结构(JDK1.7)

JVM内存结构(JDK1.8)

线程隔离数据区:

  • 程序计数器: 一块较小的内存空间,存储当前线程所执行的字节码行号指示器
  • 虚拟机栈: 里面的元素叫栈帧,存储 局部变量表、操作栈、动态链接、方法返回地址
  • 本地方法栈: 为虚拟机使用到的本地Native方法服务时的栈帧和虚拟机栈类似

线程共享数据区:

  • 方法区: 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 堆: 唯一目的就是存放对象的实例,是垃圾回收管理器的主要区域
  • 元数据区:常量池、方法元信息、

程序计数器

程序计数器Program Counter Register也叫PC寄存器。程序计数器是一块较小的内存空间可以看作是当前线程所执行的字节码的行号指示器。JVM支持多个线程同时运行每个线程都有自己的程序计数器。倘若当前执行的是 JVM 的方法则该寄存器中保存当前执行指令的地址倘若执行的是native 方法则PC寄存器中为空(undefined)。

  • 当前线程私有
  • 当前线程所执行的字节码的行号指示器
  • 不会出现OutOfMemoryError情况
  • 以一种数据结构的形式放置于内存中

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

JAVA虚拟机栈

JAVA虚拟机栈

JAVA虚拟机栈Java Virtual Machine Stacks是每个线程有一个私有的栈随着线程的创建而创建其生命周期与线程同进同退。栈里面存着的是一种叫“栈帧”的东西每个Java方法在被调用的时候都会创建一个栈帧一旦完成调用则出栈。所有的的栈帧都出栈后线程也就完成了使命。栈帧中存放了局部变量表基本数据类型和对象引用、操作数栈、动态链接(指向当前方法所属的类的运行时常量池的引用等)、方法出口(方法返回地址)、和一些额外的附加信息。栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围会抛出StackOverflowError的错误不过这个深度范围不是一个恒定的值。

  • 线程私有,生命周期与线程相同
  • java方法执行的内存模型每个方法执行的同时都会创建一个栈帧存储局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息
  • StackOverflowError:当线程请求的栈深度大于虚拟机所允许的深度
  • OutOfMemoryError:如果栈的扩展时无法申请到足够的内存

相关参数:

-Xss设置方法栈的最大值

本地方法栈

本地方法栈Native Method Stacks与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的而本地方法栈则是为执行本地方法Native Method服务的。在JVM规范中并没有对本地方法栈的具体实现方法以及数据结构作强制规定虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。

方法区

方法区Method Area用于存放虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • 又称之为非堆Non-Heap或 永久区
  • 线程共享
  • 主要存储类的类型信息、常量池Runtime Constant Pool、字段信息、方法信息、类变量和Class类的引用等
  • Java虚拟机规范规定当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常

相关参数:

-XX:PermSize设置Perm区的初始大小

-XX:MaxPermSize设置Perm区的最大值

堆内存

堆内存JAVA Heap。是被线程共享的一块内存区域创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden区、From Survivor 区和 To Survivor 区)和老年代。

  • 线程共享
  • 主要用于存储JAVA实例或对象
  • GC发生的主要区域
  • 是Java虚拟机所管理的内存中最大的一块
  • 当堆中没有内存能完成实例分配且堆也无法再扩展则会抛出OutOfMemoryError异常

相关参数:

-Xms设置堆内存初始大小

-Xmx设置堆内存最大值

-XX:MaxTenuringThreshold设置对象在新生代中存活的次数

-XX:PretenureSizeThreshold设置超过指定大小的大对象直接分配在旧生代中

新生代相关参数(注意:当新生代设置得太小时,也可能引发大对象直接分配到旧生代):

-Xmn设置新生代内存大小

-XX:SurvivorRatio设置Eden与Survivor空间的大小比例

JVM运行时内存

JVM运行时内存又称堆内存(Heap)。Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

RuntimeDataArea

JVM堆内存划分

当代主流虚拟机Hotspot VM的垃圾回收都采用“分代回收”的算法。“分代回收”是基于这样一个事实对象的生命周期不同所以针对不同生命周期的对象可以采取不同的回收方式以便提高回收效率。Hotspot VM将内存划分为不同的物理区就是“分代”思想的体现。

一个对象从出生到消亡

JVM对象申请空间流程

一个对象产生之后首先进行栈上分配栈上如果分配不下会进入伊甸区伊甸区经过一次垃圾回收之后进入surivivor区survivor区在经过一次垃圾回收之后又进入另外一个survivor与此同时伊甸区的某些对象也跟着进入另外一个survivot什么时候年龄够了就会进入old区这是整个对象的一个逻辑上的移动过程。

新生代Young Generation

主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。

  • Eden区Java新对象的出生地如果新创建的对象占用内存很大则直接分配到老年代。当Eden区内存不够的时候就会触发MinorGC对新生代区进行一次垃圾回收
  • ServivorTo保留了一次MinorGC过程中的幸存者
  • ServivorFrom上一次GC的幸存者作为这一次GC的被扫描者

MinorGC流程

  • MinorGC采用复制算法
  • 首先把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域如果有对象的年龄以及达到了老年的标准则复制到老年代区同时把这些对象的年龄+1如果ServicorTo不够位置了就放到老年区
  • 然后清空Eden和ServicorFrom中的对象
  • 最后ServicorTo和ServicorFrom互换原ServicorTo成为下一次GC时的ServicorFrom区

为什么 Survivor 分区不能是 0 个?

如果 Survivor 是 0 的话,也就是说新生代只有一个 Eden 分区,每次垃圾回收之后,存活的对象都会进入老生代,这样老生代的内存空间很快就被占满了,从而触发最耗时的 Full GC ,显然这样的收集器的效率是我们完全不能接受的。

为什么 Survivor 分区不能是 1 个?

如果 Survivor 分区是 1 个的话,假设我们把两个区域分为 1:1那么任何时候都有一半的内存空间是闲置的显然空间利用率太低不是最佳的方案。

但如果设置内存空间的比例是 8:2 ,只是看起来似乎“很好”,假设新生代的内存为 100 MB Survivor 大小为 20 MB ),现在有 70 MB 对象进行垃圾回收之后,剩余活跃的对象为 15 MB 进入 Survivor 区,这个时候新生代可用的内存空间只剩了 5 MB这样很快又要进行垃圾回收操作显然这种垃圾回收器最大的问题就在于需要频繁进行垃圾回收。

为什么 Survivor 分区是 2 个?

如果Survivor分区有2个分区我们就可以把 Eden、From Survivor、To Survivor 分区内存比例设置为 8:1:1 ,那么任何时候新生代内存的利用率都 90% 这样空间利用率基本是符合预期的。再者就是虚拟机的大部分对象都符合“朝生夕死”的特性所以每次新对象的产生都在空间占比比较大的Eden区垃圾回收之后再把存活的对象方法存入Survivor区如果是 Survivor区存活的对象那么“年龄”就+1当年龄增长到15可通过 -XX:+MaxTenuringThreshold 设定)对象就升级到老生代。

总结

根据上面的分析可以得知,当新生代的 Survivor 分区为 2 个的时候,不论是空间利用率还是程序运行的效率都是最优的,所以这也是为什么 Survivor 分区是 2 个的原因了。

老年代Old Generation

主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC使得有新生代的对象晋身入老年代导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

MajorGC流程

MajorGC采用标记—清除算法。首先扫描一次所有老年代标记出存活的对象然后回收没有标记的对象。MajorGC的耗时比较长因为要扫描再回收。MajorGC会产生内存碎片为了减少内存损耗我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候就会抛出OOMOut of Memory异常。

永久区Perm Generation

指内存的永久保存区域,主要存放元数据例如Class、Method的元信息与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说该区域的划分对垃圾回收影响比较小。GC不会在主程序运行期对永久区域进行清理所以这也导致了永久代的区域会随着加载的Class的增多而胀满最终抛出OOM异常。

JAVA8与元数据

在Java8中永久代已经被移除被一个称为“元数据区”元空间的区域所取代。元空间的本质和永久代类似都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于元空间并不在虚拟机中,而是使用本地内存。因此默认情况下元空间的大小仅受本地内存限制。类的元数据放入Native Memory字符串池和类的静态变量放入java堆中这样可以加载多少类的元数据就不再由MaxPermSize控制而由系统的实际可用空间来控制。

内存分配策略

堆内存常见的分配测试如下:

  • 对象优先在Eden区分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
参数 说明信息
-Xms 初始堆大小。如:-Xms256m
-Xmx 最大堆大小。如:-Xmx512m
-Xmn 新生代大小。通常为Xmx的1/3或1/4。新生代=Eden+2个Survivor空间。实际可用空间为=Eden+1个Survivor即 90%
-Xss JDK1.5+每个线程堆栈大小为 1M一般来说如果栈不是很深的话 1M 是绝对够用了的
-XX:NewRatio 新生代与老年代的比例。如XX:NewRatio=2则新生代占整个堆空间的1/3老年代占2/3
-XX:SurvivorRatio 新生代中Eden与Survivor的比值。默认值为 8即Eden占新生代空间的8/10另外两个Survivor各占1/10
-XX:PermSize 永久代(方法区)的初始大小
-XX:MaxPermSize 永久代(方法区)的最大值
-XX:+PrintGCDetails 打印GC信息
-XX:+HeapDumpOnOutOfMemoryError 让虚拟机在发生内存溢出时Dump出当前的内存堆转储快照以便分析用

参数基本策略

各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小分析活跃数据的大小是很好的切入点。

活跃数据的大小指应用程序稳定运行时长期存活对象在堆中占用的空间大小即Full GC后堆中老年代占用空间的大小。

可以通过GC日志中Full GC之后老年代数据大小得出比较准确的方法是在程序稳定后多次获取GC数据通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下

空间 倍数
总大小 3-4 倍活跃数据的大小
新生代 1-1.5 活跃数据的大小
老年代 2-3 倍活跃数据的大小
永久代 1.2-1.5 倍Full GC后的永久代空间占用

例如根据GC日志获得老年代的活跃数据大小为300M那么各分区大小可以设为

总堆1200MB = 300MB × 4

新生代450MB = 300MB × 1.5

老年代: 750MB = 1200MB - 450MB

这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。

引用级别

Java中4种引用的级别和强度由高到低依次为强引用→软引用→弱引用→虚引用

垃圾回收器回收时,某些对象会被回收,某些不会被回收。垃圾回收器会从根对象Object标记存活的对象,然后将某些不可达的对象和一些引用的对象进行回收。如下所示:

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 当内存不足时 对象缓存 内存不足时终止
弱引用 正常垃圾回收时 对象缓存 垃圾回收后终止
虚引用 正常垃圾回收时 跟踪对象的垃圾回收 垃圾回收后终止

引用级别

强引用StrongReference

强引用是我们最常见的对象它属于不可回收资源垃圾回收器后面简称GC绝对不会回收它即使是内存不足JVM宁愿抛出 OutOfMemoryError 异常,使程序终止,也不会来回收强引用对象。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:

Object strongReference = new Object();

内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用对象来解决内存不足的问题。 如果强引用对象不使用时,需要弱化从而使GC能够回收,如下:

strongReference = null;

显式地设置strongReference对象为null,或让其超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于GC算法。

public void test() {
	Object strongReference = new Object();
	// 省略其他操作
}

在一个方法的内部有一个强引用,这个引用保存在Java中,而真正的引用内容(Object)保存在Java中。 当这个方法运行完成后,就会退出方法栈,则引用对象的引用数0,这个对象会被回收。但是如果这个strongReference全局变量时,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。

软引用SoftReference

如果一个对象只具有软引用,则内存空间充足时,垃圾回收器不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

// 强引用
String strongReference = new String("abc");
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);

软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);

注意软引用对象是在jvm内存不够时才会被回收我们调用System.gc()方法只是起通知作用JVM什么时候扫描回收对象是JVM自己的状态决定的。就算扫描到软引用对象也不一定会回收它只有内存不够的时候才会回收。

垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象,而虚拟机会尽可能优先回收长时间闲置不用软引用对象。对那些刚构建的或刚使用过的**"较新的"软对象会被虚拟机尽可能保留**,这就是引入引用队列ReferenceQueue的原因。

弱引用WeakReference

弱引用对象相对软引用对象具有更短暂的生命周期,只要 GC 发现它仅有弱引用,不管内存空间是否充足,都会回收它,不过 GC 是一个优先级很低的线程,因此不一定会很快发现那些仅有弱引用的对象。

弱引用软引用的区别在于:只具有弱引用的对象拥有更短暂生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定很快发现那些只具有弱引用的对象。

String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
str = null;

JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:

str = null;
System.gc();

注意:如果一个对象是偶尔(很少)的使用并且希望在使用时随时就能获取到但又不想影响此对象的垃圾收集那么你应该用Weak Reference来记住此对象。

下面的代码会让一个弱引用再次变为一个强引用

String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
// 弱引用转强引用
String strongReference = weakReference.get();

同样,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象垃圾回收Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用PhantomReference

虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

应用场景:

虚引用主要用来跟踪对象被垃圾回收器回收的活动。 虚引用软引用弱引用的一个区别在于:

虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

OOM

JVM发生OOM的九种场景如下

场景一Java heap space

当堆内存Heap Space没有足够空间存放新创建的对象时就会抛出 java.lang.OutOfMemoryError:Javaheap space 错误(根据实际生产经验,可以对程序日志中的 OutOfMemoryError 配置关键字告警,一经发现,立即处理)。

原因分析

Javaheap space 错误产生的常见原因可以分为以下几类:

  • 请求创建一个超大对象,通常是一个大数组
  • 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值
  • 过度使用终结器Finalizer该对象没有立即被 GC
  • 内存泄漏Memory Leak大量对象引用没有释放JVM 无法对其自动回收,常见于使用了 File 等资源没有回收

解决方案

针对大部分情况,通常只需通过 -Xmx 参数调高 JVM 堆内存空间即可。如果仍然没有解决,可参考以下情况做进一步处理:

  • 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制
  • 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级
  • 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接

场景二GC overhead limit exceeded

当 Java 进程花费 98% 以上的时间执行 GC但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出 java.lang.OutOfMemoryError:GC overhead limit exceeded 错误。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。

此类问题的原因与解决方案跟 Javaheap space 非常类似,可以参考上文。

场景三Permgen space

该错误表示永久代Permanent Generation已用满通常是因为加载的 class 数目太多或体积太大。

原因分析

永久代存储对象主要包括以下几类:

  • 加载/缓存到内存中的 class 定义,包括类的名称,字段,方法和字节码
  • 常量池
  • 对象数组/类型数组所关联的 class
  • JIT 编译器优化后的 class 信息

PermGen 的使用量与加载到内存的 class 的数量/大小正相关。

解决方案

根据 Permgen space 报错的时机,可以采用不同的解决方案,如下所示:

  • 程序启动报错,修改 -XX:MaxPermSize 启动参数,调大永久代空间
  • 应用重新部署时报错,很可能是没有应用没有重启,导致加载了多份 class 信息,只需重启 JVM 即可解决
  • 运行时报错,应用程序可能会动态创建大量 class而这些 class 的生命周期很短暂,但是 JVM 默认不会卸载 class可以设置 -XX:+CMSClassUnloadingEnabled-XX:+UseConcMarkSweepGC 这两个参数允许 JVM 卸载 class。

如果上述方法无法解决,可以通过 jmap 命令 dump 内存对象 jmap-dump:format=b,file=dump.hprof<process-id> ,然后利用 Eclipse MAT https://www.eclipse.org/mat 功能逐一分析开销最大的 classloader 和重复 class。

场景四Metaspace

JDK 1.8 使用 Metaspace 替换了永久代Permanent Generation该错误表示 Metaspace 已被用满,通常是因为加载的 class 数目太多或体积太大。

此类问题的原因与解决方法跟 Permgenspace 非常类似,可以参考上文。需要特别注意的是调整 Metaspace 空间大小的启动参数为 -XX:MaxMetaspaceSize

场景五Unable to create new native thread

每个 Java 线程都需要占用一定的内存空间,当 JVM 向底层操作系统请求创建一个新的 native 线程时,如果没有足够的资源分配就会报此类错误。

原因分析

JVM 向 OS 请求创建 native 线程失败,就会抛出 Unableto createnewnativethread,常见的原因包括以下几类:

  • 线程数超过操作系统最大线程数 ulimit 限制
  • 线程数超过 kernel.pid_max只能重启
  • native 内存不足

该问题发生的常见过程主要包括以下几步:

  • JVM 内部的应用程序请求创建一个新的 Java 线程
  • JVM native 方法代理了该次请求,并向操作系统请求创建一个 native 线程
  • 操作系统尝试创建一个新的 native 线程,并为其分配内存
  • 如果操作系统的虚拟内存已耗尽,或是受到 32 位进程的地址空间限制,操作系统就会拒绝本次 native 内存分配
  • JVM 将抛出 java.lang.OutOfMemoryError:Unableto createnewnativethread错误

解决方案

  • 升级配置,为机器提供更多的内存
  • 降低 Java Heap Space 大小
  • 修复应用程序的线程泄漏问题
  • 限制线程池大小
  • 使用 -Xss 参数减少线程栈的大小
  • 调高 OS 层面的线程最大数:执行 ulimia-a 查看最大线程数限制,使用 ulimit-u xxx 调整最大线程数限制

场景六Out of swap space

该错误表示所有可用的虚拟内存已被耗尽。虚拟内存Virtual Memory由物理内存Physical Memory和交换空间Swap Space两部分组成。当运行时程序请求的虚拟内存溢出时就会报 Outof swap space? 错误。

原因分析

该错误出现的常见原因包括以下几类:

  • 地址空间不足
  • 物理内存已耗光
  • 应用程序的本地内存泄漏native leak例如不断申请本地内存却不释放
  • 执行 jmap-histo:live<pid> 命令,强制执行 Full GC如果几次执行后内存明显下降则基本确认为 Direct ByteBuffer 问题

解决方案

根据错误原因可以采取如下解决方案:

  • 升级地址空间为 64 bit
  • 使用 Arthas 检查是否为 Inflater/Deflater 解压缩问题,如果是,则显式调用 end 方法
  • Direct ByteBuffer 问题可以通过启动参数 -XX:MaxDirectMemorySize 调低阈值
  • 升级服务器配置/隔离部署,避免争用

场景七Kill process or sacrifice child

有一种内核作业Kernel Job名为 Out of Memory Killer它会在可用内存极低的情况下“杀死”kill某些进程。OOM Killer 会对所有进程进行打分,然后将评分较低的进程“杀死”,具体的评分规则可以参考 Surviving the Linux OOM Killer。不同于其它OOM错误 Killprocessorsacrifice child 错误不是由 JVM 层面触发的,而是由操作系统层面触发的。

原因分析

默认情况下Linux 内核允许进程申请的内存总量大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源。然而,这种方式也会无可避免地带来一定的“超卖”风险。例如某些进程持续占用系统内存,然后导致其他进程没有可用内存。此时,系统将自动激活 OOM Killer寻找评分低的进程并将其“杀死”释放内存资源。

解决方案

  • 升级服务器配置/隔离部署,避免争用
  • OOM Killer 调优

场景八Requested array size exceeds VM limit

JVM 限制了数组的最大长度该错误表示程序请求创建的数组超过最大长度限制。JVM 在为数组分配内存前,会检查要分配的数据结构在系统中是否可寻址,通常为 Integer.MAX_VALUE-2

此类问题比较罕见,通常需要检查代码,确认业务是否需要创建如此大的数组,是否可以拆分为多个块,分批执行。

场景九Direct buffer memory

Java 允许应用程序通过 Direct ByteBuffer 直接访问堆外内存,许多高性能程序通过 Direct ByteBuffer 结合内存映射文件Memory Mapped File实现高速 IO。

原因分析

Direct ByteBuffer 的默认大小为 64 MB一旦使用超出限制就会抛出 Directbuffer memory 错误。

解决方案

  • Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer因此可以通过 Arthas 等在线诊断工具拦截该方法进行排查
  • 检查是否直接或间接使用了 NIO如 nettyjetty 等
  • 通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值
  • 检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc() 失效
  • 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleanerclean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间
  • 内存容量确实不足,升级配置

最佳实践

① OOM发生时输出堆dump

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_HOME/logs

② OOM发生后的执行动作

-XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh

-XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh

OOM之后除了保留堆dump外根据管理策略选择合适的运行脚本。

GC

寻找垃圾算法

寻找垃圾算法

引用计数法

引用计数法Reference Count会给对象中添加一个引用计数器每当有一个地方引用它的时候计数器的值就 +1 ,当引用失效时,计数器值就 -1 ,计数器的值为 0 的对象不可能在被使用,这个时候就可以判定这个对象是垃圾。

引用计数法

当图中的数值变成0时这个时候使用引用计数算法就可以判定它是垃圾了但是引用计数法不能解决一个问题就是当对象是循环引用的时候计数器值都不为0这个时候引用计数器无法通知GC收集器来回收他们如下图所示

引用计数法-问题

这个时候就需要使用到我们的根可达算法。

可达性分析

根可达算法Root Searching的意思是说从根上开始搜索当一个程序启动后马上需要的那些个对象就叫做根对象所谓的根可达算法就是首先找到根对象然后跟着这根线一直往外找到那些有用的。常见的GC roots如下

  • 线程栈变量: 线程里面会有线程栈和main栈帧从这个main() 里面开始的这些对象都是我们的根对象

  • 静态变量: 一个class 它有一个静态的变量load到内存之后马上就得对静态变量进行初始化所以静态变量到的对象这个叫做根对象

  • 常量池: 如果你这个class会用到其他的class的那些个类的对象这些就是根对象

  • JNI 如果我们调用了 C和C++ 写的那些本地方法所用到的那些个类或者对象

根可达算法

图中的 object5 和object6 虽然他们之间互相引用了但是从根找不到它所以就是垃圾而object8没有任何引用自然而然也是垃圾其他的Object对象都有可以从根找到的所以是有用的不会被垃圾回收掉。

GC Root

GC Roots 是一组必须活跃的引用。用通俗的话来说就是程序接下来通过直接引用或者间接引用能够访问到的潜在被使用的对象。GC Roots 包括:

  • Java 线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我们栈帧相关的各种引用
  • 所有当前被加载的 Java 类
  • Java 类的引用类型静态变量
  • 运行时常量池里的引用类型常量String 或 Class 类型)
  • JVM 内部数据结构的一些引用,比如 sun.jvm.hotspot.memory.Universe 类
  • 用于同步的监控对象,比如调用了对象的 wait() 方法
  • JNI handles包括 global handles 和 local handles

GC Roots 大体可以分为三大类:

  • 活动线程相关的各种引用
  • 类的静态变量的引用
  • JNI 引用

清理垃圾算法

清理垃圾算法又叫内存回收算法。

标记Mark

垃圾回收的第一步,就是找出活跃的对象。根据 GC Roots 遍历所有的可达对象,这个过程,就叫作标记。

标记(Mark)

如图所示,圆圈代表的是对象。绿色的代表 GC Roots红色的代表可以追溯到的对象。可以看到标记之后仍然有多个灰色的圆圈它们都是被回收的对象。

清除Sweep

清除阶段就是把未被标记的对象回收掉。

清除(Sweep)

但是这种简单的清除方式,有一个明显的弊端,那就是碎片问题。比如我申请了 1k、2k、3k、4k、5k 的内存。

清除(Sweep)-内存

由于某种原因 2k 和 4k 的内存,我不再使用,就需要交给垃圾回收器回收。

清除(Sweep)-回收

这个时候,我应该有足足 6k 的空闲空间。接下来,我打算申请另外一个 5k 的空间,结果系统告诉我内存不足了。系统运行时间越长,这种碎片就越多。在很久之前使用 Windows 系统时,有一个非常有用的功能,就是内存整理和磁盘整理,运行之后有可能会显著提高系统性能。这个出发点是一样的。

复制Copying

复制(Copying)算法

优点

  • 因为是对整个半区进行内存回收,内存分配时不用考虑内存碎片等情况。实现简单,效率较高

不足之处

  • 既然要复制,需要提前预留内存空间,有一定的浪费
  • 在对象存活率较高时,需要复制的对象较多,效率将会变低

整理Compact

其实,不用分配一个对等的额外空间,也是可以完成内存的整理工作。可以把内存想象成一个非常大的数组,根据随机的 index 删除了一些数据。那么对整个数组的清理,其实是不需要另外一个数组来进行支持的,使用程序就可以实现。它的主要思路,就是移动所有存活的对象,且按照内存地址顺序依次排列,然后将末端内存地址以后的内存全部回收。

整理(Compact)

但是需要注意,这只是一个理想状态。对象的引用关系一般都是非常复杂的,我们这里不对具体的算法进行描述。你只需要了解,从效率上来说,一般整理算法是要低于复制算法的。

扩展回收算法

目前JVM的垃圾回收器都是对几种朴素算法的发扬光大没有最优的算法只有最合适的算法

  • 复制算法Copying:复制算法是所有算法里面效率最高的,缺点是会造成一定的空间浪费
  • 标记-清除Mark-Sweep:效率一般,缺点是会造成内存碎片问题
  • 标记-整理Mark-Compact:效率比前两者要差,但没有空间浪费,也消除了内存碎片问题

收集算法

标记清除Mark-Sweep

标记清除(Mark-Sweep)算法

首先从 GC Root 开始遍历对象图并标记Mark所遇到的每个对象标识出所有要回收的对象。然后回收器检查堆中每一个对象并将所有未被标记的对象进行回收。

不足之处

  • 标记、清除的效率都不高
  • 清除后产生大量的内存碎片,空间碎片太多会导致在分配大对象时无法找到足够大的连续内存,从而不得不触发另一次垃圾回收动作

标记整理Mark-Compact

标记整理(Mark-Compact)算法

与标记清除算法类似,但不是在标记完成后对可回收对象进行清理,而是将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

优点

  • 消除了标记清除导致的内存分散问题,也消除了复制算法中内存减半的高额代价

不足之处

  • 效率低下,需要标记所有存活对象,还要标记所有存活对象的引用地址。效率上低于复制算法

分代收集Generational Collection

研究表明大部分对象可以分为两类:

  • 大部分对象的生命周期都很短
  • 其他对象则很可能会存活很长时间

根据对象存活周期的不同将内存划分为几块。对不同周期的对象采取不同的收集算法:

  • 新生代:每次垃圾收集会有大批对象回收,所以采取复制算法
  • 老年代:对象存活率高,采取标记清理或者标记整理算法

① 年轻代Young Generation

年轻代使用的垃圾回收算法是复制算法。因为年轻代发生 GC 后,只会有非常少的对象存活,复制这部分对象是非常高效的。但复制算法会造成一定的空间浪费,所以年轻代中间也会分很多区域。

年轻代

如图所示,年轻代分为:1个伊甸园空间Eden 2个幸存者空间Survivor 。当年轻代中的 Eden 区分配满的时候,就会触发年轻代的 GCMinor GC。具体过程如下

  • 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区以下简称from
  • Eden 区再次 GC这时会采用复制算法将 Eden 和 from 区一起清理。存活的对象会被复制到 to 区,然后只需要清空 from 区就可以了

在这个过程中总会有1个 Survivor 分区是空置的。Eden、from、to 的默认比例是 8:1:1所以只会造成 10% 的空间浪费。这个比例,是由参数 -XX:SurvivorRatio 进行配置的(默认为 8

② 老年代Old/Tenured Generation

老年代一般使用“标记-清除”、“标记-整理”算法,因为老年代的对象存活率一般是比较高的,空间又比较大,拷贝起来并不划算,还不如采取就地收集的方式。对象进入老年代的途径如下:

  • 提升Promotion

    如果对象够老,会通过“提升”进入老年代

  • 分配担保

    年轻代回收后存活的对象大于10%时因Survivor空间不够存储对象就会直接在老年代上分配

  • 大对象直接在老年代分配

    超出某个大小的对象将直接在老年代分配

  • 动态对象年龄判定

    有的垃圾回收算法,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法。

    比如,如果幸存区中相同年龄对象大小的和,大于幸存区的一半,大于或等于 age 的对象将会直接进入老年代。

分区收集

GC垃圾收集器

收集器

GC垃圾收集器的JVM配置参数

  • -XX:+UseSerialGC:年轻代和老年代都用串行收集器
  • -XX:+UseParNewGC:年轻代使用 ParNew老年代使用 Serial Old
  • -XX:+UseParallelGC:年轻代使用 ParallerGC老年代使用 Serial Old
  • -XX:+UseParallelOldGC:新生代和老年代都使用并行收集器
  • -XX:+UseConcMarkSweepGC:表示年轻代使用 ParNew老年代的用 CMS
  • -XX:+UseG1GC:使用 G1垃圾回收器
  • -XX:+UseZGC:使用 ZGC 垃圾回收器

年轻代收集器

Serial收集器

处理GC的只有一条线程并且在垃圾回收的过程中暂停一切用户线程。最简单的垃圾回收器但千万别以为它没有用武之地。因为简单所以高效它通常用在客户端应用上。因为客户端应用不会频繁创建很多对象用户也不会感觉出明显的卡顿。相反它使用的资源更少也更轻量级。

Serial收集器

ParNew收集器

ParNew是Serial的多线程版本。由多条GC线程并行地进行垃圾清理。清理过程依然要停止用户线程。ParNew 追求“低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial。

ParNew收集器

Parallel Scavenge收集器

另一个多线程版本的垃圾回收器。它与ParNew的主要区别是

  • Parallel Scavenge追求CPU吞吐量能够在较短时间完成任务适合没有交互的后台计算。弱交互强计算
  • ParNew:追求降低用户停顿时间,适合交互式应用。强交互弱计算

老年代收集器

Serial Old收集器

与年轻代的 Serial 垃圾收集器对应,都是单线程版本,同样适合客户端使用。年轻代的 Serial使用复制算法。老年代的 Old Serial使用标记-整理算法。

SerialOld收集器

Parallel Old收集器

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。

ParallelOld收集器

CMS收集器

并发标记清除(Concurrent Mark Sweep,CMS)垃圾回收器,是一款致力于获取最短停顿时间的收集器,使用多个线程来扫描堆内存并标记可被清除的对象,然后清除标记的对象。在下面两种情形下会暂停工作线程:

  • 在老年代中标记引用对象的时候

  • 在做垃圾回收的过程中堆内存中有变化发生

对比与并行垃圾回收器CMS回收器使用更多的CPU来保证更高的吞吐量。如果我们可以有更多的CPU用来提升性能那么CMS垃圾回收器是比并行回收器更好的选择。使用 -XX:+UseParNewGCJVM 参数来开启使用CMS垃圾回收器。

CMS收集器

主要流程如下

  • 初始标记(CMS initial mark)仅标记出GC Roots能直接关联到的对象。需要Stop-the-world
  • 并发标记(CMS concurrenr mark)进行GC Roots遍历的过程寻找出所有可达对象
  • 重新标记(CMS remark)修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。需要Stop-the-world
  • 并发清除(CMS concurrent sweep):清出垃圾

CMS触发机制当老年代的使用率达到80%时就会触发一次CMS GC。

  • -XX:CMSInitiatingOccupancyFraction=80
  • -XX:+UseCMSInitiatingOccupancyOnly

优点

  • 并发收集
  • 停顿时间最短

缺点

  • 并发回收导致CPU资源紧张

    在并发阶段它虽然不会导致用户线程停顿但却会因为占用了一部分线程而导致应用程序变慢降低程序总吞吐量。CMS默认启动的回收线程数是CPU核数 + 3/ 4当CPU核数不足四个时CMS对用户程序的影响就可能变得很大。

  • 无法清理浮动垃圾

    在CMS的并发标记和并发清理阶段用户线程还在继续运行就还会伴随有新的垃圾对象不断产生但这一部分垃圾对象是出现在标记过程结束以后CMS无法在当次收集中处理掉它们只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。

  • 并发失败Concurrent Mode Failure

    由于在垃圾回收阶段用户线程还在并发运行那就还需要预留足够的内存空间提供给用户线程使用因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收必须预留一部分空间供并发回收时的程序运行使用。默认情况下当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX**:** CMSInitiatingOccupancyFraction 参数来设置。

    这里会有一个风险要是CMS运行期间预留的内存无法满足程序分配新对象的需要就会出现一次“并发失败”Concurrent Mode Failure这时候虚拟机将不得不启动后备预案Stop The World临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。

  • 内存碎片问题

    CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。

    为了解决这个问题CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数 -XX:CMSFullGCsBeforeCompaction这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理默认值为0表示每次进入 Full GC 时都进行碎片整理)。

作用内存区域:老年代

适用场景:对停顿时间敏感的场合

算法类型:标记-清除

新生代和老年代收集

G1收集器

G1Garbage First回收器采用面向局部收集的设计思路和基于Region的内存布局形式是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器但从局部两个Region之间上看又是基于 标记-复制 算法实现的。G1 回收过程G1 回收器的运作过程大致可分为四个步骤:

  • 初始标记会STW:仅仅只是标记一下 GC Roots 能直接关联到的对象并且修改TAMS指针的值让下一阶段用户线程并发运行时能正确地在可用的Region中分配新对象。这个阶段需要停顿线程但耗时很短而且是借用进行Minor GC的时候同步完成的所以G1收集器在这个阶段实际并没有额外的停顿。
  • 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
  • 最终标记会STW:对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。
  • 清理阶段会STW更新Region的统计数据对各个Region的回收价值和成本进行排序根据用户所期望的停顿时间来制定回收计划可以自由选择任意多个Region构成回收集然后把决定回收的那一部分Region的存活对象复制到空的Region中再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动必须暂停用户线程由多条回收器线程并行完成的。

G1收集器中的堆内存被划分为多个大小相等的内存块Region每个Region是逻辑连续的一段内存结构如下

G1-Region

每个Region被标记了E、S、O和H说明每个Region在运行时都充当了一种角色其中H是以往算法中没有的它代表Humongous巨大的这表示这些Region存储的是巨型对象humongous objectH-obj当新建对象大小超过Region大小一半时直接在新的一个或多个连续Region中分配并标记为H。

Region

堆内存中一个Region的大小可以通过 -XX:G1HeapRegionSize参数指定大小区间只能是1M、2M、4M、8M、16M和32M总之是2的幂次方。如果G1HeapRegionSize为默认值则在堆初始化时计算Region的实际大小默认把堆内存按照2048份均分最后得到一个合理的大小。

GC模式

  • young gc

    发生在年轻代的GC算法一般对象除了巨型对象都是在eden region中分配内存当所有eden region被耗尽无法申请内存时就会触发一次young gc这种触发机制和之前的young gc差不多执行完一次young gc活跃对象会被拷贝到survivor region或者晋升到old region中空闲的region会被放入空闲列表中等待下次被使用。

    • -XX:MaxGCPauseMillis设置G1收集过程目标时间默认值200ms
    • -XX:G1NewSizePercent:新生代最小值,默认值5%
    • -XX:G1MaxNewSizePercent:新生代最大值,默认值60%
  • mixed gc

    当越来越多的对象晋升到老年代old region时为了避免堆内存被耗尽虚拟机会触发一个混合的垃圾收集器即mixed gc该算法并不是一个old gc除了回收整个young region还会回收一部分的old region这里需要注意是一部分老年代,而不是全部老年代可以选择哪些old region进行收集从而可以对垃圾回收的耗时时间进行控制

  • full gc

    • 如果对象内存分配速度过快mixed gc来不及回收导致老年代被填满就会触发一次full gcG1的full gc算法就是单线程执行的serial old gc会导致异常长时间的暂停时间需要进行不断的调优尽可能的避免full gc

G1垃圾回收器 应用于大的堆内存空间。它将堆内存空间划分为不同的区域对各个区域并行地做回收工作。G1在回收内存空间后还立即对空闲空间做整合工作以减少碎片。CMS却是在全部停止(stop the world,STW)时执行内存整合工作。对于不同的区域G1根据垃圾的数量决定优先级。使用 -XX:UseG1GCJVM 参数来开启使用G1垃圾回收器。

G1收集器

主要流程如下

  • 初始标记(Initial Marking)标记从GC Root可达的对象。会发生STW
  • 并发标记(Concurrenr Marking)标记出GC Root可达对象衍生出去的存活对象并收集各个Region的存活对象信息。整个过程gc collector线程与应用线程可以并行执行
  • 最终标记(Final Marking)标记出在并发标记过程中遗漏的或内部引用发生变化的对象。会发生STW
  • 筛选回收(Live Data Counting And Evacution)垃圾清除过程如果发现一个Region中没有存活对象则把该Region加入到空闲列表中

-XX:InitiatingHeapOccupancyPercent当老年代大小占整个堆大小百分比达到该阈值时会触发一次mixed gc。

优点

  • 并行与并发,充分发挥多核优势
  • 分代收集,所以不需要与其它收集器配合即可工作
  • 空间整合,整体来看基于”标记-整理算法“,局部采用”复制算法“都不会产生内存碎片
  • 可以指定GC最大停顿时长

缺点

  • 需要记忆集来记录新生代和老年代之间的引用关系
  • 需要占用大量的内存可能达到整个堆内存容量的20%甚至更多

作用内存区域:跨代

适用场景:作为关注停顿时间的场景的收集器备选方案

算法类型:整体来看基于”标记-整理算法“,局部采用"复制算法"

ZGC收集器

Z Garbage Collector简称 ZGC是 JDK 11 中新加入的尚在实验阶段的低延迟垃圾收集器。它和 Shenandoah 同属于超低延迟的垃圾收集器,但在吞吐量上比 Shenandoah 有更优秀的表现,甚至超过了 G1接近了“吞吐量优先”的 Parallel 收集器组合,可以说近乎实现了“鱼与熊掌兼得”。

与CMS中的ParNew和G1类似ZGC也采用标记-复制算法不过ZGC对该算法做了重大改进ZGC在标记、转移和重定位阶段几乎都是并发的这是ZGC实现停顿时间小于10ms目标的最关键原因。ZGC垃圾回收周期如下图所示

ZGC收集器

ZGC只有三个STW阶段初始标记再标记初始转移。其中初始标记和初始转移分别都只需要扫描所有GC Roots其处理时间和GC Roots的数量成正比一般情况耗时非常短再标记阶段STW时间很短最多1ms超过1ms则再次进入并发标记阶段。即ZGC几乎所有暂停都只依赖于GC Roots集合大小停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比G1的转移阶段完全STW的且停顿时间随存活对象的大小增加而增加。

ZGC 的内存布局

与 Shenandoah 和 G1 一样ZGC 也采用基于 Region 的堆内存布局,但与它们不同的是, ZGC 的 Region 具有动态性,也就是可以动态创建和销毁,容量大小也是动态的,有大、中、小三类容量:

ZGC内存布局

  • 小型 Region (Small Region):容量固定为 2MB用于放置小于 256KB 的小对象
  • 中型 Region (M edium Region):容量固定为 32MB用于放置大于等于 256KB 但小于 4MB 的对象
  • 大型 Region (Large Region):容量不固定,可以动态变化,但必须为 2MB 的整数倍,用于放置 4MB 或以上的大对象。每个大型 Region 中只会存放一个大对象,这也预示着虽然名字叫作“大型 Region”但它的实际容量完全有可能小于中型 Region最小容量可低至 4MB

在 JDK 11 及以上版本,可以通过以下参数开启 ZGC-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

Shenandoah收集器

Shenandoah 与 G1 有很多相似之处,比如都是基于 Region 的内存布局,都有用于存放大对象的 Humongous Region默认回收策略也是优先处理回收价值最大的 Region。不过也有三个重大的区别

  • Shenandoah支持并发的整理算法G1整理阶段虽是多线程并行但无法与用户程序并发执行
  • 默认不使用分代收集理论
  • 使用连接矩阵 (Connection Matrix)记录跨Region的引用关系替换掉了G1中的记忆级(Remembered Set),内存和计算成本更低

Shenandoah 收集器的工作原理相比 G1 要复杂不少,其运行流程示意图如下:

Shenandoah收集器运行流程

可见Shenandoah的并发程度明显比G1更高只需要在初始标记、最终标记、初始引用更新和最终引用更新这几个阶段进行短暂的“Stop The World”其他阶段皆可与用户程序并发执行其中最重要的并发标记、并发回收和并发引用更新详情如下

  • 并发标记( Concurrent Marking)
  • 并发回收( Concurrent Evacuation)
  • 并发引用更新( Concurrent Update Reference)

Shenandoah 的高并发度让它实现了超低的停顿时间,但是更高的复杂度也伴随着更高的系统开销,这在一定程度上会影响吞吐量,下图是 Shenandoah 与之前各种收集器在停顿时间维度和系统开销维度上的对比:

收集器停顿时间和系统开销对比

OracleJDK 并不支持 Shenandoah如果你用的是 OpenJDK 12 或某些支持 Shenandoah 移植版的 JDK 的话,可以通过以下参数开启 Shenandoah-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

GC日志

日志格式

ParallelGC YoungGC日志

ParallelGCYoungGC日志

ParallelGC FullGC日志

ParallelGCFullGC日志

最佳实践

在不同的 JVM 的不垃圾回收器上,看参数默认是什么,不要轻信别人的建议,命令行示例如下:

java -XX:+PrintFlagsFinal -XX:+UseG1GC  2>&1 | grep UseAdaptiveSizePolicy

PrintCommandLineFlags通过它你能够查看当前所使用的垃圾回收器和一些默认的值。

# java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=127905216 -XX:MaxHeapSize=2046483456 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
openjdk version "1.8.0_41"
OpenJDK Runtime Environment (build 1.8.0_41-b04)
OpenJDK 64-Bit Server VM (build 25.40-b25, mixed mode)

G1垃圾收集器JVM参数最佳实践

# 1.基本参数
-server                  # 服务器模式
-Xmx12g                  # 初始堆大小
-Xms12g                  # 最大堆大小
-Xss256k                 # 每个线程的栈内存大小
-XX:+UseG1GC             # 使用 G1 (Garbage First) 垃圾收集器   
-XX:MetaspaceSize=256m   # 元空间初始大小
-XX:MaxMetaspaceSize=1g  # 元空间最大大小
-XX:MaxGCPauseMillis=200 # 每次YGC / MixedGC 的最多停顿时间 (期望最长停顿时间)

# 2.必备参数
-XX:+PrintGCDetails            # 输出详细GC日志
-XX:+PrintGCDateStamps         # 输出GC的时间戳以日期的形式如 2013-05-04T21:53:59.234+0800
-XX:+PrintTenuringDistribution # 打印对象分布为了分析GC时的晋升情况和晋升导致的高暂停看对象年龄分布日志
-XX:+PrintHeapAtGC                 # 在进行GC的前后打印出堆的信息
-XX:+PrintReferenceGC              # 打印Reference处理信息:强引用/弱引用/软引用/虚引用/finalize方法万一有问题
-XX:+PrintGCApplicationStoppedTime # 打印STW时间
-XX:+PrintGCApplicationConCurrentTime # 打印GC间隔的服务运行时长

# 3.日志分割参数
-XX:+UseGCLogFileRotation   # 开启日志文件分割
-XX:NumberOfGCLogFiles=14   # 最多分割几个文件,超过之后从头文件开始写
-XX:GCLogFileSize=32M       # 每个文件上限大小,超过就触发分割
-Xloggc:/path/to/gc-%t.log  # GC日志输出的文件路径,使用%t作为日志文件名,即gc-2021-03-29_20-41-47.log

CMS垃圾收集器JVM参数最佳实践

# 1.基本参数
-server   # 服务器模式
-Xmx4g    # JVM最大允许分配的堆内存按需分配
-Xms4g    # JVM初始分配的堆内存一般和Xmx配置成一样以避免每次gc后JVM重新分配内存
-Xmn256m  # 年轻代内存大小整个JVM内存=年轻代 + 年老代 + 持久代
-Xss512k  # 设置每个线程的堆栈大小
-XX:+DisableExplicitGC                # 忽略手动调用GC, System.gc()的调用就会变成一个空调用完全不触发GC
-XX:+UseConcMarkSweepGC               # 使用 CMS 垃圾收集器
-XX:+CMSParallelRemarkEnabled         # 降低标记停顿
-XX:+UseCMSCompactAtFullCollection    # 在FULL GC的时候对年老代的压缩
-XX:+UseFastAccessorMethods           # 原始类型的快速优化
-XX:+UseCMSInitiatingOccupancyOnly    # 使用手动定义初始化定义开始CMS收集
-XX:LargePageSizeInBytes=128m         # 内存页的大小
-XX:CMSInitiatingOccupancyFraction=70 # 使用cms作为垃圾回收使用70后开始CMS收集

# 2.必备参数
-XX:+PrintGCDetails                # 输出详细GC日志
-XX:+PrintGCDateStamps             # 输出GC的时间戳以日期的形式如 2013-05-04T21:53:59.234+0800
-XX:+PrintTenuringDistribution     # 打印对象分布为分析GC时的晋升情况和晋升导致的高暂停看对象年龄分布
-XX:+PrintHeapAtGC                 # 在进行GC的前后打印出堆的信息
-XX:+PrintReferenceGC              # 打印Reference处理信息:强引用/弱引用/软引用/虚引用/finalize方法万一有问题
-XX:+PrintGCApplicationStoppedTime # 打印STW时间
-XX:+PrintGCApplicationConCurrentTime # 打印GC间隔的服务运行时长

# 3.日志分割参数
-XX:+UseGCLogFileRotation   # 开启日志文件分割
-XX:NumberOfGCLogFiles=14   # 最多分割几个文件,超过之后从头文件开始写
-XX:GCLogFileSize=32M       # 每个文件上限大小,超过就触发分割
-Xloggc:/path/to/gc-%t.log  # GC日志输出的文件路径,使用%t作为日志文件名,即gc-2021-03-29_20-41-47.log

test、stage 环境jvm使用CMS 参数配置jdk8

-server -Xms256M -Xmx256M -Xss512k -Xmn96M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M -XX:InitialHeapSize=256M -XX:MaxHeapSize=256M  -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=2 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSParallelRemarkEnabled -XX:+UnlockDiagnosticVMOptions -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -XX:MaxTenuringThreshold=8  -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+PrintGC -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC  -XX:+PrintTenuringDistribution  -XX:SurvivorRatio=8 -Xloggc:../logs/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../dump

online 环境jvm使用CMS参数配置jdk8

-server -Xms4G -Xmx4G -Xss512k  -Xmn1536M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M -XX:InitialHeapSize=4G -XX:MaxHeapSize=4G  -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=2 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSParallelRemarkEnabled -XX:+UnlockDiagnosticVMOptions -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -XX:MaxTenuringThreshold=10  -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+PrintGC -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC  -XX:+PrintTenuringDistribution  -XX:SurvivorRatio=8 -Xloggc:../logs/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../dump

GC场景

Full GC场景

场景一System.gc()方法的调用

此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过 -XX:+ DisableExplicitGC 来禁止RMI调用System.gc()。

场景二:老年代代空间不足

  • 原因分析新生代对象转入老年代、创建大对象或数组时执行FullGC后仍空间不足
  • 抛出错误:Java.lang.OutOfMemoryError: Java heap space
  • 解决办法:
    • 尽量让对象在YoungGC时被回收
    • 让对象在新生代多存活一段时间
    • 不要创建过大的对象或数组

场景三:永生区空间不足

  • 原因分析JVM方法区因系统中要加载的类、反射的类和调用的方法较多而可能会被占满
  • 抛出错误:java.lang.OutOfMemoryError: PermGen space
  • 解决办法:
    • 增大老年代空间大小
    • 使用CMS GC

场景四CMS GC时出现promotion failed和concurrent mode failure

  • 原因分析:
    • promotion failed是在进行Minor GC时survivor space放不下、对象只能放入老年代而此时老年代也放不下造成
    • concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代而此时老年代空间不足造成的
  • 抛出错误GC日志中存在promotion failedconcurrent mode
  • 解决办法:增大幸存区或老年代

场景五:堆中分配很大的对象

  • 原因分析:创建大对象或长数据时,此对象直接进入老年代,而老年代虽有很大剩余空间,但没有足够的连续空间来存储
  • 抛出错误触发FullGC
  • 解决办法:配置-XX:+UseCMSCompactAtFullCollection开关参数用于享受用完FullGC后额外免费赠送的碎片整理过程但同时停顿时间不得不变长。可以使用-XX:CMSFullGCsBeforeCompaction参数来指定执行多少次不压缩的FullGC后才执行一次压缩

CMS GC场景

场景一:动态扩容引起的空间震荡

  • 现象

    服务刚刚启动时 GC 次数较多,最大空间剩余很多但是依然发生 GC这种情况我们可以通过观察 GC 日志或者通过监控工具来观察堆的空间变化情况即可。GC Cause 一般为 Allocation Failure且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整,如下图所示:

    动态扩容引起的空间震荡

  • 原因

    在 JVM 的参数中 -Xms-Xmx 设置的不一致,在初始化时只会初始 -Xms 大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次 GC。另外如果空间剩余很多时也会进行缩容操作JVM 通过 -XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio 来控制扩容和缩容的比例,调节这两个值也可以控制伸缩的时机。整个伸缩的模型理解可以看这个图,当 committed 的空间大小超过了低水位/高水位的大小capacity 也会随之调整:

    JVM内存伸缩模型

  • 策略

    观察 CMS GC 触发时间点 Old/MetaSpace 区的 committed 占比是不是一个固定的值,或者像上文提到的观察总的内存使用率也可以。尽量 将成对出现的空间大小配置参数设置成固定的 ,如 -Xms-Xmx-XX:MaxNewSize-XX:NewSize-XX:MetaSpaceSize-XX:MaxMetaSpaceSize 等。

场景二显式GC的去与留

  • 现象

    除了扩容缩容会触发 CMS GC 之外,还有 Old 区达到回收阈值、MetaSpace 空间不足、Young 区晋升失败、大对象担保失败等几种触发条件,如果这些情况都没有发生却触发了 GC ?这种情况有可能是代码中手动调用了 System.gc 方法,此时可以找到 GC 日志中的 GC Cause 确认下。

  • 原因

    保留 System.gcCMS中使用 Foreground Collector 时将会带来非常长的 STW在应用程序中 System.gc 被频繁调用,那就非常危险。增加 -XX:+DisableExplicitGC 参数则可以禁用。去掉 System.gc:禁用掉后会带来另一个内存泄漏的问题,为 DirectByteBuffer 分配空间过程中会显式调用 System.gc ,希望通过 Full GC 来强迫已经无用的 DirectByteBuffer 对象释放掉它们关联的 Native Memory如Netty等。

  • 策略

    无论是保留还是去掉都会有一定的风险点,不过目前互联网中的 RPC 通信会大量使用 NIO所以建议保留。此外 JVM 还提供了 -XX:+ExplicitGCInvokesConcurrent-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 参数来将 System.gc 的触发类型从 Foreground 改为 Background同时 Background 也会做 Reference Processing这样的话就能大幅降低了 STW 开销,同时也不会发生 NIO Direct Memory OOM。

场景三MetaSpace区OOM

  • 现象

    JVM 在启动后或者某个时间点开始, MetaSpace 的已使用大小在持续增长,同时每次 GC 也无法释放,调大 MetaSpace 空间也无法彻底解决

  • 原因

    在讨论为什么会 OOM 之前我们先来看一下这个区里面会存什么数据Java 7 之前字符串常量池被放到了 Perm 区,所有被 intern 的 String 都会被存在这里,由于 String.intern 是不受控的,所以 -XX:MaxPermSize 的值也不太好设置,经常会出现 java.lang.OutOfMemoryError: PermGen space 异常,所以在 Java 7 之后常量池等字面量Literal、类静态变量Class Static、符号引用Symbols Reference等几项被移到 Heap 中。而 Java 8 之后 PermGen 也被移除,取而代之的是 MetaSpace。由场景一可知为了避免弹性伸缩带来的额外 GC 消耗,我们会将 -XX:MetaSpaceSize-XX:MaxMetaSpaceSize 两个值设置为固定的,但这样也会导致在空间不够的时候无法扩容,然后频繁地触发 GC最终 OOM。所以关键原因是 ClassLoader不停地在内存中load了新的Class ,一般这种问题都发生在动态类加载等情况上。

  • 策略

    可以 dump 快照之后通过 JProfiler 或 MAT 观察 Classes 的 Histogram直方图 即可,或者直接通过命令即可定位, jcmd 打几次 Histogram 的图,看一下具体是哪个包下的 Class 增加较多就可以定位。如果无法从整体的角度定位,可以添加 -XX:+TraceClassLoading-XX:+TraceClassUnLoading 参数观察详细的类加载和卸载信息。

场景四:过早晋升

  • 现象

    这种场景主要发生在分代的收集器上面专业的术语称为“Premature Promotion”。90% 的对象朝生夕死,只有在 Young 区经历过几次 GC 的洗礼后才会晋升到 Old 区,每经历一次 GC 对象的 GC Age 就会增长 1最大通过 -XX:MaxTenuringThreshold 来控制。过早晋升一般不会直接影响 GC总会伴随着浮动垃圾、大对象担保失败等问题但这些问题不是立刻发生的我们可以观察以下几种现象来判断是否发生了过早晋升

    • 分配速率接近于晋升速率 ,对象晋升年龄较小。

      GC 日志中出现“Desired survivor size 107347968 bytes, new threshold 1(max 6) ”等信息,说明此时经历过一次 GC 就会放到 Old 区。

    • Full GC 比较频繁 ,且经历过一次 GC 之后 Old 区的 变化比例非常大

      如Old区触发回收阈值是80%经历一次GC之后下降到了10%这说明Old区70%的对象存活时间其实很短。

      FullGC变化比例大

    过早晋升的危害:

    • Young GC 频繁,总的吞吐量下降
    • Full GC 频繁,可能会有较大停顿
  • 原因

    主要的原因有以下两点:

    • Young/Eden 区过小 过小的直接后果就是 Eden 被装满的时间变短,本应该回收的对象参与了 GC 并晋升Young GC 采用的是复制算法,由基础篇我们知道 copying 耗时远大于 mark也就是 Young GC 耗时本质上就是 copy 的时间CMS 扫描 Card Table 或 G1 扫描 Remember Set 出问题的情况另说),没来及回收的对象增大了回收的代价,所以 Young GC 时间增加同时又无法快速释放空间Young GC 次数也跟着增加
    • 分配速率过大 可以观察出问题前后 Mutator 的分配速率,如果有明显波动可以尝试观察网卡流量、存储类中间件慢查询日志等信息,看是否有大量数据被加载到内存中
  • 策略

    • 如果是 Young/Eden 区过小 ,可以在总的 Heap 内存不变的情况下适当增大Young区。一般情况下Old的大小应当为活跃对象的2~3倍左右考虑到浮动垃圾问题最好在3倍左右剩下的都可以分给Young区

    • 过早晋升优化来看原配置为Young 1.2G+Old 2.8G通过观察CMS GC的情况找到存活对象大概为 300~400M于是调整Old 1.5G左右剩下2.5G分给Young 区。仅仅调了一个Young区大小参数-Xmn),整个 JVM 一分钟Young GC从26次降低到了11次单次时间也没有增加总的GC时间从1100ms降低到了500msCMS GC次数也从40分钟左右一次降低到了7小时30分钟一次

      过早晋升优化GC

      过早晋升优化Oldgen

      如果是分配速率过大:

      • 偶发较大 :通过内存分析工具找到问题代码,从业务逻辑上做一些优化
      • 一直较大 :当前的 Collector 已经不满足 Mutator 的期望了,这种情况要么扩容 Mutator 的 VM要么调整 GC 收集器类型或加大空间
  • 小结

    过早晋升问题一般不会特别明显,但日积月累之后可能会爆发一波收集器退化之类的问题,所以我们还是要提前避免掉的,可以看看自己系统里面是否有这些现象,如果比较匹配的话,可以尝试优化一下。一行代码优化的 ROI 还是很高的。如果在观察 Old 区前后比例变化的过程中,发现可以回收的比例非常小,如从 80% 只回收到了 60%说明我们大部分对象都是存活的Old 区的空间可以适当调大些。

场景五CMS Old GC频繁

  • 现象

    Old 区频繁的做 CMS GC但是每次耗时不是特别长整体最大 STW 也在可接受范围内,但由于 GC 太频繁导致吞吐下降比较多。

  • 原因

    这种情况比较常见,基本都是一次 Young GC 完成后,负责处理 CMS GC 的一个后台线程 concurrentMarkSweepThread 会不断地轮询,使用 shouldConcurrentCollect() 方法做一次检测,判断是否达到了回收条件。如果达到条件,使用 collect_in_background() 启动一次 Background 模式 GC。轮询的判断是使用 sleepBeforeNextCycle() 方法,间隔周期为 -XX:CMSWaitDuration 决定,默认为 2s。

  • 策略

    处理这种常规内存泄漏问题基本是一个思路,主要步骤如下:

    CMSOldGC频繁

    Dump Diff 和 Leak Suspects 比较直观,这里说下其它几个关键点:

    • 内存 Dump 使用 jmap、arthas 等 dump 堆进行快照时记得摘掉流量,同时 分别在 CMS GC 的发生前后分别 dump 一次
    • 分析 Top Component 要记得按照对象、类、类加载器、包等多个维度观察Histogram同时使用 outgoing和incoming分析关联的对象其次Soft Reference和Weak Reference、Finalizer 等也要看一下
    • 分析 Unreachable 重点看一下这个,关注下 Shallow 和 Retained 的大小。如下图所示的一次 GC 优化,就根据 Unreachable Objects 发现了 Hystrix 的滑动窗口问题。

    分析Unreachable

场景六单次CMS Old GC耗时长

  • 现象

    CMS GC 单次 STW 最大超过 1000ms不会频繁发生如下图所示最长达到了 8000ms。某些场景下会引起“雪崩效应”这种场景非常危险我们应该尽量避免出现。

    CMSGC单次STW长

  • 原因

    CMS在回收的过程中STW的阶段主要是 Init Mark 和 Final Remark 这两个阶段也是导致CMS Old GC 最多的原因另外有些情况就是在STW前等待Mutator的线程到达SafePoint也会导致时间过长但这种情况较少。

  • 策略

    知道了两个 STW 过程执行流程,我们分析解决就比较简单了,由于大部分问题都出在 Final Remark 过程,这里我们也拿这个场景来举例,主要步骤:

    • 【方向】 观察详细 GC 日志,找到出问题时 Final Remark 日志,分析下 Reference 处理和元数据处理 real 耗时是否正常,详细信息需要通过 -XX:+PrintReferenceGC 参数开启。 基本在日志里面就能定位到大概是哪个方向出了问题,耗时超过 10% 的就需要关注
    2019-02-27T19:55:37.920+0800: 516952.915: [GC (CMS Final Remark) 516952.915: [ParNew516952.939: [SoftReference, 0 refs, 0.0003857 secs]516952.939: [WeakReference, 1362 refs, 0.0002415 secs]516952.940: [FinalReference, 146 refs, 0.0001233 secs]516952.940: [PhantomReference, 0 refs, 57 refs, 0.0002369 secs]516952.940: [JNI Weak Reference, 0.0000662 secs]
    [class unloading, 0.1770490 secs]516953.329: [scrub symbol table, 0.0442567 secs]516953.373: [scrub string table, 0.0036072 secs][1 CMS-remark: 1638504K(2048000K)] 1667558K(4352000K), 0.5269311 secs] [Times: user=1.20 sys=0.03, real=0.53 secs]
    
    • 【根因】 有了具体的方向我们就可以进行深入的分析,一般来说最容易出问题的地方就是 Reference 中的 FinalReference 和元数据信息处理中的 scrub symbol table 两个阶段,想要找到具体问题代码就需要内存分析工具 MAT 或 JProfiler 了,注意要 dump 即将开始 CMS GC 的堆。在用 MAT 等工具前也可以先用命令行看下对象 Histogram有可能直接就能定位问题。

      • 对 FinalReference 的分析主要观察 java.lang.ref.Finalizer 对象的 dominator tree找到泄漏的来源。经常会出现问题的几个点有 Socket 的 SocksSocketImpl 、Jersey 的 ClientRuntime、MySQL 的 ConnectionImpl 等等
      • scrub symbol table 表示清理元数据符号引用耗时,符号引用是 Java 代码被编译成字节码时,方法在 JVM 中的表现形式,生命周期一般与 Class 一致,当 _should_unload_classes 被设置为 true 时在 CMSCollector::refProcessingWork() 中与 Class Unload、String Table 一起被处理
    • 【策略】 知道 GC 耗时的根因就比较好处理了,这种问题不会大面积同时爆发,不过有很多时候单台 STW 的时间会比较长,如果业务影响比较大,及时摘掉流量,具体后续优化策略如下:

      • FinalReference找到内存来源后通过优化代码的方式来解决如果短时间无法定位可以增加 -XX:+ParallelRefProcEnabled 对 Reference 进行并行处理
      • symbol table观察 MetaSpace 区的历史使用峰值,以及每次 GC 前后的回收情况,一般没有使用动态类加载或者 DSL 处理等MetaSpace 的使用率上不会有什么变化,这种情况可以通过 -XX:-CMSClassUnloadingEnabled 来避免 MetaSpace 的处理JDK8 会默认开启 CMSClassUnloadingEnabled这会使得 CMS 在 CMS-Remark 阶段尝试进行类的卸载
  • 小结

    正常情况进行的 Background CMS GC出现问题基本都集中在 Reference 和 Class 等元数据处理上,在 Reference 类的问题处理方面,不管是 FinalReference还是 SoftReference、WeakReference 核心的手段就是找准时机 dump快照然后用内存分析工具来分析。Class处理方面目前除了关闭类卸载开关没有太好的方法。在 G1 中同样有 Reference 的问题,可以观察日志中的 Ref Proc处理方法与 CMS 类似。

场景七:内存碎片&收集器退化

  • 现象

    并发的 CMS GC 算法,退化为 Foreground 单线程串行 GC 模式STW 时间超长,有时会长达十几秒。其中 CMS 收集器退化后单线程串行 GC 算法有两种:

    • 带压缩动作的算法,称为 MSC上面我们介绍过使用标记-清理-压缩,单线程全暂停的方式,对整个堆进行垃圾收集,也就是真正意义上的 Full GC暂停时间要长于普通 CMS
    • 不带压缩动作的算法,收集 Old 区,和普通的 CMS 算法比较相似,暂停时间相对 MSC 算法短一些
  • 原因

    CMS 发生收集器退化主要有以下几种情况:

    • 晋升失败Promotion Failed

    • 增量收集担保失败

    • 显式 GC

    • 并发模式失败Concurrent Mode Failure

  • 策略

    分析到具体原因后,我们就可以针对性解决了,具体思路还是从根因出发,具体解决策略:

    • 内存碎片 通过配置 -XX:UseCMSCompactAtFullCollection=true 来控制 Full GC 的过程中是否进行空间的整理(默认开启,注意是 Full GC不是普通 CMS GC以及 -XX: CMSFullGCsBeforeCompaction=n 来控制多少次 Full GC 后进行一次压缩
    • 增量收集 降低触发 CMS GC 的阈值,即参数 -XX:CMSInitiatingOccupancyFraction 的值,让 CMS GC 尽早执行,以保证有足够的连续空间,也减少 Old 区空间的使用大小,另外需要使用 -XX:+UseCMSInitiatingOccupancyOnly 来配合使用,不然 JVM 仅在第一次使用设定值,后续则自动调整
    • 浮动垃圾 视情况控制每次晋升对象的大小,或者缩短每次 CMS GC 的时间,必要时可调节 NewRatio 的值。另外使用 -XX:+CMSScavengeBeforeRemark 在过程中提前触发一次Young GC防止后续晋升过多对象
  • 小结

    正常情况下触发并发模式的 CMS GC停顿非常短对业务影响很小但 CMS GC 退化后,影响会非常大,建议发现一次后就彻底根治。只要能定位到内存碎片、浮动垃圾、增量收集相关等具体产生原因,还是比较好解决的,关于内存碎片这块,如果 -XX:CMSFullGCsBeforeCompaction 的值不好选取的话,可以使用 -XX:PrintFLSStatistics 来观察内存碎片率情况,然后再设置具体的值。最后就是在编码的时候也要避免需要连续地址空间的大对象的产生,如过长的字符串,用于存放附件、序列化或反序列化的 byte 数组等,还有就是过早晋升问题尽量在爆发问题前就避免掉。

场景八堆外内存OOM

  • 现象

    内存使用率不断上升,甚至开始使用 SWAP 内存,同时可能出现 GC 时间飙升,线程被 Block 等现象, 通过 top 命令发现 Java 进程的 RES 甚至超过了 **-Xmx** 的大小 。出现这些现象时,基本可确定是出现堆外内存泄漏。

  • 原因

    JVM 的堆外内存泄漏,主要有两种的原因:

    • 通过 UnSafe#allocateMemoryByteBuffer#allocateDirect 主动申请了堆外内存而没有释放,常见于 NIO、Netty 等相关组件
    • 代码中有通过 JNI 调用 Native Code 申请的内存没有释放
  • 策略

    原因一:主动申请未释放

    原因二:通过 JNI 调用的 Native Code 申请的内存未释放

场景九JNI引发的GC问题

  • 现象

    在 GC 日志中,出现 GC Cause 为 GCLocker Initiated GC。

    2020-09-23T16:49:09.727+0800: 504426.742: [GC (GCLocker Initiated GC) 504426.742: [ParNew (promotion failed): 209716K->6042K(1887488K), 0.0843330 secs] 1449487K->1347626K(3984640K), 0.0848963 secs] [Times: user=0.19 sys=0.00, real=0.09 secs]2020-09-23T16:49:09.812+0800: 504426.827: [Full GC (GCLocker Initiated GC) 504426.827: [CMS: 1341583K->419699K(2097152K), 1.8482275 secs] 1347626K->419699K(3984640K), [Metaspace: 297780K->297780K(1329152K)], 1.8490564 secs] [Times: user=1.62 sys=0.20, real=1.85 secs]
    
  • 原因

    JNIJava Native Interface意为 Java 本地调用,它允许 Java 代码和其他语言写的 Native 代码进行交互。JNI 如果需要获取 JVM 中的 String 或者数组,有两种方式:

    • 拷贝传递
    • 共享引用(指针),性能更高

    由于 Native 代码直接使用了 JVM 堆区的指针,如果这时发生 GC就会导致数据错误。因此在发生此类 JNI 调用时,禁止 GC 的发生,同时阻止其他线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC。

  • 策略

    • 添加 -XX+PrintJNIGCStalls 参数,可以打印出发生 JNI 调用时的线程,进一步分析,找到引发问题的 JNI 调用
    • JNI 调用需要谨慎,不一定可以提升性能,反而可能造成 GC 问题
    • 升级 JDK 版本到 14避免 JDK-8048556 导致的重复 GC

JVM性能调优

磁盘不足排查

其实磁盘不足排查算是系统、程序层面的问题排查并不算是JVM但是另一方面考虑过来就是系统磁盘的不足也会导致JVM的运行异常所以也把磁盘不足算进来了。并且排查磁盘不足是比较简单就是几个命令然后就是逐层的排查首先第一个命令就是df -h,查询磁盘的状态:

JVM-磁盘不足排查

从上面的显示中其中第一行使用的2.8G最大,然后是挂载在 / 目录下,我们直接cd /。然后通过执行:

du -sh *

查看各个文件的大小,找到其中最大的,或者说存储量级差不多的并且都非常大的文件,把那些没用的大文件删除就好。

JVM-磁盘不足排查-du-sh

然后就是直接cd到对应的目录也是执行du -sh *,就这样一层一层的执行,找到对应的没用的,然后文件又比较大的,可以直接删除。

CPU过高排查

排查过程

  • 使用top查找进程id
  • 使用top -Hp <pid>查找进程中耗cpu比较高的线程id
  • 使用printf %x <pid>将线程id十进制转十六进制
  • 使用 jstack -pid | grep -A 20 <pid>过滤出线程id锁关联的栈信息
  • 根据栈信息中的调用链定位业务代码

案例代码如下:

public class CPUSoaring {
        public static void main(String[] args) {

                Thread thread1 = new Thread(new Runnable(){
                        @Override
                        public void run() {
                                for (;;){
                                      System.out.println("I am children-thread1");
                                }
                        }
                },"children-thread1");
                
                 Thread thread2 = new Thread(new Runnable(){
                        @Override
                        public void run() {
                                for (;;){
                                      System.out.println("I am children-thread2");
                                }
                        }
                },"children-thread2");
                
                thread1.start();
                thread2.start();
                System.err.println("I am is main thread!!!!!!!!");
        }
}
  • 第一步:首先通过top命令可以查看到id为3806的进程所占的CPU最高

    CPU过高排查-top

  • 第二步:然后通过top -Hp pid命令找到占用CPU最高的线程

    CPU过高排查-top-Hp-pid

  • 第三步:接着通过:printf '%x\n' tid命令将线程的tid转换为十六进制xid

    CPU过高排查-printf

  • 第四步:最后通过:jstack pid|grep xid -A 30命令就是输出线程的堆栈信息,线程所在的位置:

    CPU过高排查-jstack

  • 第五步:还可以通过jstack -l pid > 文件名称.txt 命令将线程堆栈信息输出到文件,线下查看。

    这就是一个CPU飙高的排查过程目的就是要找到占用CPU最高的线程所在的位置,然后就是review你的代码定位到问题的所在。使用Arthas的工具排查也是一样的首先要使用top命令找到占用CPU最高的Java进程然后使用Arthas进入该进程内使用dashboard命令排查占用CPU最高的线程。,最后通过thread命令线程的信息。

内存打满排查

排查过程

  • 查找进程idtop -d 2 -c
  • 查看JVM堆内存分配情况jmap -heap pid
  • 查看占用内存比较多的对象:jmap -histo pid | head -n 100
  • 查看占用内存比较多的存活对象:jmap -histo:live pid | head -n 100

示例

  • 第一步top -d 2 -c

    img

  • 第二步jmap -heap 8338

    img

  • 第三步:定位占用内存比价多的对象

    img

    这里就能看到对象个数以及对象大小……

    img

    这里看到一个自定义的类,这样我们就定位到具体对象,看看这个对象在那些地方有使用、为何会有大量的对象存在。

OOM异常排查

OOM的异常排查也比较简单首先服务上线的时候要先设置这两个参数

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${目录}

指定项目出现OOM异常的时候自动导出堆转储文件然后通过内存分析工具Visual VM)来进行线下的分析。

首先我们来聊一聊哪些原因会导致OOM异常站在JVM的分区的角度

  • Java堆
  • 方法区
  • 虚拟机栈
  • 本地方法栈
  • 程序计数器
  • 直接内存

只有程序计数器区域不会出现OOM在Java 8及以上的元空间本地内存都会出现OOM。

而站在程序代码的角度来看总结了大概有以下几点原因会导致OOM异常

  • 内存泄露
  • 对象过大、过多
  • 方法过长
  • 过度使用代理框架,生成大量的类信息

接下来我们屋来看看OOM的排查出现OOM异常后dump出了堆转储文件然后打开jdk自带的Visual VM工具导入堆转储文件首先我使用的OOM异常代码如下

import java.util.ArrayList;
import java.util.List;

class OOM {

        static class User{
                private String name;
                private int age;

                public User(String name, int age){
                        this.name = name;
                        this.age = age;
                }
        }

        public static void main(String[] args) throws InterruptedException {
                List<User> list = new ArrayList<>();
                for (int i = 0; i < Integer.MAX_VALUE; i++) {
                        Thread.sleep(1000);
                        User user = new User("zhangsan"+i,i);
                        list.add(user);
                }
        }

}

代码很简单就是往集合里面不断地add对象带入堆转储文件后在类和实例那栏就可以看到实例最多的类

OOM异常排查-查看实例最多的类

这样就找到导致OOM异常的类还可以通过下面的方法查看导致OOM异常的线程堆栈信息找到对应异常的代码段。

OOM异常排查-查看异常代码

OOM异常排查-查看异常代码堆栈

上面的方法是排查已经出现了OOM异常的方法肯定是防线的最后一步那么在此之前怎么防止出现OOM异常呢

一般大厂都会有自己的监控平台,能够实施的监控测试环境、预览环境、线上实施的服务健康状况CPU、内存 等信息对于频繁GC并且GC后内存的回收率很差的就要引起我们的注意了。

因为一般方法的长度合理95%以上的对象都是朝生夕死,在Minor GC后只剩少量的存活对象,所以在代码层面上应该避免方法过长、大对象的现象。

每次自己写完代码,自己检查后,都可以提交给比自己高级别的工程师review自己的代码,就能及时的发现代码的问题,基本上代码没问题,百分之九十以上的问题都能避免,这也是大厂注重代码质量,并且时刻review代码的习惯。

Jvisualvm

项目频繁YGC 、FGC问题排查

内存问题

对象内存占用、实例个数监控

img

对象内存占用、年龄值监控

img

通过上面两张图发现这些对象占用内存比较大而且存活时间也是比较常所以survivor 中的空间被这些对象占用而如果缓存再次刷新则会创建同样大小对象来替换老数据这时发现eden内存空间不足就会触发yonggc 如果yonggc 结束后发现eden空间还是不够则会直接放到老年代所以这样就产生了大对象的提前晋升导致fgc增加……

优化办法:优化两个缓存对象,将缓存对象大小减小。优化一下两个对象,缓存关键信息!

CPU耗时问题排查

Cpu使用耗时监控

img

耗时、调用次数监控:

img

从上面监控图可以看到主要耗时还是在网络请求没有看到具体业务代码消耗过错cpu……

调优堆内存分配

初始堆空间大小设置

  • 使用系统默认配置在系统稳定运行一段时间后查看记录内存使用情况Eden、survivor0 、survivor1 、old、metaspace
  • 按照通用法则通过gc信息分配调整大小整个堆大小是Full GC后老年代空间占用大小的3-4倍
  • 老年代大小为Full GC后老年代空间占用大小的2-3倍
  • 新生代大小为Full GC后老年代空间占用大小的1-1.5倍
  • 元数据空间大小为Full GC后元数据空间占用大小的1.2-1.5倍

活跃数大小是应用程序运行在稳定态时长期存活的对象在java堆中占用的空间大小。也就是在应用趋于稳太时FullGC之后Java堆中存活对象占用空间大小。注意在jdk8中将jdk7中的永久代改为元数据区metaspace 使用的物理内存,不占用堆内存)

堆大小调整的着手点、分析点

  • 统计Minor GC 持续时间
  • 统计Minor GC 的次数
  • 统计Full GC的最长持续时间
  • 统计最差情况下Full GC频率
  • 统计GC持续时间和频率对优化堆的大小是主要着手点我们按照业务系统对延迟和吞吐量的需求在按照这些分析我们可以进行各个区大小的调整

年轻代调优

年轻代调优规则

  • 老年代空间大小不应该小于活跃数大小1.5倍。老年代空间大小应为老年代活跃数2-3倍
  • 新生代空间至少为java堆内存大小的10% 。新生代空间大小应为1-1.5倍的老年代活跃数
  • 在调小年轻代空间时应保持老年代空间不变

MinorGC是收集eden+from survivor 区域的当业务系统匀速生成对象的时候如果年轻带分配内存偏小会发生频繁的MinorGC如果分配内存过大则会导致MinorGC停顿时间过长无法满足业务延迟性要求。所以按照堆分配空间分配之后分析gc日志看看MinorGC的频率和停顿时间是否满足业务要求。

  • MinorGC频繁原因 MinorGC 比较频繁说明eden内存分配过小在恒定的对象产出的情况下很快无空闲空间来存放新对象所以产生了MinorGC,所以eden区间需要调大。

  • 年轻代大小调整 Eden调整到多大那我们可以查看GC日志查看业务多长时间填满了eden空间然后按照业务能承受的收集频率来计算eden空间大小。比如eden空间大小为128M每5秒收集一次则我们为了达到10秒收集一次则可以调大eden空间为256M这样能降低收集频率。年轻代调大的同时相应的也要调大老年代否则有可能会出现频繁的concurrent model failed 从而导致Full GC 。

  • MinorGC停顿时间过长 MinorGC 收集过程是要产生STW的。如果年轻代空间太大则gc收集时耗时比较大所以我们按业务对停顿的要求减少内存比如现在一次MinorGC 耗时12.8毫秒eden内存大小192M ,我们为了减少MinorGC 耗时我们要减少内存。比如我们MinorGC 耗时标准为10毫秒这样耗时减少16.6% 同样年轻代内存也要减少16.6% 即192*0.1661 = 31.89M 。年轻代内存为192-31.89=160.11M,在减少年轻代大小而要保持老年代大小不变则要减少堆内存大小至512-31.89=480.11M

堆内存512M 年轻代: 192M 收集11次耗时141毫秒 12.82毫秒/次

img

堆内存512M 年轻代192M 收集12次耗时151毫秒 12.85毫秒/次

img

按照上面计算调优 堆内存: 480M 年轻带: 160M 收集14次 耗时154毫秒 11毫秒/次 相比之前的 12.82毫秒/次 停顿时间减少1.82毫秒

img

但是还没达到10毫秒的要求继续按照这样的逻辑进行 11-10=1 1/11= 0.909 即 0.09 所以耗时还要降低9%。 年轻代减少160*0.09 = 14.545=14.55 M; 160-14.55 =145.45=145M 堆大小: 480-14.55 = 465.45=465M 但是在这样调整后使用jmap -heap 查看的时候年轻代大小和实际配置数据有出入年轻代大小为150M大于配置的145M这是因为-XX:NewRatio 默认2 即年轻代和老年代12的关系所以这里将-XX:NewRatio 设置为3 即年轻代、老年大小比为13 ,最终堆内存大小为:

img

MinorGC耗时 159/16=9.93毫秒

img

MinorGC耗时 185/18=10.277=10.28毫秒

img

MinorGC耗时 205/20=10.25毫秒

img

Ok 这样MinorGC停顿时间过长问题解决MinorGC要么比较频繁要么停顿时间比较长解决这个问题就是调整年轻代大小但是调整的时候还是要遵守这些规则。

老年代调优

按照同样的思路对老年代进行调优同样分析FullGC 频率和停顿时间,按照优化设定的目标进行老年代大小调整。

老年代调优流程

  • 分析每次MinorGC 之后老年代空间占用变化计算每次MinorGC之后晋升到老年代的对象大小
  • 按照MinorGC频率和晋升老年代对象大小计算提升率即每秒钟能有多少对象晋升到老年代
  • FullGC之后统计老年代空间被占用大小计算老年带空闲空间再按照第2部计算的晋升率计算该老年代空闲空间多久会被填满而再次发生FullGC同样观察FullGC 日志信息计算FullGC频率如果频率过高则可以增大老年代空间大小老解决增大老年代空间大小应保持年轻代空间大小不变
  • 如果在FullGC 频率满足优化目标而停顿时间比较长的情况下可以考虑使用CMS、G1收集器。并发收集减少停顿时间

栈溢出

栈溢出异常的排查(包括虚拟机栈、本地方法栈基本和OOM的一场排查是一样的导出异常的堆栈信息然后使用mat或者Visual VM工具进行线下分析找到出现异常的代码或者方法。

当线程请求的栈深度大于虚拟机栈所允许的大小时,就会出现StackOverflowError异常,二从代码的角度来看,导致线程请求的深度过大的原因可能有:方法栈中对象过大,或者过多,方法过长从而导致局部变量表过大,超过了-Xss参数的设置

死锁排查

死锁的案例演示的代码如下:

public class DeadLock {

	public static Object lock1 = new Object();
	public static Object lock2 = new Object();

	public static void main(String[] args){
		Thread a = new Thread(new Lock1(),"DeadLock1");
		Thread b = new Thread(new Lock2(),"DeadLock2");
		a.start();
		b.start();
	}
}
class Lock1 implements Runnable{
	@Override
	public void run(){
		try{
			while(true){
				synchronized(DeadLock.lock1){
					System.out.println("Waiting for lock2");
					Thread.sleep(3000);
					synchronized(DeadLock.lock2){
						System.out.println("Lock1 acquired lock1 and lock2 ");
					}
				}
			}
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}
class Lock2 implements Runnable{
	@Override
	public void run(){
		try{
			while(true){
				synchronized(DeadLock.lock2){
					System.out.println("Waiting for lock1");
					Thread.sleep(3000);
					synchronized(DeadLock.lock1){
						System.out.println("Lock2 acquired lock1 and lock2");
					}
				}
			}
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}

上面的代码非常的简单,就是两个类的实例作为锁资源,然后分别开启两个线程,不同顺序的对锁资源资源进行加锁,并且获取一个锁资源后,等待三秒,是为了让另一个线程有足够的时间获取另一个锁对象。

运行上面的代码后,就会陷入死锁的僵局:

死锁排查-死锁运行结果示例

对于死锁的排查若是在测试环境或者本地直接就可以使用Visual VM连接到该进程如下界面就会自动检测到死锁的存在

死锁排查-检测死锁

并且查看线程的堆栈信息。就能看到具体的死锁的线程:

死锁排查-查看线程堆栈信息

线上的话可以上用Arthas也可以使用原始的命令进行排查原始命令可以先使用jps查看具体的Java进程的ID然后再通过jstack ID查看进程的线程堆栈信息,他也会自动给你提示有死锁的存在:

死锁排查-jstack查看线程堆栈

Arthas工具可以使用thread命令排查死锁,要关注的是BLOCKED状态的线程,如下图所示:

死锁排查-Arthas查看死锁

具体thread的详细参数可以参考如下图所示

死锁排查-Thread详细参数

如何避免死锁

上面我们聊了如何排查死锁,下面我们来聊一聊如何避免死锁的发生,从上面的案例中可以发现,死锁的发生两个线程同时都持有对方不释放的资源进入僵局。所以,在代码层面,要避免死锁的发生,主要可以从下面的四个方面进行入手:

  • 首先避免线程的对于资源的加锁顺序要保持一致

  • 并且要避免同一个线程对多个资源进行资源的争抢

  • 另外的话对于已经获取到的锁资源尽量设置失效时间避免异常没有释放锁资源可以使用acquire() 方法加锁时可指定 timeout 参数

  • 最后,就是使用第三方的工具检测死锁,预防线上死锁的发生

死锁的排查已经说完了上面的基本就是问题的排查也可以算是调优的一部分吧但是对于JVM调优来说重头戏应该是在Java堆,这部分的调优才是重中之重。

调优实战

上面说完了调优的目的和调优的指标,那么我们就来实战调优,首先准备我的案例代码,如下:

import java.util.ArrayList;
import java.util.List;

class OOM {

	static class User{
		private String name;
		private int age;

		public User(String name, int age){
			this.name = name;
			this.age = age;
		}

	}

	public static void main(String[] args) throws InterruptedException {
		List<User> list = new ArrayList<>();
		for (int i = 0; i < Integer.MAX_VALUE; i++) {
		     Tread.sleep(1000);
			System.err.println(Thread.currentThread().getName());
			User user = new User("zhangsan"+i,i);
			list.add(user);
		}
	}
}

案例代码很简单,就是不断的往一个集合里里面添加对象,首先初次我们启动的命令为:

java   -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.dump OOM

就是纯粹的设置了一些GC的打印日志然后通过Visual VM来看GC的显示如下

调优实战-VisualVM查看GC显示

可以看到一段时间后出现4次Minor GC使用的时间是29.648ms发生一次Full GC使用的时间是41.944ms。

Minor GC非常频繁Full GC也是在短时间内就发生了几次观察输出的日志发现以及Visual VM的显示来看都是因为内存没有设置太小导致Minor GC频繁。

因此我们第二次适当的增大Java堆的大小调优设置的参数为

java -Xmx2048m -Xms2048m -Xmn1024m -Xss256k  -XX:+UseConcMarkSweepGC  -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.dump OOM

观察一段时间后,结果如下图所示:

调优实战-一段时间后VisualVM查看GC显示

可以发现Minor GC次数明显下降但是还是发生了Full GC根据打印的日志来看是因为元空间的内存不足看了上面的Visual VM元空间的内存图也是一样基本都到顶了

调优实战-元空间不足

因此第三次对于元空间的区域设置大一些并且将GC回收器换成是CMS的设置的参数如下

java -Xmx2048m -Xms2048m -Xmn1024m -Xss256k -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m -XX:+UseConcMarkSweepGC  -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.dump OOM

观察相同的时间后Visual VM的显示图如下

调优实战-元空间调后后VisualVM查看GC显示

同样的时间一次Minor GC和Full GC都没有发生所以这样我觉得也算是已经调优了。

但是调优并不是一味的调大内存是要在各个区域之间取得平衡可以适当的调大内存以及更换GC种类举个例子当把上面的案例代码的Thread.sleep(1000)给去掉。

然后再来看Visual VM的图如下

调优实战-去掉线程休眠VisualVM显示

可以看到Minor GC也是非常频繁的因为这段代码本身就是不断的增大内存直到OOM异常真正的实际并不会这样可能当内存增大到一定两级后就会在一段范围平衡。

当我们将上面的情况再适当的增大内存JVM参数如下

java -Xmx4048m -Xms4048m -Xmn2024m -XX:SurvivorRatio=7  -Xss256k -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=100m -XX:+UseConcMarkSweepGC  -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.dump OOM

可以看到相同时间内确实Minor GC减少了但是时间增大了因为复制算法基本都是存活的复制需要耗费大量的性能和时间

调优实战-减少MinorGC后VisualVM显示

所以调优要有取舍取得一个平衡点性能、状态达到佳就OK了并没最佳的状态这就是调优的基本法则而且调优也是一个细活所谓慢工出细活需要耗费大量的时间慢慢调不断的做对比。

调优参数

  • -Xms1024m 设置堆的初始大小
  • -Xmx1024m 设置堆的最大大小
  • -XX:NewSize=1024m 设置年轻代的初始大小
  • -XX:MaxNewSize=1024m 设置年轻代的最大值
  • -XX:SurvivorRatio=8 Eden和S区的比例
  • -XX:NewRatio=4 设置老年代和新生代的比例
  • -XX:MaxTenuringThreshold=10 设置晋升老年代的年龄条件

  • -Xss128k

元空间

  • -XX:MetasapceSize=200m 设置初始元空间大小
  • -XX:MaxMatespaceSize=200m 设置最大元空间大小 默认无限制

直接内存

  • -XX:MaxDirectMemorySize 设置直接内存的容量,默认与堆最大值一样

日志

  • -Xloggc:/opt/app/ard-user/ard-user-gc-%t.log 设置日志目录和日志名称
  • -XX:+UseGCLogFileRotation 开启滚动生成日志
  • -XX:NumberOfGCLogFiles=5 滚动GC日志文件数默认0不滚动
  • -XX:GCLogFileSize=20M GC文件滚动大小需开 UseGCLogFileRotation
  • -XX:+PrintGCDetails 开启记录GC日志详细信息包括GC类型、各个操作使用的时间,并且在程序运行结束打印出JVM的内存占用情况
  • -XX:+ PrintGCDateStamps 记录系统的GC时间
  • -XX:+PrintGCCause 产生GC的原因(默认开启)

GC

Serial垃圾收集器新生代

开启

  • -XX:+UseSerialGC

关闭:

  • -XX:-UseSerialGC //新生代使用Serial 老年代则使用SerialOld

Parallel Scavenge收集器新生代开启

  • -XX:+UseParallelOldGC 关闭
  • -XX:-UseParallelOldGC 新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器

ParallelOl垃圾收集器老年代开启

  • -XX:+UseParallelGC 关闭
  • -XX:-UseParallelGC 新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器

ParNew垃圾收集器新生代开启

  • -XX:+UseParNewGC 关闭
  • -XX:-UseParNewGC //新生代使用功能ParNew 老年代则使用功能CMS

CMS垃圾收集器老年代开启

  • -XX:+UseConcMarkSweepGC 关闭
  • -XX:-UseConcMarkSweepGC
  • -XX:MaxGCPauseMillis GC停顿时间垃圾收集器会尝试用各种手段达到这个时间比如减小年轻代
  • -XX:+UseCMSCompactAtFullCollection 用于在CMS收集器不得不进行FullGC时开启内存碎片的合并整理过程由于这个内存整理必须移动存活对象在Shenandoah和ZGC出现前是无法并发的
  • -XXCMSFullGCsBefore-Compaction 多少次FullGC之后压缩一次默认值为0表示每次进入FullGC时都进行碎片整理
  • -XX:CMSInitiatingOccupancyFraction 当老年代使用达到该比例时会触发FullGC默认是92
  • -XX:+UseCMSInitiatingOccupancyOnly 这个参数搭配上面那个用表示是不是要一直使用上面的比例触发FullGC如果设置则只会在第一次FullGC的时候使用-XX:CMSInitiatingOccupancyFraction的值之后会进行自动调整
  • -XX:+CMSScavengeBeforeRemark 在FullGC前启动一次MinorGC目的在于减少老年代对年轻代的引用降低CMSGC的标记阶段时的开销一般CMS的GC耗时80%都在标记阶段
  • -XX:+CMSParallellnitialMarkEnabled 默认情况下初始标记是单线程的这个参数可以让他多线程执行可以减少STW
  • -XX:+CMSParallelRemarkEnabled 使用多线程进行重新标记目的也是为了减少STW

G1垃圾收集器开启

  • -XX:+UseG1GC 关闭
  • -XX:-UseG1GC
  • -XXG1HeapRegionSize 设置每个Region的大小取值范围为1MB32MB
  • -XXMaxGCPauseMillis 设置垃圾收集器的停顿时间默认值是200毫秒通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的

JDK Tools

jps

用于显示当前用户下的所有java进程信息

# jps [options] [hostid] 
# q:仅输出VM标识符, m: 输出main method的参数,l:输出完全的包名, v:输出jvm参数
[root@localhost ~]# jps -l
28729 sun.tools.jps.Jps
23789 cn.ms.test.DemoMain
23651 cn.ms.test.TestMain

jstat

用于监视虚拟机运行时状态信息类装载、内存、垃圾收集、JIT编译等运行数据

-gc:垃圾回收统计(大小)

# 每隔2000ms输出<pid>进程的gc情况一共输出2次
[root@localhost ~]# jstat -gc <pid> 2000 2
# 每隔2s输出<pid>进程的gc情况每个3条记录就打印隐藏列标题
[root@localhost ~]# jstat -gc -t -h3 <pid> 2s
Timestamp        S0C    S1C    S0U    S1U    ... YGC     YGCT    FGC    FGCT     GCT   
         1021.6 1024.0 1024.0  0.0   1024.0  ...  1    0.012   0      0.000    0.012
         1023.7 1024.0 1024.0  0.0   1024.0  ...  1    0.012   0      0.000    0.012
         1025.7 1024.0 1024.0  0.0   1024.0  ...  1    0.012   0      0.000    0.012
Timestamp        S0C    S1C    S0U    S1U    ... YGC     YGCT    FGC    FGCT     GCT   
         1027.7 1024.0 1024.0  0.0   1024.0  ...  1    0.012   0      0.000    0.012
         1029.7 1024.0 1024.0  0.0   1024.0  ...  1    0.012   0      0.000    0.012
# 结果说明: C即Capacity 总容量U即Used 已使用的容量
##########################
# S0C年轻代中第一个survivor幸存区的容量 (kb)
# S1C年轻代中第二个survivor幸存区的容量 (kb)
# S0U年轻代中第一个survivor幸存区目前已使用空间 (kb)
# S1U年轻代中第二个survivor幸存区目前已使用空间 (kb)
# EC年轻代中Eden伊甸园的容量 (kb)
# EU年轻代中Eden伊甸园目前已使用空间 (kb)
# OCOld代的容量 (kb)
# OUOld代目前已使用空间 (kb)
# PCPerm(持久代)的容量 (kb)
# PUPerm(持久代)目前已使用空间 (kb)
# YGC从应用程序启动到采样时年轻代中gc次数
# YGCT从应用程序启动到采样时年轻代中gc所用时间(s)
# FGC从应用程序启动到采样时old代(全gc)gc次数
# FGCT从应用程序启动到采样时old代(全gc)gc所用时间(s)
# GCT从应用程序启动到采样时gc用的总时间(s)

-gcutil:垃圾回收统计(百分比)

[root@localhost bin]# jstat -gcutil <pid>
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  99.80  16.21  26.18  93.34  90.74      9    0.056     2    0.045    0.102
# 结果说明
##########################
# S0年轻代中第一个survivor幸存区已使用的占当前容量百分比
# S1年轻代中第二个survivor幸存区已使用的占当前容量百分比
# E年轻代中Eden伊甸园已使用的占当前容量百分比
# Oold代已使用的占当前容量百分比
# Pperm代已使用的占当前容量百分比
# YGC从应用程序启动到采样时年轻代中gc次数
# YGCT从应用程序启动到采样时年轻代中gc所用时间(s)
# FGC从应用程序启动到采样时old代(全gc)gc次数
# FGCT从应用程序启动到采样时old代(全gc)gc所用时间(s)
# GCT从应用程序启动到采样时gc用的总时间(s)

-gccapacity:堆内存统计

[root@localhost ~]# jstat -gccapacity <pid>
 NGCMN    NGCMX     NGC     S0C   S1C       EC      OGCMN      OGCMX       OGC         OC      PGCMN    PGCMX     PGC       PC     YGC    FGC 
 84480.0 1349632.0 913408.0 54272.0 51200.0 502784.0   168448.0  2699264.0   168448.0   168448.0  21504.0  83968.0  51712.0  51712.0      9     0
# 结果说明
##########################
# NGCMN年轻代(young)中初始化(最小)的大小 (kb)
# NGCMX年轻代(young)的最大容量 (kb)
# NGC年轻代(young)中当前的容量 (kb)
# S0C年轻代中第一个survivor幸存区的容量 (kb)
# S1C年轻代中第二个survivor幸存区的容量 (kb)
# EC年轻代中Eden伊甸园的容量 (kb)
# OGCMNold代中初始化(最小)的大小 (kb)
# OGCMXold代的最大容量 (kb)
# OGCold代当前新生成的容量 (kb)
# OCOld代的容量 (kb)
# PGCMNperm代中初始化(最小)的大小 (kb)
# PGCMXperm代的最大容量 (kb)
# PGCperm代当前新生成的容量 (kb)
# PCPerm(持久代)的容量 (kb)
# YGC从应用程序启动到采样时年轻代中gc次数
# GCT从应用程序启动到采样时gc用的总时间(s)

-gccause:垃圾收集统计概述(同-gcutil附加最近两次垃圾回收事件的原因

[root@localhost ~]# jstat -gccause <pid>
  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC       
  0.00  79.23  39.37  39.92  99.74      9    0.198     0    0.000    0.198 Allocation Failure   No GC
# 结果说明
##########################
# LGCC最近垃圾回收的原因
# GCC当前垃圾回收的原因

jstack

jstack(Java Stack Trace)主要用于打印线程的堆栈信息是JDK自带的很强大的线程分析工具可以帮助我们排查程序运行时的线程状态、死锁状态等。

# dump出进程<pid>的线程堆栈快照至/data/1.log文件中
jstack -l <pid> >/data/1.log

# 参数说明:
# -F如果正常执行jstack命令没有响应比如进程hung住了可以加上此参数强制执行thread dump
# -m除了打印Java的方法调用栈之外还会输出native方法的栈帧
# -l打印与锁有关的附加信息。使用此参数会导致JVM停止时间变长在生产环境需慎用

jstack dump文件中值得关注的线程状态有

  • 死锁Deadlock —— 重点关注
  • 执行中Runnable
  • 等待资源Waiting on condition —— 重点关注
    • 等待某个资源或条件发生来唤醒自己。具体需结合jstacktrace来分析如线程正在sleep网络读写繁忙而等待
    • 如果大量线程在“waiting on condition”并且在等待网络资源可能是网络瓶颈的征兆
  • 等待获取监视器Waiting on monitor entry —— 重点关注
    • 如果大量线程在“waiting for monitor entry”可能是一个全局锁阻塞住了大量线程
  • 暂停Suspended
  • 对象等待中Object.wait() 或 TIMED_WAITING
  • 阻塞Blocked —— 重点关注
  • 停止Parked

注意如果某个相同的call stack经常出现 我们有80%的以上的理由确定这个代码存在性能问题(读网络的部分除外)。

场景一分析BLOCKED问题

"RMI TCP Connection(267865)-172.16.5.25" daemon prio=10 tid=0x00007fd508371000 nid=0x55ae waiting for monitor entry [0x00007fd4f8684000]
   java.lang.Thread.State: BLOCKED (on object monitor)
at org.apache.log4j.Category.callAppenders(Category.java:201)
- waiting to lock <0x00000000acf4d0c0> (a org.apache.log4j.Logger)
at org.apache.log4j.Category.forcedLog(Category.java:388)
at org.apache.log4j.Category.log(Category.java:853)
at org.apache.commons.logging.impl.Log4JLogger.warn(Log4JLogger.java:234)
at com.tuan.core.common.lang.cache.remote.SpyMemcachedClient.get(SpyMemcachedClient.java:110)
  • 线程状态是 Blocked阻塞状态。说明线程等待资源超时
  • “ waiting to lock <0x00000000acf4d0c0>”指,线程在等待给这个 0x00000000acf4d0c0 地址上锁英文可描述为trying to obtain 0x00000000acf4d0c0 lock
  • 在 dump 日志里查找字符串 0x00000000acf4d0c0发现有大量线程都在等待给这个地址上锁。如果能在日志里找到谁获得了这个锁如locked < 0x00000000acf4d0c0 >),就可以顺藤摸瓜了
  • “waiting for monitor entry”说明此线程通过 synchronized(obj) {……} 申请进入了临界区从而进入了下图1中的“Entry Set”队列但该 obj 对应的 monitor 被其他线程拥有,所以本线程在 Entry Set 队列中等待
  • 第一行里,"RMI TCP Connection(267865)-172.16.5.25"是 Thread Name 。tid指Java Thread id。nid指native线程的id。prio是线程优先级。[0x00007fd4f8684000]是线程栈起始地址。

场景二分析CPU过高问题

1.top命令找出最高占用的进程Shift+P

2.查看高负载进程下的高负载线程top -Hp 或ps -mp -o THREAD,tid,time

3.找出最高占用的线程并记录thread_id把线程号进行换算成16进制编号printf "%X\n" thread_id

4.可选执行查看高负载的线程名称jstack 16143 | grep 3fb6

5.导出进程的堆栈日志找到3fb6 这个线程号jstack 16143 >/home/16143.log

6.根据找到的堆栈信息关联到代码进行定位分析即可

jmap

jmap(Java Memory Map)主要用于打印内存映射。常用命令:

jmap -dump:live,format=b,file=xxx.hprof <pid>

查看JVM堆栈的使用情况

[root@localhost ~]# jmap -heap 7243
Attaching to process ID 27900, please wait...
Debugger attached successfully.
Client compiler detected.
JVM version is 20.45-b01
using thread-local object allocation.
Mark Sweep Compact GC
Heap Configuration: #堆内存初始化配置
   MinHeapFreeRatio = 40     #-XX:MinHeapFreeRatio设置JVM堆最小空闲比率  
   MaxHeapFreeRatio = 70   #-XX:MaxHeapFreeRatio设置JVM堆最大空闲比率  
   MaxHeapSize = 100663296 (96.0MB)   #-XX:MaxHeapSize=设置JVM堆的最大大小
   NewSize = 1048576 (1.0MB)     #-XX:NewSize=设置JVM堆的新生代的默认大小
   MaxNewSize = 4294901760 (4095.9375MB) #-XX:MaxNewSize=设置JVM堆的新生代的最大大小
   OldSize = 4194304 (4.0MB)  #-XX:OldSize=设置JVM堆的老生代的大小
   NewRatio = 2    #-XX:NewRatio=:‘新生代’和‘老生代’的大小比率
   SurvivorRatio = 8  #-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值
   PermSize = 12582912 (12.0MB) #-XX:PermSize=<value>:设置JVM堆的持久代的初始大小  
   MaxPermSize = 67108864 (64.0MB) #-XX:MaxPermSize=<value>:设置JVM堆的持久代的最大大小  
Heap Usage:
New Generation (Eden + 1 Survivor Space): #新生代区内存分布,包含伊甸园区+1个Survivor区
   capacity = 30212096 (28.8125MB)
   used = 27103784 (25.848182678222656MB)
   free = 3108312 (2.9643173217773438MB)
   89.71169693092462% used
Eden Space: #Eden区内存分布
   capacity = 26869760 (25.625MB)
   used = 26869760 (25.625MB)
   free = 0 (0.0MB)
   100.0% used
From Space: #其中一个Survivor区的内存分布
   capacity = 3342336 (3.1875MB)
   used = 234024 (0.22318267822265625MB)
   free = 3108312 (2.9643173217773438MB)
   7.001809512867647% used
To Space: #另一个Survivor区的内存分布
   capacity = 3342336 (3.1875MB)
   used = 0 (0.0MB)
   free = 3342336 (3.1875MB)
   0.0% used
PS Old Generation: #当前的Old区内存分布
   capacity = 67108864 (64.0MB)
   used = 67108816 (63.99995422363281MB)
   free = 48 (4.57763671875E-5MB)
   99.99992847442627% used
PS Perm Generation: #当前的 “持久代” 内存分布
   capacity = 14417920 (13.75MB)
   used = 14339216 (13.674942016601562MB)
   free = 78704 (0.0750579833984375MB)
   99.45412375710227% used

新生代内存回收就是采用空间换时间方式如果from区使用率一直是100% 说明程序创建大量的短生命周期的实例使用jstat统计jvm在内存回收中发生的频率耗时以及是否有full gc使用这个数据来评估一内存配置参数、gc参数是否合理。

统计一【jmap -histo】:统计所有类的实例数量和所占用的内存容量

[root@localhost ~]# jmap -histo 7243
 num     #instances         #bytes  class name
----------------------------------------------
   1:          8969       19781168  [B
   2:          1835        2296720  [I
   3:         19735        2050688  [C
   4:          3448         385608  java.lang.Class
   5:          3829         371456  [Ljava.lang.Object;
   6:         14634         351216  java.lang.String
   7:          6695         214240  java.util.concurrent.ConcurrentHashMap$Node
   8:          6257         100112  java.lang.Object
   9:          2155          68960  java.util.HashMap$Node
  10:           723          63624  java.lang.reflect.Method
  11:            49          56368  [Ljava.util.concurrent.ConcurrentHashMap$Node;
  12:           830          46480  java.util.zip.ZipFile$ZipFileInputStream
  13:          1146          45840  java.lang.ref.Finalizer
  ......

统计二【jmap -histo】查看对象数最多的对象并过滤Map关键词然后按降序排序输出

[root@localhost ~]# jmap -histo 7243 |grep Map|sort -k 2 -g -r|less
Total         96237       26875560
   7:          6695         214240  java.util.concurrent.ConcurrentHashMap$Node
   9:          2155          68960  java.util.HashMap$Node
  18:           563          27024  java.util.HashMap
  21:           505          20200  java.util.LinkedHashMap$Entry
  16:           337          34880  [Ljava.util.HashMap$Node;
  27:           336          16128  gnu.trove.THashMap
  56:           163           6520  java.util.WeakHashMap$Entry
  60:           127           6096  java.util.WeakHashMap
  38:           127          10144  [Ljava.util.WeakHashMap$Entry;
  53:           126           7056  java.util.LinkedHashMap
......

统计三【jmap -histo】统计实例数量最多的前10个类

[root@localhost ~]# jmap -histo 7243 | sort -n -r -k 2 | head -10
 num     #instances         #bytes  class name
----------------------------------------------
Total         96237       26875560
   3:         19735        2050688  [C
   6:         14634         351216  java.lang.String
   1:          8969       19781168  [B
   7:          6695         214240  java.util.concurrent.ConcurrentHashMap$Node
   8:          6257         100112  java.lang.Object
   5:          3829         371456  [Ljava.lang.Object;
   4:          3448         385608  java.lang.Class
   9:          2155          68960  java.util.HashMap$Node
   2:          1835        2296720  [I

统计四【jmap -histo】统计合计容量最多的前10个类

[root@localhost ~]# jmap -histo 7243 | sort -n -r -k 3 | head -10
 num     #instances         #bytes  class name
----------------------------------------------
Total         96237       26875560
   1:          8969       19781168  [B
   2:          1835        2296720  [I
   3:         19735        2050688  [C
   4:          3448         385608  java.lang.Class
   5:          3829         371456  [Ljava.lang.Object;
   6:         14634         351216  java.lang.String
   7:          6695         214240  java.util.concurrent.ConcurrentHashMap$Node
   8:          6257         100112  java.lang.Object
   9:          2155          68960  java.util.HashMap$Node

dump注意事项

  • 在应用快要发生FGC的时候把堆数据导出来

    老年代或新生代used接近100%时就表示即将发生GC也可以再JVM参数中指定触发GC的阈值。

    • 查看快要发生FGC使用命令jmap -heap < pid >
    • 数据导出jmap -dump:format=b,file=heap.bin < pid >
  • 通过命令查看大对象jmap -histo < pid >|less

使用总结

  • 如果程序内存不足或者频繁GC很有可能存在内存泄露情况这时候就要借助Java堆Dump查看对象的情况
  • 要制作堆Dump可以直接使用jvm自带的jmap命令
  • 可以先使用jmap -heap命令查看堆的使用情况,看一下各个堆空间的占用情况
  • 使用jmap -histo:[live]查看堆内存中的对象的情况。如果有大量对象在持续被引用,并没有被释放掉,那就产生了内存泄露,就要结合代码,把不用的对象释放掉
  • 也可以使用 jmap -dump:format=b,file=<fileName>命令将堆信息保存到一个文件中再借助jhat命令查看详细内容
  • 在内存出现泄露、溢出或者其它前提条件下建议多dump几次内存把内存文件进行编号归档便于后续内存整理分析
  • 在用cms gc的情况下执行jmap -heap有些时候会导致进程变T因此强烈建议别执行这个命令如果想获取内存目前每个区域的使用状况可通过jstat -gc或jstat -gccapacity来拿到

jhat

jhatJVM Heap Analysis Tool命令是与jmap搭配使用用来分析jmap生成的dumpjhat内置了一个微型的HTTP/HTML服务器生成dump的分析结果后可以在浏览器中查看。在此要注意一般不会直接在服务器上进行分析因为jhat是一个耗时并且耗费硬件资源的过程一般把服务器生成的dump文件复制到本地或其他机器上进行分析。

# 解析Java堆转储文件,并启动一个 web server
jhat heapDump.dump

jconsole

jconsole(Java Monitoring and Management Console)是一个Java GUI监视工具可以以图表化的形式显示各种数据并可通过远程连接监视远程的服务器VM。用java写的GUI程序用来监控VM并可监控远程的VM非常易用而且功能非常强。命令行里打jconsole选则进程就可以了。

第一步在远程机的tomcat的catalina.sh中加入配置

JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=192.168.202.121 -Dcom.sun.management.jmxremote"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=12345"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=true"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.pwd.file=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.101-3.b13.el7_2.x86_64/jre/lib/management/jmxremote.password"

第二步:配置权限文件

[root@localhost bin]# cd /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.101-3.b13.el7_2.x86_64/jre/lib/management/
[root@localhost management]# cp jmxremote.password.template jmxremote.password
[root@localhost management]# vi jmxremote.password

monitorRole QED controlRole chenqimiao

第三步配置权限文件为600

[root@localhost management]# chmod 600 jmxremote.password jmxremote.access

这样基本配置就结束了下面说两个坑第一个就是防火墙的问题要开放指定端口的防火墙我这里配置的是12345端口第二个是hostname的问题

jconsole-ip2

请将127.0.0.1修改为本地真实的IP,我的服务器IP是192.168.202.121

jconsole-ip

第四步查看JConsole

JConsole-新建连接

JConsole-Console

jvisualvm

jvisualvm(JVM Monitoring/Troubleshooting/Profiling Tool)同jconsole都是一个基于图形化界面的、可以查看本地及远程的JAVA GUI监控工具Jvisualvm同jconsole的使用方式一样直接在命令行打入Jvisualvm即可启动不过Jvisualvm相比界面更美观一些数据更实时。 jvisualvm的使用VisualVM进行远程连接的配置和JConsole是一摸一样的最终效果图如下

jvisualvm

Visual GC(监控垃圾回收器)

Java VisualVM默认没有安装Visual GC插件需要手动安装JDK的安装目录的bin目露下双击 jvisualvm.sh即可打开Java VisualVM点击菜单栏 工具->插件 安装Visual GC最终效果如下图所示

Visual-GC

大dump文件

从服务器dump堆内存后文件比较大5.5G左右加载文件、查看实例对象都很慢还提示配置xmx大小。表明给VisualVM分配的堆内存不够找到$JAVA_HOME/lib/visualvm}/etc/visualvm.conf这个文件修改

default_options="-J-Xms24m -J-Xmx192m"

再重启VisualVM就行了。

jmc

jmcJava Mission Control是JDK自带的一个图形界面监控工具监控信息非常全面。JMC打开性能日志后主要包括一般信息、内存、代码、线程、I/O、系统、事件 功能。

jmc-main

JMC的最主要的特征就是JFRJava Flight Recorder是基于JAVA的飞行记录器JFR的数据是一些列JVM事件的历史纪录可以用来诊断JVM的性能和操作收集后的数据可以使用JMC来分析。

启动JFR

在商业版本上面JFR默认是关闭的可以通过在启动时增加参数 -XX:+UnlockCommercialFeatures -XX:+FlightRecorder 来启动应用。启动之后也只是开启了JFR特性但是还没有开始进行事件记录。这就要通过GUI和命令行了。

  • 通过Java Mission Control启动JFR

    打开Java Mission Control点击对应的JVM启动即可事件记录有两种模式如果选择第2种模式那么JVM会使用一个循环使用的缓存来存放事件数据

    • 记录固定一段时间的事件比如1分钟
    • 持续进行记录
  • 通过命令行启动JFR

    通过在启动的时候,增加参数:-XX:+FlightRecorderOptions=string 来启动真正地事件记录,这里的 string 可以是以下值下列参数都可以使用jcmd命令在JVM运行的时候进行动态调整参考地址

    • name=name标识recording的名字一个进程可以有多个recording存在它们使用名字来进行区分
    • defaultrecording=<ture|false>是否启动recording默认是false我们要分析必须要设置为true
    • setting=paths包含JFR配置的文件名字
    • delay=time启动之后经过多长时间比如30s1h开始进行recording
    • duration=time做多长时间的recording
    • filename=pathrecordding记录到那个文件里面
    • compress=<ture|false>是否对recording进行压缩gzip,默认为false
    • maxage=time:在循环使用的缓存中,事件数据保存的最大时长
    • maxsize=size事件数据缓存的最大大小比如1024k1M

常用JFR命令

  • 启动recording

    命令格式:jcmd process_id JFR.start [options_list]其中options_list就是上述的参数值。

  • dump出循环缓存中的数据

    命令格式:jcmd process_id JFR.dump [options_list]其中options_list参数的可选值如下

    • name=namerecording的名字
    • recording=nJFR recording的数字一个标识recording的随机数
    • filename=pathdump文件的保存路径
  • 查看进程中所有recording

    命令格式: jcmd process_id JFR.check [verbose]不同recording使用名字进行区分同时JVM还为它分配一个随机数。

  • 停止recording

    命令格式: jcmd process_id JFR.stop [options_list]其中options_list参数的可选值如下

    • name=name要停止的recording名字
    • recording=n要停止的recording的标识数字
    • discard=boolean如果为true数据被丢弃而不是写入下面指定的文件当中
    • filename=path:写入数据的文件名称

命令启动JFR案例

  • 第一步创建一个包含了你自己配置的JFR模板文件template.jfc。运行jmc, 然后Window->Flight Recording Template Manage菜单。准备好档案后就可以导出文件并移动到要排查问题的环境中

    jmc-jfc

  • 第二步由于JFR需要JDK的商业证书这一步需要解锁jdk的商业特性

    [root@localhost bin]# jcmd 12234 VM.unlock_commercial_features
    12234: Commercial Features already unlocked.
    
  • 第三步最后你就可以启动JFR命令格式如下

    jcmd <PID> JFR.start name=test duration=60s [settings=template.jfc] filename=output.jfr
    

    上述命令立即启动JFR并开始使用 template.jfc(在 $JAVA_HOME/jre/lib/jfr 下有 default.jfcprofile.jfc 模板)的配置收集 60s 的JVM信息输出到 output.jfr 中。一旦记录完成之后,就可以复制.jfr文件到你的工作环境使用jmc GUI来分析。它几乎包含了排查jvm问题需要的所有信息包括堆dump时的异常信息。使用案例如下

    [root@localhost bin]# jcmd 12234 JFR.start name=test duration=60s filename=output.jfr
    12234: Started recording 6. The result will be written to: /root/zookeeper-3.4.12/bin/output.jfr
    [root@localhost bin]# ls -l
    -rw-r--r-- 1 root root 298585 6  29 11:09 output.jfr
    

JFRJava Flight Recorder

  • Java Mission Control的最主要的特征就是Java Flight Recorder。正如它的名字所示JFR的数据是一些列JVM事件的历史纪录可以用来诊断JVM的性能和操作
  • JFR的基本操作就是开启哪些事件比如线程由于等待锁而阻塞的事件。当开启的事件发生了事件相关的数据会记录到内存或磁盘文件上。记录事件数据的缓存是循环使用的只有最近发生的事件才能够从缓存中找到之前的都因为缓存的限制被删除了。Java Mission Control能够对这些事件在界面上进行展示从JVM的内存中读取或从事件数据文件中读取我们可以通过这些事件来对JVM的性能进行诊断
  • 事件的类型、缓存的大小、事件数据的存储方式等等都是通过JVM参数、Java Mission Control的GUI界面、jcmd命令来控制的。JFR默认是编译进程序的因为它的开销很小一般来说对应用的影响小于1%。不过如果我们增加了事件的数目、修改了记录事件的阈值都有可能增加JFR的开销

JFR概况

下面对GlassFish web服务器进行JFR记录的例子在这个服务器上面运行着在第2章介绍的股票servlet。Java Mission Control加载完JFR获取的事件之后大概是下面这个样子

jfr-main

我们可以看到通过上图可以看到CPU使用率Heap使用率JVM信息System PropertiesJFR的记录情况等等。

JFR内存视图

Java Mission Control 可以看到非常多的信息下图只显示了一个标签的内容。下图显示了JVM 的内存波动非常频繁因为新生代经常被清除有意思的是head的大小并没有增长。下面左边的面板显示了最近一段时间的垃圾回收情况包括GC的时长和垃圾回收的类型。如果我们点击一个事件右边的面板会展示这个事件的具体情况包括垃圾垃圾回收的各个阶段及其统计信息。从面板的标签可以看到还有很多其它信息比如有多少对象被清除了花了多长时间GC算法的配置分配的对象信息等等。在第5章和第6章中我们会详细介绍。

jfr-memory

JFR 代码视图

这张图也有很多tab可以看到各个包的使用频率和类的使用情况、异常、编译、代码缓存、类加载情况等等

jfr-code

JFR事件视图

下图显示了事件的概述视图:

jfr-event

EclipseMAT

虽然Java虚拟机可以帮我们对内存进行回收但是其回收的是Java虚拟机不再引用的对象。很多时候我们使用系统的IO流、Cursor、Receiver如果不及时释放就会导致内存泄漏OOM。但是很多时候内存泄漏的现象不是很明显比如内部类、Handler相关的使用导致的内存泄漏或者你使用了第三方library的一些引用比较消耗资源但又不是像系统资源那样会引起你足够的注意去手动释放它们。以下通过内存泄漏分析、集合使用率、Hash性能分析和OQL快读定位空集合来使用MAT。

GC Roots

JAVA虚拟机通过可达性Reachability)来判断对象是否存活,基本思想:以”GC Roots”的对象作为起始点向下搜索搜索形成的路径称为引用链当一个对象到GC Roots没有任何引用链相连即不可达的则该对象被判定为可以被回收的对象反之不能被回收。GC Roots可以是以下任意对象

  • 一个在current thread当前线程的call stack调用栈上的对象如方法参数和局部变量
  • 线程自身或者system class loader系统类加载器加载的类
  • native code本地代码保留的活动对象

内存泄漏

当对象无用了,但仍然可达(未释放),垃圾回收器无法回收。

Java四种引用类型

  • Strong References强引用

    普通的java引用我们通常new的对象就是StringBuffer buffer = new StringBuffer(); 如果一个对象通过一串强引用链可达,那么它就不会被垃圾回收。你肯定不希望自己正在使用的引用被垃圾回收器回收吧。但对于集合中的对象,应在不使用的时候移除掉,否则会占用更多的内存,导致内存泄漏。

  • Soft Reference软引用

    当对象是Soft Reference可达时gc会向系统申请更多内存而不是直接回收它当内存不足的时候才回收它。因此Soft Reference适合用于构建一些缓存系统比如图片缓存。

  • Weak Reference弱引用

    WeakReference不会强制对象保存在内存中。它拥有比较短暂的生命周期允许你使用垃圾回收器的能力去权衡一个对象的可达性。在垃圾回收器扫描它所管辖的内存区域过程中一旦gc发现对象是Weak Reference可达就会把它放到 Reference Queue等下次gc时回收它。

    系统为我们提供了WeakHashMap和HashMap类似只是其key使用了weak reference。如果WeakHashMap的某个key被垃圾回收器回收那么entity也会自动被remove。由于WeakReference被GC回收的可能性较大因此在使用它之前你需要通过weakObj.get()去判断目的对象引用是否已经被回收。一旦WeakReference.get()返回null它指向的对象就会被垃圾回收那么WeakReference对象就没有用了意味着你应该进行一些清理。比如在WeakHashMap中要把回收过的key从Map中删除掉避免无用的的weakReference不断增长。

    ReferenceQueue可以让你很容易地跟踪dead references。WeakReference类的构造函数有一个ReferenceQueue参数当指向的对象被垃圾回收时会把WeakReference对象放到ReferenceQueue中。这样遍历ReferenceQueue可以得到所有回收过的WeakReference。

  • Phantom Reference虚引用

    其余Soft/Weak Reference区别较大是它的get()方法总是返回null。这意味着你只能用Phantom Reference本身而得不到它指向的对象。当Weak Reference指向的对象变得弱可达(weakly reachable时会立即被放到ReferenceQueue中这在finalization、garbage collection之前发生。理论上你可以在finalize()方法中使对象“复活”使一个强引用指向它就行了gc不会回收它。但没法复活PhantomReference指向的对象。而PhantomReference是在garbage collection之后被放到ReferenceQueue中的没法复活。

MAT视图与概念

  • Shallow Heap

    Shallow Size就是对象本身占用内存的大小不包含其引用的对象内存实际分析中作用不大。 常规对象非数组的Shallow Size由其成员变量的数量和类型决定。数组的Shallow Size有数组元素的类型对象类型、基本类型和数组长度决定。案例如下

public class String {
    public final class String {8 Bytes header
    private char value[]; 4 Bytes
    private int offset; 4 Bytes
    private int count; 4 Bytes
    private int hash = 0; 4 Bytes
	// ......
}
// "Shallow size“ of a String ==24 Bytes12345678

Java的对象成员都是些引用。真正的内存都在堆上看起来是一堆原生的byte[]、char[]、int[]对象本身的内存都很小。所以我们可以看到以Shallow Heap进行排序的Histogram图中排在第一位第二位的是byte和char。

  • Retained Heap

    Retained Heap值的计算方式是将Retained Set中的所有对象大小叠加。或者说由于X被释放导致其它所有被释放对象包括被递归释放的所占的Heap大小。当X被回收时哪些将被GC回收的对象集合。比如:

    一个ArrayList持有100000个对象每一个占用16 bytes移除这些ArrayList可以释放16×100000+XX代表ArrayList的Shallow大小。相对于Shallow HeapRetained Heap可以更精确的反映一个对象实际占用的大小因为如果该对象释放Retained Heap都可以被释放

  • Histogram

    可列出每一个类的实例数。支持正则表达式查找也可以计算出该类所有对象的Retained Size。

mat-histogram

  • Dominator Tree

    对象之间dominator关系树。如果从GC Root到达Y的的所有path都经过X那么我们称X dominates Y或者X是Y的Dominator Dominator Tree由系统中复杂的对象图计算而来。从MAT的dominator tree中可以看到占用内存最大的对象以及每个对象的dominator。 我们也可以右键选择Immediate Dominator”来查看某个对象的dominator。

mat-dominator-tree

  • Path to GC Roots

    查看一个对象到RC Roots的引用链通常在排查内存泄漏的时候我们会选择exclude all phantom/weak/soft etc.references, 意思是查看排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收我们要看的就是某个对象否还存在Strong 引用链在导出HeapDump之前要手动出发GC来保证如果有则说明存在内存泄漏然后再去排查具体引用。

    mat-path-to-gc-roots

    查看当前Object所有引用,被引用的对象:

    • List objects with 以Dominator Tree的方式查看
      • incoming references 引用到该对象的对象
      • outcoming references 被该对象引用的对象
    • Show objects by class 以class的方式查看
      • incoming references 引用到该对象的对象
      • outcoming references 被该对象引用的对象
  • OQL(Object Query Language)

类似SQL查询语言ClassesTable、ObjectsRows、FiledsCols

select * from com.example.mat.Listener
# 查找size0并且未使用过的ArrayList
select * from java.util.ArrayList where size=0 and modCount=01
# 查找所有的Activity 
select * from instanceof android.app.Activity
  • 内存快照对比

方式一Compare To Another Heap Dump直接进行比较

mat-compare-to-another-heap-dump-1

mat-compare-to-another-heap-dump-2

mat-compare-to-another-heap-dump-3

方式二Compare Baseket更全面可以直接给出百分比

mat-compare-baseket-1

mat-compare-baseket-2

mat-compare-baseket-3

mat-compare-baseket-4

MAT内存分析实战

  • 实战一:内存泄漏分析

    查找导致内存泄漏的类。既然环境已经搭好heap dump也成功倒入接下来就去分析问题。

    • 查找目标类 如果在开发过程中你的目标很明确比如就是查找自己负责的服务那么通过包名或者Class筛选OQL搜索都可以快速定位到。点击OQL图标在窗口输入并按Ctrl + F5或者!按钮执行:

      select * from instanceof android.app.Activity

    • Paths to GC Rootsexclude all phantom/weak/soft etc.references

      查看一个对象到RC Roots是否存在引用链。要将虚引用/弱引用/软引用等排除,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收

    • 分析具体的引用为何没有被释放,并进行修复

小技巧:

  • 当目的不明确时可以直接定位到RetainedHeap最大的ObjectSelect incoming references查看引用链定位到可疑的对象然后Path to GC Roots进行引用链分析
  • 如果大对象筛选看不出区别可以试试按照class分组再寻找可疑对象进行GC引用链分析
  • 直接按照包名直接查看GC引用链可以一次性筛选多个类但是如下图所示选项是 Merge Shortest Path to GCRoots这个选项具体不是很明白不过也能筛选出存在GC引用链的类这种方式的准确性还待验证

mat-实践一

所以有时候进行MAT分析还是需要一些经验能够帮你更快更准确的定位。

  • 实战二:集合使用率分析

    集合在开发中会经常使用到如何选择合适的数据结构的集合初始容量是多少太小可能导致频繁扩容太大又会开销跟多内存。当这些问题不是很明确时或者想查看集合的使用情况时可以通过MAT来进行分析。

    • 筛选目标对象

      mat-实践二-1

    • Show Retained Set查找当X被回收时那些将被GC回收的对象集合

      mat-实践二-2

    • 筛选指定的ObjectHash MapArrayList并按照大小进行分组

      mat-实践二-3

    • 查看指定类的Immediate dominators

      mat-实践二-4

Collections fill ratio

这种方式只能查看那些具有预分配内存能力的集合比如HashMapArrayList。计算方式”size / capacity”

mat-实践二-5

mat-实践二-6

  • 实战三Hash相关性能分析

    当Hash集合中过多的对象返回相同Hash值的时候会严重影响性能Hash算法原理自行搜索这里来查找导致Hash集合的碰撞率较高的罪魁祸首。

    • Map Collision Ratio

      检测每一个HashMap或者HashTable实例并按照碰撞率排序碰撞率 = 碰撞的实体/Hash表中所有实体

      mat-实践三-1

    • 查看Immediate dominators

      mat-实践三-2

      mat-实践三-3

    • 通过HashEntries查看key value

      mat-实践三-4

    • Array等其它集合分析方法类似

  • 实战四通过OQL快速定位未使用的集合

    • 通过OQL查询empty并且未修改过的集合

      select * from java.util.ArrayList where size=0 and modCount=01
      select * from java.util.HashMap where size=0 and modCount=0
      select * from java.util.Hashtable where count=0 and modCount=012
      

      mat-实践四-1

    • Immediate dominators(查看引用者)

      mat-实践四-2

    • 计算空集合的Retained Size值查看浪费了多少内存

      mat-实践四-3

🔥火焰图

火焰图是用来分析程序运行瓶颈的工具。火焰图也可以用来分析 Java 应用。

环境安装

确认你的机器已经安装了git、jdk、perl、c++编译器

安装Perl

wget http://www.cpan.org/src/5.0/perl-5.26.1.tar.gz
tar zxvf perl-5.26.1.tar.gz
cd perl-5.26.1
./Configure -de
make
make test
make install

wget后面的路径可以按需更改。安装过程比较耗时间安装完成后可通过perl -version查看是否安装成功。

C++编译器

apt-get install g++

一般用于编译c++程序缺少这个编译器进行make编译c++代码时会报“g++: not found”的错误。

clone相关项目

下载下来所需要的两个项目这里建议放到data目录下

git clone https://github.com/jvm-profiling-tools/async-profiler
git clone https://github.com/brendangregg/FlameGraph

编译项目

下载好以后需要打开async-profiler文件输入make指令进行编译

cd async-profiler
make

生成文件

生成火焰图数据

可以从 github 上下载 async-profiler 的压缩包进行相关操作。进入async-profiler项目的目录下然后输入如下指令

./profiler.sh -d 60 -o collapsed -f /tmp/test_01.txt ${pid}

上面的-d表示的是持续时长后面60代表持续采集时间60s-o表示的是采集规范这里用的是collapsed-f后面的路径表示的是数据采集后生成的数据存放的文件路径这里放在了/tmp/test_01.txt${pid}表示的是采集目标进程的pid回车运行运行期间阻塞知道约定时间完成。运行完成后去tmp下看看有没有对应文件。

生成svg文件

上一步产生的文件里的内容,肉眼是很难看懂的,所以现在FlameGraph的作用就体现出来了,它可以读取该文件并生成直观的火焰图,现在进入该项目目录下面,执行如下语句:

perl flamegraph.pl --colors=java /tmp/test_01.txt > test_01.svg

因为是perl文件这里使用perl指令运行该文件后面--colors表示着色风格这里是java后面的是数据文件的路径这里是刚刚上面生成的那个文件/tmp/test_01.txt最后的test_01.svg就是最终生成的火焰图文件存放的路径和文件命名这里是命名为test_01.svg并保存在当前路径运行后看到该文件已经存在于当前目录下。

展示火焰图

现在下载下来该文件,使用浏览器打开,效果如下:

火焰图案例

火焰图案例

生成的火焰图案例如下:

火焰图案例

瓶颈点1

CoohuaAnalytics$KafkaConsumer:::send方法中Gzip压缩占比较高。已经定位到方法级别再看代码就快速很多直接找到具体位置找到第一个消耗大户Gzip压缩

火焰图-瓶颈点1

瓶颈点2

展开2这个波峰查看到这个getOurStackTrace方法占用了大比例的CPU怀疑代码里面频繁用丢异常的方式获取当前代码栈

火焰图-瓶颈点2

直接看代码:

火焰图-瓶颈点2-代码

果然如推断找到第二个CPU消耗大户new Exception().getStackTrace()。

瓶颈点3

展开波峰3可以看到是这个Gzip解压缩

火焰图-瓶颈点3

定位到具体的代码可以看到对每个请求的参数进行了gzip解压缩

火焰图-瓶颈点3-代码