JVM 性能调优

JVM 结构(Java 运行时数据结构)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Metaspace(方法区)

注意

  1. JDK7 及以前,方法区被习惯性称为永久代,JDK8 开始,使用元空间实现方法区。
    在这里插入图片描述
    在这里插入图片描述
    注意:从 JDK7 及以后版本的 HotSpot 虚拟机,静态「变量」随着 Class 对象放到 Java 堆中,但「变量」指向的对象一直都存放在 Java 堆中。

  2. 方法区用于存储类型信息、常量、静态变量、即时编译器编译的代码缓存等。

  3. static 变量在字节码加载到 JVM 的 初始化 阶段被赋值,static final 变量则是在编译阶段就被赋值。
    在这里插入图片描述

  4. 元空间取代永久代的原因:

    • 为永久代估计大小很困难,当永久代大小估计得太小时,JVM 加载太多 class 文件时会导致 Perm 区发生 OOM,而元空间位于 Java 堆外,默认只受本地内存大小的限制。
    • 对永久代进行调优很困难((比如 FullGC 太耗时,ROI 也不高)。
  5. 方法区垃圾收集主要集中在废弃的常量和不再使用的类型。

    • 常量:字面量和符号引用,HotSpot 规定常量池中的常量没有被任何地方引用就可以被回收。
      • 文本字符串、被申明为 final 的常量值等
      • 符号引用:1)类和接口的全限定名;2)字段的名称和描述符;3)方法的名称和描述符
    • 不再被使用的类允许被 JVM 回收的判断条件
      • 该类的所有实例都被回收,即 Java 堆中不存在该类及其任何派生子类的对象;
      • 加载该类的类加载器已经被回收;
      • 该类对应的 java.lang.Class 对象没有在任何地方被使用,无法在任何地方通过反射访问到该类的方法。

深入理解堆外内存 Metaspace

Metaspace 区域位于堆外(native heap),所以它的最大内存大小取决于系统内存而不是堆大小,我们可以指定 MaxMetaspaceSize 参数来限定它的最大内存。Metaspace 是用来存放 class metadata,即记录一个 Java 类在 JVM 中的信息,包括但不限于 JVM class file format 的运行时数据:
1、Klass 结构,这个非常重要,把它理解为一个 Java 类在虚拟机内部的表示;
2、method metadata,包括方法的字节码、局部变量表、异常表、参数信息等;
3、常量池;
4、注解;
5、方法计数器,记录方法被执行的次数,用来辅助 JIT 决策;
6、 其他

  1. 什么时候分配 Metaspace 空间
    虽然每个 Java 类都关联了一个 java.lang.Class 的实例,而且它是一个贮存在堆中的 Java 对象。但是类的 class metadata 不是一个 Java 对象,它不在堆中而是在 Metaspace 中。
    在这里插入图片描述
  2. 什么时候回收 Metaspace 空间
    分配给一个类的空间,是归属于这个类的类加载器的,只有当这个类加载器卸载的时候,这个空间才会被释放。所以,只有当这个类加载器加载的所有类都没有存活的对象,并且没有到达这些类和类加载器的引用时,相应的 Metaspace 空间才会被 GC 释放。看下图:
    在这里插入图片描述
    所以,一个 Java 类在 Metaspace 中占用的空间,它是否释放,取决于这个类的类加载器是否被卸载。
直接内存(本地内存,Direct Memory、Native Memory)
IO 与 NIO

IO 面向字节流(byte stream),
传统 IO:非直接缓冲区

NIO (New IO / Non-blocking IO)面向基于 buffer 的 channel。Java 中的直接内存就是通过堆中的 DirectByteBuffer 操作 Native 内存。
NIO:直接缓冲区

PC寄存器

在这里插入图片描述

Java 虚拟机栈

在 JVM 中,一个线程就对应一个 Java 虚拟机栈,一个栈由多个栈桢组成,一个栈桢对应一个方法,一个栈帧可能有多个 OopMap。
Java 虚拟机栈栈桢结构

  • 局部变量表(一维数字数组):存储方法参数和方法内的局部变量,包括基本数据类型和对象引用,以及 returnAddress
  • 动态链接:指向运行时常量池的方法引用
  • 方法返回地址:方法正常退出或异常退出的定义
  • 局部变量与操作数栈
    • 协同工作过程:
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  1. 在源代码文件在编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在 class 文件的常量池中。
    常量池中以及符号引用理解
    注意:常量池按照 JVM 规范,是放在方法区中的。

  2. 描述一个方法调用另外的方法时,就是使用常量池中指向方法的符号引用来表示的。
    在这里插入图片描述

  3. 动态链接就是将这些符号引用转换为调用方法的直接「引用」。
    在这里插入图片描述

  4. 方法调用:在 JVM 中,将符号引用转化为调用方法的直接引用与方法的绑定机制有关

    • 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行时保持不变,就会将调用方法的符号引用转换为直接引用。比如 invokestatic、invokespecial (代码中调用某个类的构造器,在编译后会得到这个指令)这些都是 JVM 实现静态链接的指令。
      1. 非虚方法:静态方法、私有方法、final 方法、实例构造器、父类方法
    • 动态链接:被调用的方法在编译期无法被确定下来,只能在程序运行期间才能将符号引用转换为直接引用。比如 invokevirtual、invokeinterface(代码中通过接口引用调用方法,在编译后会的到这个指令)、invokedynamic(JDK 7 引入,JDK 8 中的 Lambda 就是通过生成该指令来实现的)都是 JVM 实现动态链接的指令。
      1. 虚方法:除非虚方法的其它方法

