Skip to content

JVM入门_基础概念

1、什么是 JVM

JVM 全称是 Java Virtual Machine,中文名称叫Java虚拟机,它是由软件技术模拟出计算机运行的一个虚拟的计算机。

JVM 充当着一个翻译官的角色,能够将我们编写出的Java程序,翻译给系统“听”,告诉它我们的程序需要做什么操作。

Java的代码需要经过编译器,生成.Class文件后,JVM才能识别并运行它,JVM针对每个操作系统开发其对应的解释器,所以只要其操作系统有对应版本的JVM,那么这份Java编译后的代码就能够运行起来,这就是Java能一次编译,到处运行的原因。

当前市面上使用范围最广的,是Sun/OracleJDK或者OpenJDK中默认的 HotSpot 虚拟机,这里也是以 HotSpot 虚拟机展开,对JVM 基础概念进行阐述。

2、类加载机制

上文提到,Java的代码经过编译器,生成.Class文件后,JVM才能识别并运行。

类加载过程

Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?

系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

整个类加载的过程中,包括加载、验证、准备、解析、初始化五个阶段。

在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。

另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

  • 类的加载: 查找并加载类的二进制数据
  • 连接
    • 验证: 确保被加载的类的正确性
    • 准备: 为类的静态变量分配内存,并将其初始化为默认值
    • 解析: 把类中的符号引用转换为直接引用
  • 初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
  • 使用: 类访问方法区内的数据结构的接口, 对象是Heap区的数据
  • 卸载: 结束生命周期

类加载的层次

image.png

  • 启动类加载器: Bootstrap ClassLoader,
    • 负责加载存放在JDK\jre\lib (JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.* 开头的类均被Bootstrap ClassLoader加载)。
    • 启动类加载器是无法被Java程序直接引用的。
  • 扩展类加载器: Extension ClassLoader,
    • 该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如javax.* 开头的类),
    • 开发者可以直接使用扩展类加载器。
  • 应用程序类加载器: Application ClassLoader,
    • 该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,
    • 开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  • 自定义类加载器:
    • 因为JVM自带的 ClassLoader 只是懂得从本地文件系统加载标准的 java class 文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
      • 在执行非置信代码之前,自动验证数字签名。
      • 动态地创建符合用户特定需要的定制化构建类。
      • 从特定的场所取得java class,例如数据库中和网络中。

Class.forName() 和 ClassLoader.loadClass()区别?

  • Class.forName(): 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass(): 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
  • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象

Java的类加载机制是Java虚拟机(JVM)的一个核心组成部分,它负责动态加载、链接和初始化所有的类和接口。类加载机制的工作原理是分阶段进行的,主要包括加载(Loading)、链接(Linking)和初始化(Initialization)三个阶段。

  1. 加载(Loading)

在加载阶段,JVM会通过类加载器(ClassLoader)读取二进制数据(通常是从.class文件中)并将其转换为java.lang.Class的一个实例。这个实例代表了类在JVM中的一个原始数据结构。

类加载器类型:

  • 引导类加载器(Bootstrap ClassLoader): 加载JVM的核心类库。
  • 扩展类加载器(Extension ClassLoader): 加载jre/lib/ext目录下或者由系统属性java.ext.dirs指定位置中的类库。
  • 系统类加载器(System ClassLoader): 加载系统类路径(classpath)上的类库。
  1. 链接(Linking)

链接阶段负责将加载的类或接口的二进制数据合并到JVM的运行状态中。链接阶段分为三个子阶段:

  • 验证(Verification): 确保被加载的类或接口的二进制表示符合JVM规范,没有安全问题。
  • 准备(Preparation): JVM为类变量分配内存并设置默认初始值,这些变量所使用的内存在方法区中进行分配。
  • 解析(Resolution): JVM将常量池内的符号引用替换为直接引用。
  1. 初始化(Initialization)

在初始化阶段,JVM负责执行类构造器 <clinit>() 方法的过程。<clinit>() 方法由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static{}块)中的语句合并而成。初始化阶段是执行这些初始化语句和静态代码块的过程。

类加载机制的特点

  • 懒加载: 类加载器采用懒加载策略,即当首次使用某个类时才加载。
  • 缓存机制: 加载的类信息会被缓存,同一个类加载器下,一个类型只会被加载一次。

类比

可以把类加载机制想象成图书馆的工作流程:

  • 加载: 类似于根据书名找到书籍的过程。
  • 链接: 类似于检查书籍是否完整、页码是否正确,并准备好放到书架上。
  • 初始化: 类似于将书籍放到指定的书架上,标记为可借阅状态。

双亲委派模型

双亲委派模型(Parent Delegation Model)是Java类加载器(ClassLoader)中的一个核心概念,用于加载类和接口。这个模型旨在提供一种安全性较高的类加载机制,确保Java核心库的类型安全,避免类的重复加载。

双亲委派模型的工作原理

  1. 委派过程: 当一个类加载器尝试加载某个类时,它不会立即尝试加载这个类。相反,它会首先将加载任务委派给父类加载器去完成。
  2. 向上递归: 这个过程会递归向上进行,直到顶层的启动类加载器(Bootstrap ClassLoader)。
  3. 尝试加载: 如果父类加载器可以完成类的加载工作,那么就成功返回;如果父类加载器无法完成这个加载(因为类不在其搜索范围内),那么子类加载器会尝试自己去加载这个类。

双亲委派模型的优势

  1. 避免类的重复加载: 由于在向上委派的过程中,系统类加载器在加载类时会先检查这个类是否已经被加载过了,这样就可以避免重复加载。
  2. 保护程序安全性: 防止核心API被随意篡改。例如,用户可以自己写一个名为java.lang.Object的类,但系统类加载器会在用户的类加载器之前加载Object类,因此不会加载到用户自定义的那个类。

双亲委派模型的例外

尽管双亲委派模型是Java推荐的类加载机制,但在某些情况下,类加载器可能需要违反这个模型。例如,Java的SPI(Service Provider Interface)机制允许服务提供者在运行时被插入和替换,这就需要用到一种不同于标准双亲委派模型的类加载方式。此外,一些应用服务器也会对Java的双亲委派模型进行修改以满足特定需求。

类比

可以将双亲委派模型类比于组织中的任务委托过程。当一个任务到来时,员工会先上报给自己的直接上司,上司再上报给更高层的管理者。管理者层层审批,如果上层可以处理这个任务,就不再往下传递。这样可以确保任务处理的正确性和权威性,同时避免重复处理同一个任务。


打破双亲委派机制

打破双亲委派机制通常涉及自定义类加载器,并重写其loadClass方法的行为。这通常是出于对特定环境的特殊需求,比如在容器中隔离应用程序、热部署、插件加载等。

打破双亲委派机制的方法

要打破双亲委派机制,可以通过创建自定义的类加载器并重写loadClass方法来实现:

  1. 创建自定义类加载器:继承ClassLoader类。
  2. 重写loadClass方法:在自定义的loadClass方法中,可以先尝试自己加载类,如果不成功,再根据需要选择是否委托给父加载器。
public class CustomClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 首先, 检查请求的类是否已经被加载
        Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass == null) {
            try {
                // 尝试自己加载类
                loadedClass = findClass(name);
            } catch (ClassNotFoundException e) {
                // 如果自己无法加载, 则委托给父加载器
                loadedClass = super.loadClass(name, false);
            }
        }
        return loadedClass;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 实现自己的加载逻辑
        // ...
    }
}

双亲委派机制的设计理念

双亲委派机制的设计主要基于以下几个目的:

  1. 安全性:防止核心API被随意篡改。例如,防止用户自定义的类替换掉核心的类,如java.lang.Object
  2. 避免类的重复加载:当父加载器已经加载了该类时,就没有必要子加载器再重新加载一次。
  3. 保持命名空间的清晰:通过该机制,可以确保在不同的类加载器中加载的类能够相互不见,避免不同类加载器加载相同全名类而引起的冲突。

打破双亲委派机制的理由

尽管双亲委派机制在多数情况下是合理的,但在某些场景下可能需要打破这一机制:

  1. 隔离加载:在容器化的环境中,比如在一个JVM中运行多个应用程序,每个应用程序可能需要使用不同版本的同一库。
  2. 热替换和热部署:在开发过程中或者某些应用程序中,可能需要重新加载修改过的类,以实现不重启应用程序的更新。
  3. 插件化架构:应用程序可能需要加载扩展或插件,这些插件与应用程序的类加载器相独立。

在设计自定义类加载器和考虑是否打破双亲委派模型时,需要仔细权衡安全性、隔离性以及动态加载的需求。


什么是类加载机制,讲一下双亲委派机制,为什么有时候要打破双亲委派机制,怎么打破。

类加载机制是 JVM 的一个核心功能,他能够将类和接口加载;

类加载主要分为三个阶段,分别是:加载、连接和初始化;

在第一个阶段中,JVM 会通过类加载器将 .class 文件进行读取二进制数据并转换为 Class 实例;

其中类加载器主要分为:引导类加载器 Bootstrap ClassLoader 、扩展类加载器 Extension ClassLoader、系统类加载器 System ClassLoader

在第二个阶段中,连接会将加载的类或者接口的二进制数据合并到 JVM 的运行状态中。

同时连接又分为三个子阶段,分别是:验证、准备、解析。

在第三个阶段中,初始化会执行类构造器方法,即执行初始化语句和静态代码块的过程。

双亲委派机制是类加载器中的一个核心概念,用于加载类和接口。他的一个核心过程是通过向上递归的操作,子加载器会先委派任务给父加载器,直到顶层的类加载器 Bootstrap ClassLoader ,如果父加载器无法完成这个加载,那么此时子加载器才会尝试自己去加载这个类。 这种操作可以避免类的重复加载和防止核心API被随意篡改。

打破双亲委派机制通常是需要创建自定义的类加载器并重写 loadClass 方法来实现。

3、JVM的结构体系

image.png

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。

运行时数据区根据线程是否私有或者共享进行区分

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存 (非运行时数据区的一部分)

Java 虚拟机规范对于运行时数据区域的规定是相当宽松的。以堆为例:堆可以是连续空间,也可以不连续。堆的大小可以固定,也可以在运行时按需扩展 。虚拟机实现者可以使用任何垃圾回收算法管理堆,甚至完全不进行垃圾收集也是可以的。

直接内存(Java7的永久代或JDK8的元空间、代码缓存)

JDK 1.8 和之前的版本略有不同,我们这里以 JDK 1.7 和 JDK 1.8 这两个版本为例介绍。

1.8之前版本

image.png|600

JVM1.8之后版本

image.png|600

1.8之前的版本,线程共享为两个区域:堆与方法区;其中方法区只是JVM虚拟机规范的一部分,不是实际的实现;

直接内存:是JVM以外的本地内存;

版本区别介绍

  • 区别一(方法区)
    • 1.8版本之后,使用元数据区实现了方法区,之前是使用永久代来实现方法区,大小是启动时固定好的;
    • 元空间不在虚拟机中,而是使用本地内存,并且大小可以自动增长,减少了OOM(内存溢出)的几率;
  • 区别二(堆区)
    • Java7之后运行时常量池从方法区移到了这里,为Java8移出永久代做好准备;

4、运行时数据区域

1. 程序计数器(Program Counter Register)

  • 描述: 程序计数器是一小块内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
  • 作用: 每个线程都有自己的程序计数器,是线程私有的。它的作用是记住下一条JVM指令的执行位置,如果是执行Java方法,则记录字节码指令的地址;如果是执行本地方法,则为空(Undefined)。
  • 特点: 不会发生内存泄漏问题,是唯一一个没有规定任何OutOfMemoryError情况的区域。