小知识点:

在 Java 字节码中与调用相关的指令共有五种:

  1. invokestatic:用于调用静态方法。
  2. invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
  3. invokevirtual:用于调用非私有实例方法。
  4. invokeinterface:用于调用接口方法。
  5. invokedynamic:用于调用动态方法。
  1. 动态分派中的方法重写的本质
    1. 找到操作数栈栈顶元素所执行对象的实际类型,计作 C;
    2. 如果在 C 中找到与期望的调用方法的方法签名相同的方法,则进行访问权限校验,通过则返回该方法的直接引用;
    3. 否则按照继承关系,从下往上对 C 的各个父类中进行第 2 步操作的查找与权限验证过程
方法表

为了提高性能,JVM 在类的方法区引入了一个虚方法表(非虚方法不会出现在这个表中,虚方法表在 JVM 加载字节码文件的链接阶段被创建和初始化)来实现动态分派。

方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。 这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。

方法表满足两个特质:

  1. 子类方法表中包含父类方法表中的所有方法;
  2. 子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。

如前文所述,方法调用指令中的符号引用会在执行之前解析成实际引用。对于静态绑定的方法调用而言,实际引用将指向具体的目标方法。对于动态绑定的方法调用而言,实际引用则是方法表的索引值(实际上并不仅是索引值)。 在执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。

使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者,读取调用者的动态类型,读取该类型的方法表,读取方法表中某个索引值所对应的目标方法。相对于创建并初始化 Java 栈帧来说,这几个内存解引用操作的开销简直可以忽略不计。

内联缓存

内联缓存是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。

对象创建

对象创建过程

  1. 当遇到 new 指令时,虚拟机会首先检查能否在 Metaspace 常量池中根据参数找到对应类的符号引用,并检查这个符号引用是否已经被加载、解析以及初始化。若无,则会在双亲委派机制下使用当前类加载器以 cloassloader + 包名 + 类名为 key 查找对应的 class 文件,如果找不到对应的文件,则抛出 ClassNotFoundException,否则进行类加载生成对应的 Class 对象。
    Java 对象内存布局
    对象结构

对象头

在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机对象的对象头部分包括两类信息。

  1. 第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、 GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为 “Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32、 64位Bitmap结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率, Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下, Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码, 4个比特用于存储对象分代年龄, 2个比特用于存储锁标志位, 1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、 GC标记、可偏向) 下对象的存储内容如表2-1所示。
    在这里插入图片描述
  2. 对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针, Java虚拟机通过这个指针来确定该对象是哪个类的实例。
  3. 此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。

即若对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。

在32位虚拟机中,1字宽等于4字节,即32bit,在32位虚拟机中,1字宽等于8字节,即64bit,

hashCode 源码流程分析

在这里插入图片描述

文章索引
  1. 《深入理解Java虚拟机:周志华》
  2. hotspot中java对象默认hashcode的生成方式

java中,计算对象的默认hashcode的方法主要在synchronizer.cpp文件中。对象的hashcode并不是在创建对象时,而是在首次调用hashCode方法时进行计算的,并存储在对象头的标记字中的。

  1. hashCode 源码解析

执行引擎

在这里插入图片描述
将字节码文件解释/编译为对应平台可识别的机器指令。
在这里插入图片描述