2. 虚拟机栈(VM Stacks)

  • 描述: 虚拟机栈是描述Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
  • 作用: 每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 特点: 生命周期和线程相同。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError。

3. 本地方法栈(Native Method Stacks)

  • 描述: 本地方法栈与虚拟机栈发挥的作用非常相似,区别是虚拟机栈为执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
  • 特点: 和虚拟机栈一样,生命周期也是和线程相同,受到本地方法栈大小的限制,也可能抛出StackOverflowError和OutOfMemoryError。

4. 堆(Heap)

  • 描述: 堆是Java虚拟机管理的最大一块内存区域,它被所有线程共享,在虚拟机启动时创建。
  • 作用: 它的主要目的是存放对象实例,几乎所有的对象实例都在这里分配内存。
  • 特点: 堆是垃圾收集器管理的主要区域,因此也被称作“GC堆”。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将抛出OutOfMemoryError。

5. 方法区(Method Area)

  • 描述: 方法区与Java堆一样,是各个线程共享的内存区域。它用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
  • 作用: 存储每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。
  • 特点: 方法区也被称为永久代(PermGen),但从JDK 1.8开始,已经被元空间(MetaSpace)所取代。如果方法区无法满足内存分配需求时,将抛出OutOfMemoryError。

这些区域各有其特定的用途,它们共同工作以支持JVM的运行。理解这些区域如何交互和管理内存,是深入理解Java运行时环境的关键。

类比

可以将JVM的运行时数据区想象成一家工厂:

  • 程序计数器: 工厂中的任务列表,记录下一个任务的位置。
  • 虚拟机栈: 工人的工作台,每当开始一个新任务,就会在工作台上准备相应的材料和工具。
  • 本地方法栈: 特殊任务的工作台,用于处理一些非标准的任务。
  • : 材料仓库,存放所有需要的材料。
  • 方法区: 设计图纸存放区,存放产品设计图纸和制造指导。

JVM 的运行时数据区区域有哪些。

JVM 将 .class 文件进行类加载后,数据会到达 运行时数据区。

区域主要包括:堆(Heap)、方法区(Method Area)、虚拟机栈(VM Stacks)、本地方法栈(Native Method Stacks)和程序计数器(Program Counter Register)

区分可以看一下这个: JVM入门_基础概念#3、JVM的结构体系

5、垃圾回收机制

Java虚拟机(JVM)的垃圾回收机制是一种自动内存管理系统,它旨在帮助开发者管理应用程序使用的内存。其核心思想是自动发现并释放那些不再被应用程序使用的内存区域,从而避免内存泄露和过度的内存消耗。

1. 垃圾回收的基本原理

垃圾回收的基本原理是确定哪些内存是可达的,即在程序的当前状态下,哪些内存仍然可以被程序访问。

内存分配给对象时,如果该对象可达,它就会继续占用内存;如果对象不可达,那么它占用的内存就可以被视为垃圾,并由垃圾收集器回收。

2. 判定对象存活的方法

  • 引用计数法: 每个对象有一个引用计数器,当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1。任何时刻计数器为0的对象都是不可能再被使用的。但这种方法无法解决对象之间相互循环引用的问题。
  • 可达性分析算法(更常用): 这种算法通过一系列称为“根”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到根节点没有任何引用链相连时,则证明此对象是不可用的。
JVM分代年龄为什么是15

在Java虚拟机(JVM)的分代垃圾回收机制中,"分代年龄"指的是一个对象在新生代中经过多少次垃圾回收(Minor GC)之后还存活的计数。

当对象的年龄达到一定的阈值时,它就会从新生代晋升到老年代。这个阈值被称为晋升年龄(Promotion Age),默认值通常是15。

默认值15是JVM设计者根据经验设置的,目的是在内存占用和垃圾回收成本之间寻求平衡。

JVM分代年龄的最大值

JVM分代年龄的最大值 是 15.

一个对象的GC年龄,是存储在对象头里面的,一个Java对象在JVM内存中的布局由三个部分组成,分别是对象头,实例数据,对齐填充。而对象头(Mark Word)里面有4个bit位来存储GC年龄。而4个bit位能够存储的最大数值是1111(十进制就是15)。

image.png

3. 垃圾收集算法

  • 标记-清除算法(Mark-Sweep): 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
  • 复制算法(Copying): 将内存分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
  • 标记-整理算法(Mark-Compact): 类似于标记-清除算法,但在完成标记后,它会将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
  • 分代收集算法(Generational Collection): 将堆内存分为几个区域(如年轻代、老年代),根据各个年龄段的特点采用最适当的收集算法。

类比

可以将垃圾回收比作是清洁工人在城市中收集垃圾的过程:

  • 可达性分析就像是确定哪些垃圾桶是空的,哪些是满的。
  • 标记-清除就像是标记出所有满的垃圾桶,然后一一清空。
  • 复制算法就像是将半满的垃圾桶的内容转移到一个空的垃圾桶中,然后清理原来的垃圾桶。
  • 标记-整理就像是将所有的垃圾集中到垃圾场的一角,然后清理掉其余的空间。

4. JVM中的垃圾收集器

JVM提供了多种垃圾收集器,每种收集器都有各自的优势,适用于不同类型和性能要求的应用场景。常见的垃圾收集器包括:

  • Serial GC: 单线程执行,适合单核处理器。
  • Parallel GC: 多线程执行,关注吞吐量。
  • CMS (Concurrent Mark Sweep) GC: 关注最短停顿时间。
  • G1 GC: 面向服务端应用,将堆内存分为多个区域,并并行执行垃圾回收。

每种垃圾回收器都有其特定的算法和运行机制,旨在优化不同的性能指标,如吞吐量、暂停时间或内存占用。默认垃圾回收器的选择取决于JVM版本和具体的配置参数。

1. 串行垃圾回收器(Serial GC)
  • 运行机制:串行垃圾回收器使用单个线程执行垃圾回收。在执行垃圾回收时,它会暂停所有的应用程序线程(称为Stop-The-World,STW),直到垃圾回收完成。
  • 适用场景:适用于小内存和单核处理器的环境,或者对暂停时间不敏感的应用。
2. 并行垃圾回收器(Parallel GC)
  • 运行机制:并行垃圾回收器使用多个线程并行执行垃圾回收任务。在垃圾回收期间,应用程序的执行也会暂停。
  • 适用场景:适用于需要高吞吐量和多核处理器的服务器环境。它能有效利用CPU资源来缩短垃圾回收的时间。
3. CMS垃圾回收器(Concurrent Mark Sweep GC)
  • 运行机制
    • 初始标记(Initial Mark):标记GC Roots能直接关联到的对象,需要STW暂停。
    • 并发标记(Concurrent Mark):在应用线程运行的同时,标记GC Roots可达的对象。
    • 重新标记(Remark):修正并发标记期间因应用程序运行而产生变动的标记记录,需要STW暂停。
    • 并发清除(Concurrent Sweep):清除不可达的对象。
  • 适用场景:适用于对响应时间要求高的应用,因为它减少了垃圾回收时的停顿时间。

CMS(Concurrent Mark-Sweep)垃圾回收器是JVM中一种以获取最短回收停顿时间为目标的收集器,特别适用于那些对响应时间有很高要求的场景。CMS垃圾回收器主要用于回收老年代(Old Generation)的空间。

分为几个阶段:

  1. 初始标记(Initial Mark)
  • 运行机制:这个阶段标记所有与GC Roots直接相连的对象。这一步需要"Stop-The-World"(STW),所有的应用线程都会被暂停,但通常这个阶段很快。
  1. 并发标记(Concurrent Mark)
  • 运行机制:在这一阶段,GC线程标记所有从直接相连的对象可达的对象。这个阶段是并发的,即应用程序和垃圾回收线程同时执行,不需要暂停用户线程。
  1. 重新标记(Remark)
  • 运行机制
    • 由于在并发标记阶段,应用程序线程仍在运行,可能会有新的对象被分配,或者对象引用关系发生变化。因此,需要一个STW的阶段来修正并发标记期间的变动。
    • 为了减少这个阶段的停顿时间,CMS使用了一种名为“增量更新”的技术(Card Marking),来记录并发阶段期间引用关系的变化。
    • 此外,CMS还可以选择使用一种名为“快照算法”(Snapshot-at-the-beginning, SATB)来处理这些变化。
  1. 并发清除(Concurrent Sweep)
  • 运行机制:此阶段回收那些已经标记为不可达的对象占用的空间。这一步是并发的,不需要暂停用户线程。
  1. 并发重置(Concurrent Reset)
  • 运行机制:这个阶段是清理内部数据结构,为下一次GC循环做准备的过程。这一步是并发的,不需要暂停用户线程。

类比

可以将CMS垃圾回收类比为一个有序的清洁工作:

  • 初始标记就像是确定清洁的起始位置。
  • 并发标记就像是在日常的清洁过程中标记出需要清洁的区域。
  • 重新标记就像是在清洁前再次检查标记的区域,确保没有遗漏。
  • 并发清除则是实际的清洁过程。
  • 并发重置就像是清理和准备清洁工具,为下一次清洁做准备。

CMS垃圾回收器的主要目标是减少垃圾回收时的停顿时间,从而提高应用程序的响应性。然而,CMS也有一些缺点,比如它对CPU资源的使用较多,且由于并发执行,可能会导致更多的内存碎片。

4. G1垃圾回收器(G1 GC)
  • 运行机制:G1垃圾回收器将堆内存划分为多个区域,并根据每个区域的回收价值和成本来优先回收,目的是在有限的停顿时间内回收尽可能多的内存。
  • 适用场景:适用于大内存环境,并且需要更可控的垃圾回收暂停时间。

默认的垃圾回收器

默认的垃圾回收器取决于JVM版本和JVM参数。例如,在较新的Java版本中(如Java 9及之后),默认的垃圾回收器可能是G1 GC。

选择不同垃圾回收器的情况

不同的垃圾回收器适用于不同的场景和需求。以下是选择不同垃圾回收器的一些情况:

  • 对暂停时间要求严格:如果需要减少垃圾回收期间的停顿时间,可以考虑使用CMS或G1。
  • 需要最大化吞吐量:如果应用需要最大化CPU时间用于执行应用逻辑,可以考虑使用并行垃圾回收器。
  • 资源受限的环境:在单核处理器或内存较小的环境中,串行垃圾回收器可能是一个较好的选择。

选择合适的垃圾回收器需要考虑应用程序的具体需求和运行环境。在实际应用中,可能还需要根据应用程序的行为进行调优以达到最佳性能。


垃圾回收主要是针对哪块区域进行回收的

垃圾回收主要是针对堆内存进行的,这是大部分Java对象存储和回收的地方。

堆内存是垃圾收集器的主要工作区域,这里是动态分配内存的地方,对象实例和数组在这里创建和回收。

新生代的对象怎么到达老年代的。

在Java虚拟机(JVM)中,对象通常首先在新生代(Young Generation)分配。随着时间的推移和垃圾回收的进行,对象可能会从新生代晋升(Promotion)到老年代(Old Generation)。这个过程是JVM自动内存管理的一部分,主要基于对象的存活周期和垃圾回收机制。以下是对象从新生代到达老年代的过程:

  1. 对象的初始分配

大部分情况下,新创建的对象首先在新生代的Eden区域分配。这是基于假设大多数对象都是朝生夕死的。

  1. Minor GC(小垃圾回收)