解释器 与 JIT 编译器

  1. 参数设置模式
    • -Xint:完全采用解释器模式执行程序;
    • -Xcomp:完全采用 JIT 模式执行程序。注:当 JIT 出现问题时,解释器也会介入执行。
    • -Xmixed(默认):解释器与 JIT 混合模式执行。
  2. 解释器:根据预定义的规范将字节码中的内容逐行翻译成本地机器指令执行。
  3. JIT (Just In Time)编译器:进过热点代码分析后,将源代码直接编译成本地机器指令,放入到方法区中的 CodeCache 中进行缓存。由于编译发生在代码执行过程中,所以也被称为栈上替换(OSR:On Stack Replacement)。
    • 热点代码:一个被多次调用的代码,或方法体内部循环次数较多的循环体都可以称为“热点代码”。目前 HotSpot VM 采用的是基于计数器的热点探测,即其会为每个方法都建立两个不同类型的计数器,分别为方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。
      • 方法调用计数器:统计方法的调用次数。在 Client 模式下默认阈值为 1500 次,在 Server 模式下默认阈值为 10000 次,超过阈值就会触发 JIT 编译。也可以通过参数 -XX:CompileThreshhold 设定。当一个方法被调用时,HotSpot VM 会先检查该方法是否存在 JIT 编译的版本,如果存在则优先使用被编译的本地代码来执行;如果不存在则将此方法的方法调用计数器数值加一,然后判断方法计数器数值和回边计数器数值之后是否大于方法调用计数器的阈值,如果超过就会向 JIT 提交一个该方法的代码编译请求。
        在这里插入图片描述
        • 热度衰减:若不做任何设置,方法调用计数器并不是统计的是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。当超过一定的时间限度,方法调用次数仍不足以将其提交给 JIT 进行编译,那这个方法的调用计数器的值就会减少一半,这个过程被称为方法调用技术的热度衰减。进行热度衰减的动作是虚拟机进行 GC 时顺便进行的,可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减,这样只要系统运行足够长的时间,绝大部分方法都会被编译成本地代码。另外也可以使用 -XX:CounterHalfLifeTime 来设置半衰期时间(单位是秒)。
      • 回边计数器:统计循环体的循环次数
        在这里插入图片描述
  • HotSpot VM 内嵌有两个 JIT 编译器,分别为 Clienet Compiler(C1 编译器) 和 Server Compiler(C2 编译器)
    • -client 指定为 C1 编译器(Client 模式),会对字节码进行耗时较短的简单可靠优化,以达到更快的编译速度。优化策略有:
      • 方法内联:将引用的方法代码编译到引用点处,这样可以减少栈桢的生成,参数传递以及跳转过程。
      • 去虚拟化:对唯一的实现类进行内联。
      • 冗余消除:在运行期间折叠一些不会被执行的代码。
    • -server 指定 C2 编译器(Server 模式,64bit 操作系统只能为 Server 模式),会对字节码进行耗时较长的激进优化,使得代码的执行效率更高。
      在这里插入图片描述
      C2 主要是在全局层面进行优化,逃逸分析是其优化的基础。
      • 栈上替换:对于未逃逸的对象将其分配在栈上而非堆上。
      • 标量替换:用标量值代替聚合对象的属性值。
      • 同步消除:清除同步操作,通常指 synchronized。

Java Escape Analysis

  1. GlobalEscape:an object escapes the method and thread. For example, an object stored in a static field or a field of an escaped object, or returned as the result of the current method.
  2. ArgEscape:an object that is passed as an argument to a method but cannot otherwise be observed outside the method or by other threads (the object escapes that method via being passed as method arguments, but does not escape the thread in which it is created).
  3. NoEscpae:an object does not escape the method that creates it, and the thread in which the method is invoked. In such cases, the objects are scalar replaceable objects, meaning their allocation could be removed from generated code. For example, when an object is created locally in a method and the method does not create any other thread objects, the object is a ‘NoEscape’ one and cannot be observed outside the current method or thread.

注意 JDK7 之后,一旦通过 -server 显示开启 Server 模式,HotSpot VM 会默认开启分层编译策略,由 C1 和 C2 相互协作共同进行编译任务。

  • JDK10 起,引入了 Graal 编译器

  • JDK9 引入了 AOT(Ahead Of Timer)编译器,通过 jaotc 工具在程序运行之前就将字节码转化为本地机器码,并将其放到动态共享库中。(.java -> .class -> .so)。缺点有:1)打破了 Java 一次编译处处运行的理念;2)降低了 Java 的动态链接性;3)目前只支持 Linux X64 Java base

垃圾回收算法

GC Roots

在这里插入图片描述

基础知识

  1. 引用计数:每个对象都维护一个自己被多少对象引用的计数器,即有新对象引用它时,计数器加一,当引用失效时,计数器减一,任何时刻计数器为零的对象就是应该被回收的对象。这种算法不管在实现上还是理解成本上都比较低,但不能处理一些特殊情况:两个本应该被回收的对象相互引用,根据引用计数器算法,这两个对象都不能被回收。
  2. 可达性分析:通过一系列被称为“GC Roots”的根对象作为引用的起始点,根据引用关系向下进行搜索,搜索所走过的路径称为“引用链”,若一个对象到 GC Roots 之间没有引用链相连,就认为该对象是可以被回收的。

Java 体系里可以作为 GC Roots 的对象:

  1. 在虚拟机栈(栈桢中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
    NullPointExcepiton、OutOfM emoryError)等,还有系统类加载器。
  6. 所有被同步锁(synchronized关键字)持有的对象。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  1. 并发标记算法 - 三色标记

首先,我们将对象分成三种类型的。

  • 黑色:根对象,或者该对象与它的子对象都被扫描
  • 灰色:对象本身被扫描,但还没扫描完该对象中的子对象
  • 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象。

当GC开始扫描对象时,按照如下图步骤进行对象的扫描:

  1. 根对象被置为黑色,子对象被置为灰色。
    在这里插入图片描述
  2. 继续由灰色遍历,将已扫描了子对象的对象置为黑色,遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。
    在这里插入图片描述
    这个过程看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到对象丢失问题。我们看下面一种情况,当垃圾收集器扫描到下面情况时:
    在这里插入图片描述
    这时候应用程序执行了以下操作:
A.c=C
B.c=null

这时候垃圾收集器再标记扫描的时候,就会将 C 标记为白色,即会被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下两种可行的方式:

  • 在插入的时候记录对象
  • 在删除的时候记录对象

这刚好对应CMS和G1的两种不同实现方式:

  • 在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
  • 在G1中,使用的是STAB(snapshot-at-the-beginning,效率更高)的方式,删除的时候记录所有的对象,它有3个步骤:
    1. 在开始标记的时候,生成一个快照图,标记存活对象
    2. 在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)
    3. 可能存在游离的垃圾,将在下次被收集。(快照中是灰色的,但在并发标记时实际应该是白色的)

内存分配:JVM 的栈上分配、TLAB、PLAB 有啥区别?

  • 栈上分配(栈上替换): JVM (HotSpot JVM)会通过逃逸分析(JDK6 6u23 之后默认开启,-XX:DoEscapeAnalysis 开启逃逸分析,-XX:PrintEscapeAnalysis 查看逃逸分析的筛选结果),将本来应该分配在堆中的对象,让其分配在线程私有的栈上。通过这种方式,减少垃圾回收的压力,提高虚拟机的运行效率。这样对象可以在函数调用后销毁,减轻堆的压力,避免不必要的gc。

    • 注:逃逸分析还可能做另外一项优化,标量替换。参数 -XX:EliminateAllocations 开启标量替换(默认开启),允许将对象打散分配到栈上。
      标量替换示例-1标量替换示例-2
  • TLAB(Thread Local Allocation Buffer:线程本地分配缓存):在 TLAB 启用的情况下(默认开启),JVM 会为每一个线程在 eden 区分配一块 TLAB。通过将对象分配在 TLAB 可以避免将对象分配在线程共享的堆上造成的线程同步操作的性能损耗,从而提高对象分配的效率。
    在这里插入图片描述

    1. 可以通过 -XX:UseTLAB 设置是否开启 TLAB;
    2. 默认情况下 TLAB 空间只占用 Eden 空间的 1%,可以通过 -XX:TLABWasteTargetPercent 设置 TLAB 占 Eden 的百分比;
  • PLAB(Promotion Local Allocation Buffers:晋升本地分配缓存):作用于 TLAB 类似,都是为了加速对象分配效率,避免多线程竞争而诞生的。 只不过 PLAB 是应用于对象晋升到 Survivor 区或老年代。与 TLAB 类似,每个线程都有独立的 PLAB 区。

  • 对象分配流程:
    在这里插入图片描述

算法类型

分代收集

分代假说:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

    Java 堆应该按照“对象的年龄”划分区域,从而在不用的区域利用不同的垃圾回收算法(比如新生代的 Eden + from 区到 to 区,使用标记-复制算法,老年代使用标记-整理算法),提高垃圾回收的效率和质量。

  3. 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

    只需建立全局的 RSet(Remembered Set)就可以解决存在对象跨代引用时,Minor GC 也需要扫描整个老年代的对象才能正确回收对象的问题,反过来 Major GC 也存在同样的问题。RSet 把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots 进行扫描。

名词对齐

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,需按上下文区分到底是指老年代的收集还是整堆收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。

在完成初始标记、根区域扫描、并发标记、最终标记后,G1就可以知道哪些老的分区可回收垃圾最多。 在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:
在这里插入图片描述
混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。
在这里插入图片描述
注意:在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的,整个应用处于假死状态,不能处理任何请求。
那么发生Full GC的情况有哪些呢?

  1. 并发模式失败
    G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。。
  2. 晋升失败或者疏散失败
    G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:1)增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。2)通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。3)也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。
  3. 巨型对象分配失败
    当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
标记-清除算法

在这里插入图片描述

复制算法

在这里插入图片描述

标记整理算法

在这里插入图片描述
在这里插入图片描述

垃圾回收器

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
查看默认垃圾回收器

  1. java 启动参数

+XX:PrintCommandLineFlags

  1. java 调试工具:jinfo -flag 相关垃圾回收器参数(比如:UseParallelGC) PID
    在这里插入图片描述