当新生代的Eden区域填满时,JVM会进行一次 Minor GC。这次GC会检查Eden区以及两个Survivor区(通常称为From和To,或者S0和S1):

  • 存活对象的复制:存活的对象会被复制到当前的Survivor区(例如,从Eden复制到S1)。
  • 年龄计数:每当对象在Minor GC后存活,它的年龄就会增加。当对象在新生代中存活足够长的时间(超过特定的年龄阈值,通常由参数-XX:MaxTenuringThreshold设定)后,它就会被认为足够成熟,可以晋升到老年代。
  1. 对象的晋升

对象不是在每次Minor GC后都会晋升到老年代。它们可能会在新生代的Survivor区之间来回复制几次。对象晋升到老年代通常是基于以下几个条件:

  • 年龄阈值:当对象的年龄达到预设的阈值时。
  • Survivor空间不足:如果Survivor空间不足以容纳一次Minor GC后存活的对象,这些对象将直接晋升到老年代。
  • 大对象:大对象(大于-XX:PretenureSizeThreshold设定的大小)可能直接在老年代分配,以避免在新生代中反复复制。

类比:一个人的成长

可以将对象从新生代晋升到老年代类比为一个人的成长过程:

  • 新生代:就像儿童时期,个体(对象)在这个阶段快速成长(创建),但许多很快就不再存在(朝生夕死)。
  • Minor GC:就像学校的考试,检验哪些学生(对象)足够强壮(存活)以继续到下一个学期(Survivor区或老年代)。
  • 晋升到老年代:个体(对象)通过多次考验(Minor GC)证明了其稳定性(存活率高),因此可以进入更为稳定的成年阶段(老年代)。

分代年龄为什么要设计为 15,默认值是 15, 最大是多少。

在垃圾回收和内存分配之间的一个平衡值,最大值是 15,默认值也是15

参考:JVM入门_基础概念#JVM分代年龄为什么是15

垃圾回收器

常见的垃圾回收器有 单线程、多线程、CMS、G1。

哪些对象是可以被看做是 GCRoot

在Java虚拟机(JVM)中,垃圾收集器在回收内存之前需要确定哪些对象是活动的,即不应该被回收的。这是通过可达性分析(Reachability Analysis)来完成的,过程中以一组称为“GC Roots”(垃圾回收根)的对象为起点。以下是常见的GC Root对象:

  1. 在虚拟机栈中引用的对象
  • 局部变量表中的引用对象。这些引用来自于各个线程的方法调用栈。每个线程都有自己的虚拟机栈,其中的局部变量表可能引用了其他对象。
  1. 方法区中的类静态属性引用的对象
  • 静态属性(static field)位于方法区,它们引用的对象也作为GC Roots。
  1. 方法区中常量引用的对象
  • 常量,比如字符串常量池(String Constant Pool)中的引用。
  1. 本地方法栈中JNI(即通常所说的Native方法)引用的对象
  • 由Java本地接口(JNI)引用的对象,例如在本地代码(C或C++等)中创建并被Java代码使用的对象。
  1. 活动的Java线程(Thread对象)
  • 活动的线程也是一个GC Root。

类比

可以将GC Roots想象为家族树的根:家族树中的每个成员(对象)都可以通过一系列的关系(引用)追溯到家族的始祖(GC Roots)。如果一个成员(对象)不能通过任何关系(引用链)与始祖(GC Roots)相连,那么可以认为这个成员(对象)已经“离世”(可以被垃圾回收)。

在并发标记过程中,可以会出现一些多标、漏标的过程,CMS是怎么处理的。

在CMS(Concurrent Mark-Sweep)垃圾回收器中,并发标记过程确实可能会面临多标(多次标记同一个对象)或漏标(未标记到应该存活的对象)的问题,特别是因为应用程序的线程在这个阶段是处于活动状态的,对象的引用关系可能会发生变化。CMS采用了几种技术和策略来处理这些问题,确保标记过程的准确性。以下是CMS处理这些问题的一些方法:

  1. 写屏障(Write Barrier)
  • 写屏障是一种系统机制,用于监控和拦截对象引用字段的写操作。
  • 在并发标记阶段,JVM通过写屏障来记录那些被修改的引用。这些信息将在稍后的重新标记阶段被使用,以确保所有存活的对象都被正确标记。
  1. 增量更新(Incremental Update)
  • 增量更新是在写屏障的帮助下实现的。它记录了在并发标记阶段和应用程序线程同时运行时,引用关系发生变化的对象。
  • 在接下来的重新标记阶段,增量更新的信息被用来修正并发标记期间可能出现的漏标问题。
  1. 重新标记(Remark Phase)
  • 尽管写屏障和增量更新可以减少漏标的问题,但仍然需要一个STW(Stop-The-World)的阶段来最终确保所有存活的对象都被正确标记。这个阶段就是重新标记。
  • 在重新标记阶段,CMS收集器会处理所有的增量更新信息,并且通过一种名为“快照算法”(Snapshot-at-the-beginning, SATB)来处理在并发阶段发生的引用变化。

类比

可以将CMS的并发标记过程类比为绘制一幅复杂的画作:

  • 并发标记就像是为画作勾勒初步的轮廓。画家(垃圾收集器)在绘制的同时,模特(应用程序中的对象)可能会稍微移动,从而使得画作的某些部分需要调整。
  • 写屏障和增量更新就像是画家记录下模特的每一个小动作,以便稍后修正画作的细节。
  • 重新标记则是画家最终审视画作,根据之前记录的所有小动作调整画作,确保画作的准确性。

CMS的设计是为了减少垃圾回收时的停顿时间,但为了确保准确性,它需要采用上述机制来处理并发标记过程中可能出现的问题。

三色标记算法有了解过吗

三色标记(Tri-color Marking)算法是垃圾回收中使用的一种标记算法,特别是在支持并发垃圾收集的情况下,例如在CMS(Concurrent Mark-Sweep)和G1(Garbage-First)垃圾回收器中。它的基本思想是在标记过程中,用三种颜色来表示对象的状态,从而确保在并发环境下能够正确地进行垃圾回收。这三种颜色通常是白色、灰色和黑色。

三色标记算法的工作原理

  1. 白色:表示对象尚未被访问。初始时,所有对象都标记为白色。
  2. 灰色:表示对象已被访问,但该对象引用的其他对象还没有全部访问。换句话说,从根集合出发,引用路径上的对象已被访问,但这些对象的子对象还未被全部访问。
  3. 黑色:表示对象已被访问,且该对象引用的所有其他对象也都已访问。黑色对象是安全的,因为它和它引用的对象都已经被扫描过,不会再引用任何白色对象。

三色标记的过程

  • 初始阶段:所有对象都标记为白色。
  • 标记阶段
    • 从根集合开始,访问和标记对象。首先,将根对象标记为灰色。
    • 遍历灰色对象,将其引用的对象标记为灰色(如果它们是白色的),然后将该对象标记为黑色。
    • 重复这个过程,直到没有灰色对象为止。

三色标记和并发垃圾回收

在并发垃圾回收中,应用线程和垃圾收集线程是同时运行的。这可能导致所谓的“浮动垃圾”(Floating Garbage),即在标记过程中由于应用线程的运行产生的新垃圾。三色标记算法通过维护这些颜色的不变性和使用一些技术,如写屏障(Write Barrier)和增量更新(Incremental Update),来确保垃圾收集的正确性。

类比

可以将三色标记算法类比为管理一家图书馆的过程:

  • 白色书籍:尚未检查的书籍。
  • 灰色书籍:检查过,但书籍里面的引用(例如书中的引用文献)还没有全部检查的书籍。
  • 黑色书籍:完全检查过的书籍,包括书本及其引用的所有内容。

图书管理员(垃圾收集器)需要确保所有的书籍都被正确地检查过,同时处理图书馆运营期间新加入或变动的书籍(应用线程的活动)。

6、堆空间

基本结构

Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 内存中对象的分配与回收。

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)

从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。

<= JDK 7

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  1. 新生代内存(Young Generation)
  2. 老生代(Old Generation)
  3. 永久代(Permanent Generation)

image.png

Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。

= JDK8

JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代,元空间使用的是直接内存

Survivor 区

对象在GC的时候在新生代的一个活动是怎么样的;元空间/永久代有什么用

在Java虚拟机(JVM)中,新生代是堆内存的一部分,主要用于存放新创建的对象。新生代通常分为三个区域:Eden区、两个Survivor区(分别称为S0和S1)。

对象在新生代的分配和回收是垃圾回收机制的核心部分。此外,元空间(或在Java 8之前的版本中的永久代)也在内存管理中扮演着重要的角色。

新生代的内存区域

  1. Eden区:
    • 当新对象被创建时,它们通常首先在Eden区分配。Eden区占新生代的大部分空间。
  2. 两个Survivor区(S0和S1):
    • Survivor区用来保存一次Minor GC后仍然存活的对象。在两个Survivor区中,任何时候只有一个是被使用的(活动的),另一个是空闲的。
两个生存区中,只有一个是空闲的,另外一个是活动的。

对象在新生代的GC活动

在垃圾回收过程中,新生代的对象经历以下活动:

  1. 新对象分配:
    • 新对象首先在Eden区分配。
  2. 第一次Minor GC:
    • 当Eden区满时,进行一次Minor GC。
    • 存活的对象从Eden区移动到一个Survivor区(比如S0),并将其年龄设置为1。
  3. 后续的Minor GC:
    • 在后续的Minor GC中,Eden区和当前活动的Survivor区(比如S0)中存活的对象被移动到另一个Survivor区(比如S1),对象年龄增加1。
    • 如果一个对象的年龄超过了一定的阈值(通常是15),它就会被晋升到老年代。
  4. 两个Survivor区的交换:
    • 每次Minor GC后,活动的和空闲的Survivor区会交换。这意味着如果S0是上次GC后的活动区,那么下次GC时,S1将成为活动区,反之亦然。

类比

可以将新生代的内存管理比作是一个孩子的成长过程:

  • Eden区就像婴儿室,新生的孩子(对象)首先在这里。
  • Survivor区就像学校,经过几次考核(GC)后,表现好的孩子(对象)会转到学校(Survivor区)。
  • 随着孩子们的成长(对象的年龄增长),一些会转到大学(老年代)。

元空间/永久代的作用

  • 永久代(PermGen, Java 8之前):

    • 用于存储JVM加载的类信息、常量池、静态变量等数据。
    • 由于永久代的大小是固定的,它可能会因为类加载器加载了太多的类而导致内存溢出(OutOfMemoryError)。
  • 元空间(Metaspace, Java 8及之后):

    • 元空间取代了永久代,用于存储类的元数据。
    • 元空间使用本地内存(而非JVM内存),因此它的大小只受本地内存限制,这减少了发生内存溢出的风险。

内存分配和回收原则

看一下这个: https://javaguide.cn/java/jvm/jvm-garbage-collection.html#内存分配和回收原则

在Java虚拟机(JVM)中,堆空间是对象存储的主要区域。堆空间的内存分配和回收是垃圾回收机制的核心。以下是JVM中堆空间的内存分配和回收原则的详细解释,我们将使用一些简单的类比来帮助理解。

内存分配策略
  • 对象优先在Eden分配:大多数情况下,对象在年轻代的Eden区分配。当Eden区没有足够空间进行分配时,JVM会进行一次Minor GC。
  • 大对象直接进入老年代:避免在Eden区及两个Survivor区之间发生大量内存复制。
  • 长期存活的对象将进入老年代:对象在年轻代中经过多次GC后仍然存活,会被移动到老年代。