Serial

在这里插入图片描述

ParNew

在这里插入图片描述

ParallelGC

ParNew注重的是降低暂停时间,因此更适合需要低延迟的应用,如Web服务器、交互式应用等。而Parallel Scavenge注重高吞吐量,更适合后台运算为主的场景,如大型计算任务、批处理等。ParNew 是唯一一个与 CMS 组合使用的新生代垃圾收集器。
在这里插入图片描述
注意:JDK8 默认垃圾回收器(-XX:+UseParallelGC)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

CMS

注意:JDK14 已经移除 CMS(-XX:+UseConcMarkSweepGC)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

G1(Garbage First)

在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

注意:JDK9 中默认的垃圾回收器

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

YoungGC(MinorGC)

在这里插入图片描述
在这里插入图片描述

(全局)并发标记

在这里插入图片描述

混合回收

在这里插入图片描述
在这里插入图片描述

FullGC

在这里插入图片描述

ZGC:JDK14 新特性

书籍:《新一代垃圾回收器ZGC设计与实现》
在这里插入图片描述

文章

Arguments

Oracle JVM 参数官方教程

  • G1 参数

-XX:+UseG1GC -Xmx32g-XX:MaxGCPauseMillis=200

G1会尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。 那G1是如何做到最大暂停时间的呢?这涉及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域集合。

  • Young GC:选定所有新生代里的region。通过控制新生代的region个数来控制young GC的开销。
  • Mixed GC:选定所有新生代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。

我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。

注意:避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。

-XX:ParallelGCThreads=n

设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量,最多为 8。如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。

-XX:ConcGCThreads=n

设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。

-XX:InitiatingHeapOccupancyPercent=45

设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

  • XX:G1HeapRegionSize

设置每个 Region 的大小,值是 2 的幂,范围为 [1MB, 32MB],目标是根据最小的 Java 堆大小划分出 2048 个 Region。默认是堆内存的 1/2000。

  • 调试参数

-XX:+TraceClassLoading 和 -XX:+TraceClassUnloading

Metaspace 元空间位于堆外,主要是存储类的元数据信息,我们的应用里加载的各种类描述信息,比如类名,属性,方法,访问限制等。如果碰到经常Full GC的情况,但是老年代空间使用的却不多,年轻代GC后的情况也很正常,同时也不存在突然大对象的情况,但是元空间却一直递增,那么可以考虑下是不是使用了反射等手段导致元空间加载的类太多了,导致元空间爆满触发Full GC,那么此时就可以加上-XX:+TraceClassLoading和-XX:+TraceClassUnloading这两个参数,看下类加载和卸载的情况,确定下是不是有哪些类反复被生成和加载,找到相应的类,然后跟踪到代码里,排除问题。输出结果
提示使用 xlog
在 JVM 发生 OOM 时,自动导出内存映像文件。

-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=“指定的 hprof 文件路径”

在这里插入图片描述

-XX:OnOutOfMemoryError=/opt/box/latest/dropin/notify_oome

打印 GC 日志详情,JDK9 后被 -Xlog 取代。见 后文

-XX:+PrintGCDetails

工具

Oracle JDK Tools and Utilities

Monitoring Tools (jps, jstat, jstatd)
Troubleshooting Tools (jinfo, jhat, jmap, jsadebugd, jstack)
jstat 工具展示 -gcutil 选项
jstat 工具展示 -gc 选项
-gcutil 展示 JVM 各个分代区域的使用比例,-gc 展示 JVM 各个分代区域的实际 capacity 和 used

  • jcmd pid VM.metaspace:non-class 与 class 之间的区别是啥?
    在这里插入图片描述
  • The dmesg Command:dmesg -T

命令行

jps

在这里插入图片描述
在这里插入图片描述

jstat

在这里插入图片描述

jinfo

在这里插入图片描述
在这里插入图片描述

jmap

jmap -dump:live,format=b,file= <PID>
在这里插入图片描述

在这里插入图片描述

jhat

在这里插入图片描述

jstack

在这里插入图片描述
示例演示
在这里插入图片描述
重点参数:
在这里插入图片描述

jcmd

在这里插入图片描述

列出当前指定进程支持的所有命令

jcmd <PID> help

在这里插入图片描述

GUI

在这里插入图片描述

JDK 自带
  1. jconsole
  2. jvisualvm:在高版本JDK(大于1.8或后期更新的1.8版本)中已经不会再自动集成 VisualVM。需下载独立版本
三方工具
MAT(Memory Analysis Tool)

Java 堆内存分析工具,可以为开发者生成内存泄漏报表。
在这里插入图片描述
浅堆(Shallow Heap)与深堆(Retained Heap):
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