堆空间的内存分配

  1. 对象的创建和存储:
    • 当新对象被创建时,JVM首先会检查堆空间是否有足够的空间分配给这个新对象。
    • 如果有足够空间,对象将被存储在堆内存中;如果没有,JVM会尝试通过垃圾回收来释放空间。
  2. 分代分配策略:
    • 大部分JVM使用分代垃圾回收机制,将堆空间分为年轻代(Young Generation)、老年代(Old Generation)等区域。
    • 新创建的对象首先在年轻代中分配(通常在其中一个称为Eden的区)。当Eden区满时,会进行一次Minor GC,清理年轻代中不再存活的对象。

类比:城市建设

可以将堆空间的内存分配想象为城市建设。新建的建筑(对象)需要在城市(堆空间)中找到合适的位置。新建筑首先会在开发区(年轻代)中找地方,随着时间的推移,一些经久耐用的建筑会被迁移到更稳定的区域(老年代)。

内存回收

堆空间的内存回收

  1. 可达性分析:
    • JVM通过可达性分析算法来判断对象是否存活。从一系列的“根”对象(如线程栈中的局部变量、静态变量等)开始,JVM查找并标记所有可达的对象。不可达的对象可以被认为是垃圾,可以被回收。
  2. 垃圾回收算法:
    • 标记-清除(Mark-Sweep): 标记所有不可达的对象,然后统一清除。
    • 复制(Copying): 将存活的对象复制到另一块区域,然后清理掉原来的全部空间。
    • 标记-整理(Mark-Compact): 标记所有存活的对象,然后将所有存活的对象都向一端移动,清理掉边界以外的内存。
    • 分代收集(Generational Collection): 根据对象存活的时间不同,将堆分为几块,如年轻代、老年代,各代使用最适合的收集算法。
  3. 触发垃圾回收的条件:
    • 当堆空间不足时,例如创建新对象时没有足够的空间。
    • System.gc() 被调用时(注意,这只是建议JVM执行GC,不是强制的)。
    • JVM的内存管理策略决定执行。

类比:城市清洁

垃圾回收可以比作城市的清洁工作。不再使用的建筑(不可达对象)需要被拆除来腾出空间。城市管理者(垃圾收集器)会定期检查哪些建筑不再被使用,并有计划地进行拆除和清理。

7、常见面试题

  • 1、JVM是什么,讲一下运行流程
  • 2、什么是程序计数器
  • 3、介绍一下堆
  • 4、什么是虚拟机栈
  • 5、讲一下方法区
  • 6、听过直接内存吗
  • 7、什么是类加载器,什么是双亲委派机制
  • 8、说一下类加载的执行过程
  • 9、对象什么时候可以被垃圾器回收
  • 10、JVM 垃圾回收算法有哪些
  • 11、说一下 JVM 的分代回收
  • 12、说一下 JVM 有哪些垃圾回收器
  • 13、详细聊一下 G1 垃圾回收器
  • 14、强引用、弱引用、软引用、虚引用的区别
  • 15、JVM调优参数可以在哪里设置参数值
  • 16、用的JVM调优的参数都有哪些
  • 17、说一下 JVM 调优的工具
  • 18、JVM 内存泄漏的排查思路
  • 19、CPU 飙高的排查方案及其思路

常见面试题

image.png

还有一些相关的:沙箱安全机制、动态逃逸

JVM 组成

JVM 是 什么

Java Virtul Machine:Java 程序的运行环境

image.png

JVM 组成

image.png

程序计数器

程序计数器:用于记录正在执行的字节码指令的地址。

image.png

Java 堆

image.png

虚拟机栈

什么是虚拟机栈

  • 每个线程运行时所需要的内存,成为虚拟机栈
  • 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

垃圾回收是否涉及栈内存

  • 垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放

栈内存分配越大越好吗

  • 未必,默认的栈内存通常为 1024 K, 栈帧过大会导致线程数变少

方法内的局部变量是否线程安全

  • 如果方法内局部变量没有逃离方法的作用范围,他是线程安全的
  • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

什么情况下会导致栈内存溢出

  • 1、栈帧过多导致内存溢出,典型问题:递归调用
  • 2、栈帧过大导致栈内存溢出

image.png

堆栈的区别 是 什么

image.png

方法区

image.png

运行时常量池

常量池

image.png

运行时常量池

image.png

总结(方法区)

image.png

直接内存

先看一下普通 IO 和 NIO 的数据拷贝图示区别

普通 IO

使用常规IO 进行数据拷贝的时候,需要使用到 Java 缓冲区 和 操作系统的 系统缓存区的两个缓存区的数据交互(Java 不能直接访问系统缓存区,两个缓存区之间有一个数据复制耗时)

image.png

NIO

提供了一个直接内存(Java 能直接访问)

image.png

总结

image.png

类加载器

类加载器

image.png

双亲委派机制

image.png

类装载的过程

总体的一个过程:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

加载

image.png

验证

image.png

准备

image.png

解析:把类中的符号引用转换为直接引用

image.png

初始化:对类的静态变量,静态代码块执行初始化操作。

初始化的时候关于子类和父类以及静态变量的加载顺序可能要注意一下

image.png

使用: JVM 开始从入口方法开始执行用户的程序代码。

image.png

卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的 Class 对象。

总结

image.png

垃圾回收

对象什么时候可以被垃圾器回收

判断对象是垃圾,则进行回收

回收确认垃圾的方式有两种:

  • 引用计数法
  • 可达性分析算法

image.png

引用计数法

引用计数法:一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收

  • 问题:当对象间出现了循环引用,则引用技术法会失效(两个的引用次数都不是0,不使用的时候引用次数互相为 1)

可达性分析算法

image.png

哪些对象可以作为 GC Root ?

image.png

总结

image.png