支配树(Dominate Tree):
在这里插入图片描述
在这里插入图片描述

Arthas
  1. Arthas之trace、monitor、watch命令
参考资料

支配:简单的理解就是 objectA 支配 objectB ,意味着当 objectA 被 gc 回收后,objectB 一定也会被回收,那么称 objectA 是 objectB 的支配者。类的各个实例所支配的对象的总大小就是 Retained Heap。
原理:jvm 通过 jmap(jmap -dump:format=b,file=[pid].hprof [pid]) 或-XX:+HeapDumpOnOutOfMemoryError 打出 dump hprof (Heap PROFile)文件,MAT Parser 会按照格式解析hprof 文件,然后从 GC Roots 构建成第一个图,接着从 GC Roots 开始遍历按照支配原则生成一个新的树–Dominator Tree,从 Dominator Tree 衍生出 Histogram、Path to GC Roots 的概念,并且通过API暴露出去。同时为了加速构建,会生产多种类型的index索引文件。

  1. Jprofier
  2. Arthas

实战分析

GC 日志

-Xlog[:[selections][:[output][:[decorators][:output-options]]]],其中selections 由 tag = level 表示;
-Xloggc:<fiilename>

日志说明

在这里插入图片描述
在这里插入图片描述

MinorGC

在这里插入图片描述

FullGC

在这里插入图片描述

OOM

问题定位于File.deleteOnExit()方法的调用,导致内存泄漏。调用该方法只会将需要删除文件的路径,维护在类DeleteOnExit的一个LinkedHashSet中,在JVM关闭时,才会去真正执行删除文件操作。这样导致DeleteOnExitHook这个对象越来越大,最终内存溢出。

堆外内存

锁消除

在动态编译同步代码块时,JIT 编译期可以借助逃逸分析来判断同步块所使用的锁对象是否只能被一个线程访问而没有发不到其它线程。如果没有,那么 JIT 编译期在编译这个同步代码块的时候就会取消对这部分代码的同步,从而大大提高并发性和性能。这个取消同步的过程叫同步省略或锁消除。
锁消除示例

其他

STW

在这里插入图片描述

SafePoint & SafeRegion

在这里插入图片描述

如何在垃圾收集发生时让所有线程(这里其实不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来?目前主流的 JVM 采用的是主动式中断线程的方案:当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。

在这里插入图片描述
在这里插入图片描述

案例
  1. JDK8 线程未到达安全点导致 GC 时间变长的问题

OopMap

R大:找出栈上的指针/引用

  1. 准确式GC: 给定某个位置上的某块数据,要能知道它的准确类型是什么,这样才可以合理地解读数据的含义;GC所关心的含义就是“这块数据是不是指针”。要实现这样的GC,JVM就要能够判断出所有位置上的数据是不是指向GC堆里的引用,包括活动记录(栈+寄存器)里的数据。
  2. 在HotSpot中对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的;这些数据是在类加载过程中计算得到的。每个被JIT编译过后的方法也会在 Safepoint 记录下OopMap,记录了执行到该方法的某条指令的时候,栈上和寄存器里哪些位置是引用。这样GC在扫描栈的时候就会查询这些OopMap就知道哪里是引用了。仍然在解释器中执行的方法则可以通过解释器里的功能自动生成出OopMap出来给GC用。 平时这些OopMap都是压缩了存在内存里的,在GC的时候才按需解压出来使用。对Java线程中的JNI方法,它们既不是由JVM里的解释器执行的,也不是由JVM的JIT编译器生成的,所以会缺少OopMap信息。那么GC碰到这样的栈帧该如何维持准确性呢?
    HotSpot的解决方法是:所有经过JNI调用边界(调用JNI方法传入的参数、从JNI方法传回的返回值)的引用都必须用“句柄”(handle)包装起来。JNI需要调用Java API的时候也必须自己用句柄包装指针。在这种实现中,JNI方法里写的“jobject”实际上不是直接指向对象的指针,而是先指向一个句柄,通过句柄才能间接访问到对象。这样在扫描到JNI方法的时候就不需要扫描它的栈帧了——只要扫描句柄表就可以得到所有从JNI方法能访问到的GC堆里的对象。但这也就意味着调用JNI方法会有句柄的包装/拆包装的开销,是导致JNI方法的调用比较慢的原因之一。

VM 之 OopMap 和 RememberedSet

OopMap 记录了栈上本地变量到堆上对象的引用关系。其作用是:垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 Reference 类型。如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收。但问题是,栈上的本地变量表里面只有一部分数据是 Reference 类型的(它们是我们所需要的),那些非 Reference 类型的数据对我们而言毫无用处,但我们还是不得不对整个栈全部扫描一遍,这是对时间和资源的一种浪费。

一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 gc 的时候就可以直接读取,而不用再一点一点的扫描了。事实上,大部分主流的虚拟机也正是这么做的,比如 HotSpot ,它使用一种叫做 OopMap 的数据结构来记录这类信息。

我们知道,一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 OopMap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。

R大:OopMap 应用GC过程

HotSpot VM的JIT编译器做的优化,硬要说的话效果跟这个类似。这个是显式把局部变量’lo’置为null从而切断其对LargeObject对象的引用,而实际发生的状况是JIT编译器在doSomething()调用之后就不把局部变量’lo’包含在OopMap里了,于是GC根本看不到这个变量,也不关心它引用了谁,自然的切断了引用。

HotSpot VM里,解释执行的方法可以在任意字节码边界上进入GC,但JIT编译后的代码并不能在“任意位置”进入GC。可以进入GC的“特定位置”叫做“GC safepoint”,或者简称“safepoint”。

  • 主动safepoint:由方法里的代码通过主动轮询去发现需要进入safepoint。有两种情况:
    • 循环回跳处(loop backedge)
      • 非 counted loop(非有界循环),即 while(true) 死循环这种,每次循环回跳之前会埋safepoint
      • 有界循环,如循环变量是 long 类型,有 safepoint,循环变量是 int 类型,需要添加 -XX:+UseCountedLoopSafepoints 才有 safepoint
    • 被调用方法临返回处(return)
  • 被动safepoint:调用别的方法的调用点。之所以叫做“被动”是因为并不是该方法主动发现要进入safepoint 的,而是某个被调用的方法主动进入了safepoint,导致其整条调用链上的调用者都被动的进入了safepoint。

垃圾回收相关知识

我们知道,一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 OopMap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。使用 OopMap 可以「避免全栈扫描」,加快枚举根节点的速度。但这并不是它的全部用意。它的另外一个更根本的作用是,可以帮助 HotSpot 实现准确式 GC (使用准确式内存管理,虚拟机可用知道内存中某个位置的数据具体是什么类型) 。

What does Oop Maps means in Hotspot VM exactly

OopMap is a structure that records where object references (OOPs) are located on the Java stack. Its primary purpose is to find GC roots on Java stacks and to update the references whenever objects are moved within the Heap.

There are three kinds of OopMaps:

  1. OopMaps for interpreted methods. They are computed lazily, i.e. when GC happens, by analyzing bytecode flow. The best reference is the source code (with lots of comments), see generateOopMap.cpp. InterpreterOopMaps are stored in OopMapCache.
  2. OopMaps for JIT-compiled methods. They are generated during JIT-compilation and kept along with the compiled code so that VM can quickly find by instruction address the stack locations and the registers where the object references are held.
  3. OopMaps for generated shared runtime stubs. These maps are constructed manually by the developers - authors of these runtime stubs.


During GC JVM walks through all thread stacks. Each stack is parsed as a stream of stack frames. The frames are either interpreted or compiled or stubs. Interpreted frames contain information about Java method and bci (bytecode index). OopMapCache helps to find an OopMap corresponding to the given method and bci. The method of a compiled frame is discovered by instruction address lookup.

R大:JVM-如何判断一段数据是真正的数据,还是对象的引用

JVM 判断一段数据到底是数据还是引用类型,首先要看JVM选择用什么方式。通常这个选择会影响到GC的实现。
一、保守式
如果JVM选择不记录任何这种类型的数据,那么它就无法区分内存里某个位置上的数据到底应该解读为引用类型还是整型还是别的什么。这种条件下,实现出来的GC就会是“保守式GC(conservative GC) ”。 在进行GC的时候,JVM开始从一些已知位置(例如说JVM栈)开始扫描内存,扫描的时候每看到一个数字就看看它“像不像是一个指向GC堆中的指针”。 这 里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指 针),之类的。然后递归的这么扫描出去。
保守式GC的好处是相对来说实现简单些,而且可以方便的用在对GC没有特别支持的编程语言里提供自动内存管理功能。
保守式GC的缺点:

  1. 会有部分对象本来应该已经死了,但有疑似指针指向它们,使它们逃过GC的收集。这对程序语义来说是安全的,因为所有应该活着的对象都会是活 的;但对内存占用量来说就不是件好事,总会有一些已经不需要的数据还占用着GC堆空间。具体实现可以通过一些调节来让这种无用对象的比例少一些,可以缓解 (但不能根治)内存占用量大的问题。
  2. 由于不知道疑似指针是否真的是指针,所以它们的值都不能改写;移动对象就意味着要修正指针。换言之,对象就不可移动了。有一种办法可以在使用 保守式GC的同时支持对象的移动,那就是增加一个间接层,不直接通过指针来实现引用,而是添加一层“句柄”(handle)在中间,所有引用先指到一个句 柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容即可。但是这样的话引用的访问速度就降低了。Sun JDK的Classic VM用过这种全handle的设计,但效果实在算不上好。