垃圾回收算法有哪些

  • 标记清除算法
  • 复制算法
  • 标识整理算法

标记清除算法

(使用较少)

image.png

标记整理算法

image.png

复制算法

image.png

总结:

  • 标记清除算法(使用较少,会产生内存碎片)
  • 标识整理算法(老年代用这种方式回收多一些)
  • 复制算法(新生代用这种方式回收多一些)

image.png


说一下 JVM 的分代回收

工作机制

  • 1、新创建的对象,都会先分配到eden区
  • 2、当伊甸园内存不足,标记伊甸园与 from(现阶段没有) 的存活对象
  • 3、将存活对象采用复制算法复制到to中,复制完毕后,伊甸园和 from内存都得到释放
  • 4、经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将存活的对象复制到from区
  • 5、当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

image.png

MinorGC、Mixed GC、FullGC 的区别是什么?

image.png

总结

image.png


垃圾回收器

在 jvm 中,实现了多种垃圾收集器,包括:

  • 串行垃圾收集器
  • 并行垃圾收集器
  • CMS(并发)垃圾收集器
  • G1垃圾收集器

串行垃圾收集器

image.png

并行垃圾收集器

image.png

CMS(并发)垃圾收集器

image.png

总结

image.png


G1 垃圾收集器

image.png

Young Collection(年轻代垃圾回收)

  • 1、初始时,所有区域都处于空闲状态
  • 2、创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象
  • 3、当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程
  • 5、随着时间流逝,伊甸园的内存又有不足
  • 6、将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代

Young Collection+Concurrent Mark(年轻代垃圾回收+并发标记)

  • 当老年代占用内存超过阈值(默认是45%)后,触发并发标记,这时无需暂停用户线程

  • 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。

  • 这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是Gabage First名称的由来)。

Mixed Collection(混合垃圾回收)

复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集

其中大对象如果一个固定区域不够的话,会分配一个连续的空间给他

总结

image.png


强引用、软引用、弱引用和虚引用的区别?

  • 强引用
  • 软引用

image.png

弱引用

image.png

虚引用

image.png

总结

image.png

JVM 实践

JVM 调优参数可以在哪里设置参数值

一般是分为两种情况,一种是 tomcat 的 war 包,还有一种是 SpringBoot 打包的 Jar 包

War 包

image.png

Jar 包

image.png

总结:

image.png


JVM 调优参数有哪些

官网参考: https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html

一般不会用到很多

对于 JVM调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型。

  • 设置堆空间大小
  • 虚拟机栈的设置
  • 年轻代中Eden区和两个Survivor区的大小比例
  • 年轻代晋升老年代阈值
  • 设置垃圾回收收集器

设置堆空间大小

image.png

虚拟机栈的设置

image.png

年轻代中 Eden 区和 两个 Survivor 区的大小比例

image.png

设置垃圾回收收集器

image.png

总结

image.png


说一下 JVM 调优的工具

image.png

具体待实践使用一下

image.png

  • jmap

image.png

  • jstat

image.png

  • 调优工具:jconsole

image.png

  • 调优工具 VisualVM

image.png

查看运行中的dump

Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中

https://zhuanlan.zhihu.com/p/372721732

可以使用 JProfiler 工具来查看

或者

image.png

总结:略


Java 内存泄漏的排查思路

Java 的内存溢出有三个方向:栈、元空间和堆,栈一般是递归调用造成,堆的情况是平时最多的时候,一般考察也是问堆内存溢出怎么排查。

image.png

排查的话,提供一种方法:生成 dump 文件,然后分析这个 dump 文件

image.png

生成 dump 文件:

  • 一般是设置 JVM 参数,有时候程序是直接闪退挂了,不太可能让你运行的时候使用 jmap 命令(使用 vm 参数获取 dump 文件)

image.png

分析 dump 文件

  • 1、打开 VisualVM
  • 2、选择:文件 → 装入 → (选择文件) → 打开
  • 3、概要 → 堆转储
  • 4、通过查看堆信息的情况,可以大概定位内存谥出是哪行代码出了问题

image.png

总结:

image.png


CPU 飙高排查思路

image.png

参考: https://www.bilibili.com/video/BV1yT411H7YK/?p=134&spm_id_from=pageDriver&vd_source=6a019ecccfe7d8f62b9a3fe99c723bd0

分析一下这个过程

  • 1、使用top命令查看CPU使用情况,确认问题的存在。
  • 2、在top命令输出中,找到CPU使用率最高的Java进程,记下它的PID。比如,Java进程的PID为12345。
  • 3、使用jstack命令导出Java进程的线程堆栈信息到文件。
  • 4、找出CPU使用率最高的线程

查看进程中的线程信息

首先,使用pstop、或jps(Java Virtual Machine Process Status Tool)命令找到Java进程的PID(Process ID)

使用top命令以线程模式运行,指定Java进程的PID:

top -H -p 12345

这将显示进程12345中所有线程的CPU使用情况(切换到top命令的线程视图,查看高CPU使用的线程。)

将线程ID转换为16进制

Linux系统中top命令显示的是线程的PID(实际上是轻量级进程ID,LWP ID),需要将其转换为16进制,以匹配jstack输出中的线程ID。

假设线程ID是6789,可以使用printf命令进行转换:

printf "%x\n" 6789

输出将是线程ID的16进制表示。

获取线程堆栈

使用jstack命令导出Java进程的线程堆栈信息:

jstack 12345 > threadDump.txt

这将把进程12345的所有线程堆栈信息输出到threadDump.txt文件中。

threadDump.txt文件中,查找转换为16进制的线程ID对应的堆栈信息。这通常以nid=0x开头,后跟线程的16进制ID。

定位到高CPU使用线程的堆栈信息后,分析它正在执行的代码。查找该堆栈中的Java方法和类,特别关注自己的应用程序代码。

image.png



参考