二、半保守
JVM可以选择在栈上不记录类型信息(同保守式 ),而在对象上记录类型信息。这样的话,扫描栈的时候仍然会跟上面说的过程一样,但扫描到GC堆内的对象时因为对象带有足够类型信息了,JVM就能够判断出在该对象内什么位置的数据是引用类型了。这种是“半保守式GC ”。

三、准确式
与保守式GC相对的是“准确式GC ”,原文可以是precise GC、exact GC、accurate GC或者type accurate GC。外国人也挺麻烦的。是什么东西“准确”呢?关键就是“类型”,也就是说给定某个位置上的某块数据,要能知道它的准确类型是什么,这样才可以合理地解读数据的含义;GC所关心的含义就是“这块数据是不是指针”。要实现这样的GC,JVM就要能够判断出所有位置上的数据是不是指向GC堆里的引用,包括活动记录(栈+寄存器)里的数据。

概念及作用

全称为Ordinary Object Pointer Map(普通对象指针地图),在 JVM 被用来实现准确式 GC(Exact VM:能确定当前寄存器中存储的二进制串代表的是数字,还是指向堆中对象的地址,HotSpot VM 就属于 Exact VM),通过 OopMap 记录了哪些地方存着对象引用。另外在 GC 时,需要从栈、方法区或寄存器等地方获取 GC Roots,完全遍历这些地方需要消耗大量的时间,加上 GC 时会 “Stop The World”终止用户线程,以准确确定 GC Roots(如果不 STW,可能存在新入栈的栈桢直接引用的对象 A 本应该是 GC Roots 但最终被当作不可用对象被回收的情况,从而产生错误),这种 GC 实现方式会严重影响应用线程的正常运行是不可接受的,所以也可通过 OopMap 快速准确地枚举出 GC Roots。

OopMap 中存储了两种对象的引用:

  • 栈里和寄存器内的引用:在即时编译中,在特定的位置记录下栈里和寄存器里哪些位置是引用。

  • 对象内的引用:类加载动作完成时,HotSpot 就会计算出对象内什么偏移量上是什么类型的数据。
    注:把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为有效地址或偏移量,因此,实际地址=所在段的起始地址+偏移量

// 方法1存储在栈帧3
public void testMethod1() {
    // 栈里和寄存器内的引用
    DemoD demoD = new DemoD();
}

// 方法2存储在栈帧8
public void testMethod2() {
    // 栈里和寄存器内的引用
    DemoA demoA = new DemoA();
    // 对象内的引用
    demoA.setDemoC(new DemoC());
    
    // 栈里和寄存器内的引用
    DemoA demoB = new DemoB();
} 

上面代码段对应的 OopMap 图示结构为
在这里插入图片描述

RSet(RememberedSet)

对于位于不同年代对象之间的引用关系,虚拟机会在程序运行过程中给记录下来。比如:“老年代对象引用新生代对象”这种情况,会在引用关系发生时,在新生代边上专门开辟一块空间记录下来,这就是 RememberedSet,
在这里插入图片描述

为什么在讲GC ROOT时会提OopMap存放了GC ROOT,但讲跨代引用时总是以需要遍历整个老年代来引出RemberSet?

在这里插入图片描述
由于回收年轻代的时候会忽略位于老年代的GC ROOT(如果不忽略的话,相当于发生full gc),所以需要去寻找老年代中有跨代引用的对象,如果发生的是full gc,所有的gc root都会被进行可达性分析,那么就不用考虑跨代引用的问题。

总之,RememberedSet记录的是新生代的对象被老年代引用的关系。所以“新生代的 GC Roots ” + “ RememberedSet 存储的内容”,才是新生代收集时真正的 GC Roots 。然后就可以以此为据,在新生代上做可达性分析,进行垃圾回收。

衡量垃圾回收器性能指标

目前主流标准:在最大吞吐量优先的情况下,降低停顿耗时。

  1. 吞吐
    在这里插入图片描述

  2. 低延时(暂停时间)
    在这里插入图片描述

  3. 吞吐优先 vs 低延时优先
    在这里插入图片描述
    在这里插入图片描述

内存泄漏

严格:对象不再被程序使用,但其占用的空间又不能被 GC 回收。
宽泛:在实际场景中,一些不太好的实践会导致对象的生命周期变得很长甚至导致 OOM。
在这里插入图片描述
在这里插入图片描述

案例

在这里插入图片描述

同步

尚硅谷 - 宋红康视频讲解

JVM 有两种同步方式,都是通过 monitor 来实现的。

方法级同步

在这里插入图片描述

同步代码块

在这里插入图片描述
在这里插入图片描述

资料

  1. Java 虚拟机规范
  2. 深入理解JVM(六)一一运行时数据区(方法区)
  • 19
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值