JVM-Java虚拟机

⭐️JVM组成?运行流程?

  • Java Virtual Machine,java虚拟机,Java程序的运行环境
  • 好处:一次编写,到处运行;自动内存管理,垃圾回收机制

JVM 主要由四个部分组成: 运行流程:

0. Java 编译器(javac)将 Java 代码转换为字节码(.class 文件)

1. 类加载器(ClassLoader)

  • 负责加载 .class 文件,将 Java 字节码加载到内存

2. 运行时数据区(Runtime Data Area)

  • 管理JVM使用的内存。主要包括:方法区,堆,栈,程序计数器,本地方法栈(Native Method Stack):支持本地方法的调用(通过 JNI)。
  • 其中方法区和堆是线程共享的,虚拟机栈、本地方法栈和程序计数器是线程私有的。

3. 执行引擎(Execution Engine)

  • 负责执行字节码,包含:
  • 解释器:逐条解释执行字节码。
  • JIT 编译器:将热点代码编译为机器码,提高执行效率。
  • 垃圾回收器:回收堆中的不再使用的对象,释放内存。

4. 本地库接口(Native Method Library)

  • 允许 Java 程序通过 java本地接口 JNI (Java Native Interface)调用本地方法(如 C/C++ 编写的代码),与底层系统或硬件交互。

img

1. 运行时数据区

1.1 ⭐️⭐️⭐️运行时数据区中包含哪些区域?

JVM 运行时数据区包括:

  • 程序计数器是每个线程私有的,用于存储当前线程正在执行的字节码指令的地址。
  • 是 JVM 中最大的内存区域,负责存储所有的对象实例和数组,并进行垃圾回收。
  • 方法区存储类的元数据、常量池、静态变量和 JIT 编译后的代码。
  • 虚拟机栈存储每个线程的局部变量、方法调用信息和返回地址等。
  • 本地方法栈支持 JNI 本地方法调用,线程私有。
    专门为本地方法调用而设计。它用于执行本地代码时所需的栈空间。

JDK 1.8 时 JVM 的内存结构主要有两点不同,

  • 一个是方法区(Method Area)在 JDK 1.8 被替换为元空间(Metaspace),且元空间使用本地内存。
  • 另一个是运行时常量池(Runtime Constant Pool)在 JDK 1.7 属于方法区的一部分,而在 JDK 1.8 变成元空间的一部分。

哪些线程私有?哪些共享?

  • 线程私有的:线程计数器、虚拟机栈、本地方法栈
  • 线程共享的:堆、方法区

哪些区域可能会出现 OutOfMemoryError?

  • 堆(Heap): 当堆内存不足时,会抛出OutOfMemoryError。
  • 方法区(Method Area): 当方法区内存不足时,会抛出OutOfMemoryError,通常是由于加载的类太多或常量池中的数据过多。

1.2 什么是程序计数器?

  • 程序计数器(Program Counter Register, PC)JVM 中的一部分,它的作用是跟踪当前线程执行的字节码指令的地址
  • 简单来说,程序计数器记录着每个线程正在执行到哪一条字节码指令,当一条指令执行完后,程序计数器会指向下一条要执行的指令。
  • 这是线程私有的,每个线程有自己的程序计数器,确保线程切换时能够继续执行

1.3 ⭐️⭐️你能给我详细的介绍Java堆吗?

  • Java堆是JVM运行时内存的一部分,它是用于存放对象实例数组的区域。
  • 所有通过new关键字创建的对象,都会存储在堆中。
  • 堆是JVM垃圾回收的主要区域。堆是线程共享的。

它被分为:

年轻代存放新创建的对象,又被划分为:

  • Eden区:存放新创建的对象;
  • Survivor区:存放经过一次垃圾回收后仍然存活的对象,Survivor区分为S0和S1;

老年代:在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。

JDK 7 及之前,JVM 的方法区(也称永久代),保存的类信息、静态变量、常量、编译后的代码;
JDK 8 及之后,永久代被 Metaspace(元空间)取代,移除了永久代,把数据存储到了本地内存的元空间中,且其大小不再受 JVM 堆的限制防止内存溢出

img

1.4 ⭐️⭐️⭐️能不能解释一下方法区?

  • 方法区 是 JVM 运行时数据区的一部分,主要用于存储类的信息、常量、静态变量以及 JIT 编译后的代码
  • 在 JDK 7 之前,这部分内存称为永久代(PermGen),而在 JDK 8 以后,永久代被移除,取而代之的是元空间(Metaspace),它位于本地内存中,不再受堆内存限制。
  • 当 JVM 加载一个类时,类的结构信息(比如类名、方法名、字段、常量池)就会被存放在方法区中。
  • 方法区是所有线程共享的内存区域,不是线程私有的。
  • 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace。

方法区和永久代以及元空间是什么关系呢?

  • 方法区是 JVM 规范中的一块内存区域,它主要用来存储类信息、常量、静态变量和 JIT 编译后的代码,是所有线程共享的。
  • JVM 规范只规定了“方法区”这个概念,并没有规定它具体怎么实现

在实际实现中:

  • JDK 1.7 及之前,HotSpot 虚拟机用 永久代(PermGen) 来实现方法区;
  • JDK 1.8 开始永久代被移除,方法区改用 元空间(MetaSpace) 实现,元空间使用的是 本地内存,不再受堆大小限制。

元空间和方法区的区别?

  • 方法区是 JVM 规范里定义的一块内存区域,用来存储类信息、常量、静态变量等,它本身是一个概念
  • 而元空间(MetaSpace)是从 JDK 1.8 开始,用来实现方法区的一种方式,它和之前的永久代不同,不再使用堆内存,而是使用本地内存(Native Memory)
  • 解决内存溢出问题:JDK 7 之前类元数据存在永久代中,空间固定,容易 OOM;而元空间用的是本地内存,只受系统限制,更灵活。
  • 性能优化:元空间不占用堆,减少了 GC 压力,类元数据的回收由 JVM 和操作系统共同管理,提升了类加载和内存管理效率

1.5 什么是虚拟机栈?

Java Virtual machine Stacks (java 虚拟机栈)

  • 虚拟机栈是 JVM 中用于方法调用和执行的内存区域,每当线程调用一个方法,就会在栈中创建一个栈帧,用来保存这个方法的局部变量、操作数、返回地址等信息。
  • 它是线程私有的,随着线程的创建和销毁而创建或释放,遵循先进后出的原则。
  • 如果方法调用太深或者递归没有出口,虚拟机栈可能会溢出,抛出 StackOverflowError

1.0 垃圾回收是否涉及栈内存?

  • 垃圾回收是否涉及栈内存?垃圾回收主要指就是堆内存
  • 栈内存中不会有垃圾回收的概念,因为栈内存是由 JVM 自动管理的,方法执行完成时,栈帧弹栈,内存就会释放;

1.0 栈内存分配越大越好吗?

  • 未必,默认的栈内存通常为1024k;
  • 栈内存****过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的线程数减半

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

  • 方法内的局部变量在大多数情况下是线程安全的,因为它们只属于当前线程的栈帧,每个线程调用方法时都会创建自己的栈帧,互不影响。
  • 但也有例外:如果局部变量中引用了共享对象,那这个对象就可能在多个线程之间共享,导致线程不安全。

1.0 栈内存溢出情况(StackOverflowError)

  • 栈帧过多导致栈内存溢出;
    典型问题:递归调用会在栈中创建新的栈帧,如果递归深度过大,可能会导致栈空间耗尽,从而抛出
    img
  • 栈帧过大导致栈内存溢出

1.0 堆 栈的区别是什么?

存储内容

  • :主要存储局部变量方法调用的栈帧,每次方法调用时都会压入栈中,方法执行完后弹出。
  • :用于存储对象实例数组。当你使用 new 创建对象时,对象会被分配到堆中。

生命周期

  • :由方法调用控制,方法执行完毕,栈帧就会销毁,自动管理
  • :由垃圾回收器管理,对象会一直存在,直到没有引用指向它,垃圾回收器才回收它

线程隔离

  • :每个线程都有自己的栈,线程私有,栈中的数据只能被该线程访问。
  • :堆是线程共享的,多个线程可以访问堆中的对象,可能会出现线程安全问题。

异常错误不同,但如果栈内存或者堆内存不足都会抛出异常

  • 栈空间不足:java.lang.StackOverFlowError
  • 堆空间不足:java.lang.OutOfMemoryError

1.6⭐️⭐️⭐️Java对象的创建过程?

img

类加载检查

  • 当程序执行到 new 指令时,JVM 会检查对应的类是否已经被加载、解析和初始化。
  • 如果类尚未加载,JVM 会按照类加载机制(加载、验证、准备、解析、初始化)完成类的加载过程。
  • 这确保了类的元数据已经准备好,为后续的对象创建奠定基础。

内存的分配

  • JVM 会为新对象分配内存空间,内存大小在类加载完成后确定。内存分配方式有两种

  • ①指针碰撞,当堆内存是规整的,JVM 会通过指针移动来分配内存。

  • ②空闲列表,当堆内存碎片化时,JVM 会通过空闲列表管理并分配内存。

  • 此外,为了保证多线程的安全性,JVM 会使用 CAS 操作 或为每个线程分配专属内存区域来避免竞争。

零值初始化

  • JVM 会对分配的内存空间进行初始化,将其所有字段设置为零值(如 int 为 0,boolean 为 false,引用类型为 null)。
  • 这一步确保了对象的实例字段在未显式赋值前有一个默认值,从而避免未初始化的变量被访问。

设置对象头

  • 对象头包含Mark Word、Klass Pointer和数组长度。
  • Mark Word 用于存储对象的哈希码、GC 分代年龄、锁状态标志等信息。
  • Klass Pointer 指向对象所属类的元数据。

执行构造方法

  • JVM 会调用构造方法 方法完成对象的初始化。
  • 构造方法会给对象的字段赋值,并调用父类构造方法来完成继承链的初始化。此时,对象才真正完成创建并可供使用。

img

1.7Java 创建对象四种常见方式

  • new 关键字(最常见)

    使用 new 关键字可以直接创建对象,并调用 无参或有参构造方法 进行初始化。

  • 反射

    反射机制可以在运行时动态创建对象。

  • clone() 方法(对象克隆)

    clone() 方法用于创建一个相同内容的新对象,不会调用构造方法。需要实现 Cloneable 接口,并重写 clone() 方法。

  • 反序列化(Serializable)

    反序列化可以将存储或传输的对象数据恢复成 Java 对象,不会调用构造方法。

1.8⭐️对象访问定位的两种方式知道吗?优缺点?

对象访问定位是 JVM 中通过引用变量找到实际对象的过程。在 Java 中,有两种主要的对象访问定位方式:句柄和直接指针。

  • 句柄访问:引用变量存储的是句柄的地址,句柄再指向对象。
    • 优点:GC 时对象移动不影响引用,稳定性高;实例和类型数据分离,便于管理。
    • 缺点:访问慢(两次跳转),内存占用大(需要句柄池)。
  • 直接指针访问:引用变量直接存储对象地址。
    • 优点:访问快(一次跳转),内存占用小。
    • 缺点:GC 时需要修改所有引用,开销大。

1.9 你听过直接内存吗?

  • 直接内存是 JVM 堆外的内存,也称为堆外内存,它并不直接受 JVM 的垃圾回收(GC)管理。
  • 直接内存的分配和释放由操作系统来管理,通常用于处理大规模数据、高性能 I/O 操作(如网络通信、文件操作等),减少了内存复制的开销。

优点

  1. 减少内存复制:在传统的 I/O 操作中,数据需要从操作系统内存复制到 JVM 堆中,而直接内存可以避免这一步,减少数据复制的开销。
  2. 提高性能:直接内存的访问速度较快,因为它不经过 JVM 堆,减少了 GC 的干扰,适合大数据量、高性能应用。

缺点:由于直接内存不受 JVM 的垃圾回收管理,需要手动管理内存的分配和释放,容易出错

img

2. 类加载器

2.1⭐️⭐️⭐️什么是类加载器,类加载器有哪些?

  • JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将.class字节码文件加载到JVM内存,生成对应的Class对象,供程序使用。
  • 启动类加载器:
    负责加载 JDK 核心库,是 最基本的类加载器,通常无法直接访问。
  • 扩展类加载器:
    用于加载 JDK 扩展库。
  • 应用类加载器:
    用于加载classPath下的类,也就是加载开发者自己编写的Java类。
  • 自定义类加载器:
    开发者自定义类继承ClassLoader,实现自定义类加载规则。

img

2.2 ⭐️⭐️⭐️什么是双亲委派模型?

  • 双亲委派模型要求类加载器在加载某一个类时,先委托父加载器尝试加载。
  • 如果父加载器可以完成类加载任务,就返回成功;
  • 只有父加载器无法加载时,子加载器才会加载。

2.3⭐️JVM为什么采用双亲委派机制?

  • 避免类的重复加载:父加载器加载的类,子加载器无需重复加载。
  • 保证核心类库的安全性:为了安全,保证类库API不会被修改。如 java.lang包下的类只能由 启动类加载器加载,防止被篡改。

2.4⭐️说一下类的生命周期?

  • 一个类从被加载到虚拟机内存中开始,到从内存中卸载,它的整个生命周期包括了:
    加载、验证、准备、解析、初始化、使用和卸载这7个阶段。
  • 其中,验证、准备和解析这三个部分统称为连接(linking)。

img

2.5 ⭐️⭐️⭐️说一下类装载的执行过程?

类装载过程包括:载入连接初始化,连接细分为 验证、准备、解析,这是标准的 JVM 类装载流程。

  1. 加载(Loading):通过类加载器找到 .class 文件读取到内存,生成 Class 对象。
  2. 验证:检查字节码是否合法,防止恶意代码破坏 JVM;
  3. 准备:为类的静态变量分配内存并设置默认初始值,但不执行赋值逻辑;
  4. 解析:将常量池中的 符号引用(如类名、方法名)转为 直接引用(内存地址)。
  5. 初始化(Initialization):执行类的静态代码块和静态变量赋值

在准备阶段,静态变量已经被赋过默认初始值了,在初始化阶段,静态变量将被赋值为代码期望赋的值。比如说 static int a = 1;,在准备阶段,a 的值为 0,在初始化阶段,a 的值为 1

类装载完成后的阶段:类进入‘使用阶段’。当 Class 对象不再被引用时,可能触发‘卸载’。

  • 使用:JVM 通过 Class 对象创建实例、调用方法,进入正常运行阶段。
  • 卸载:当 Class 对象不再被引用时,由 GC 回收,但 JVM 核心类不会被卸载。

3. 垃圾回收

3.1 简述Java垃圾回收机制?(GC是什么?为什么要GC)

  • GC(Garbage Collection,垃圾回收)是 Java 中自动管理内存的机制,负责回收不再使用的对象,以释放内存空间。
  • 垃圾回收是 Java 程序员不需要显式管理内存的一大优势,它由 JVM 自动进行。

GC 的主要目的是:

  • 自动管理内存:程序运行过程中会创建大量的对象,但一些对象在使用完后不再被引用。手动管理这些对象的内存释放非常繁琐且容易出错;
  • 防止内存泄漏:如果不及时释放无用对象的内存,系统的可用内存会越来越少;
  • 避免内存溢出:GC 机制能够保证内存不会因为长期积累未回收的对象而耗尽。

内存溢出是指内存使用超出限制,导致程序崩溃,

内存泄漏是指程序未能释放不再使用的对象引用,导致内存无法回收,最终导致内存溢出。

3.2⭐️对象什么时候可以被垃圾器回收?如何判断对象是否死亡?

  • 对象的垃圾回收是由 JVM 的垃圾回收器(GC)管理的。
  • 当一个对象不再有任何有效引用指向它时,垃圾回收器就会认为该对象是可回收的

如果要定位什么是垃圾,有两种方式来确定,

引用计数法

  • 这种方法通过维护每个对象的引用计数器来判断对象是否可回收。当一个对象的引用计数为 0 时,表示没有任何活跃的引用指向该对象,可以回收
  • 缺点:引用计数法不能处理 循环引用 的问题。例如,两个对象相互引用,即使它们不再被其他对象使用,计数也不会归零,因此无法正确回收。

可达性分析算法

  • 这是 Java 中使用的主要垃圾回收方法。 垃圾回收根对象GC Roots 开始,遍历所有对象的引用链。如果一个对象无法从 GC Roots 访问到(即不可达),则认为该对象是垃圾,可以回收。
  • 这种方法能够处理 循环引用 的问题,因为即使对象之间相互引用,只要它们不再被其他对象引用(即不可达),就能被回收。

哪些对象可以作为GC Roots?

GC Roots 是一些 不会被回收 的对象,它们是垃圾回收的起点。垃圾回收器会从这些对象开始,查找它们能“触及到”的其他对象,只有那些不能被这些对象触及到的对象才会被回收。
可以作为 GC Roots 的对象有:

  • 当前线程栈上的对象:方法中使用的局部变量和参数会作为 GC Roots,因为方法调用时它们是可以被访问到的。
  • 静态变量:类的静态变量(static 修饰的字段)是全局共享的,因此它们是 GC Roots。
  • 常量池中的对象:常量池中存储的常量,如字符串常量,是永远存在的,属于GC Roots
  • 活动的线程:正在执行的线程也会被视作 GC Roots,因为线程本身不会被回收。
  • JNI 引用的对象:通过 Java Native Interface(JNI)进行本地代码(如 C、C++)调用时,本地代码持有的对象引用也可以作为 GC Roots。

3.3 ⭐️⭐️⭐️JVM 垃圾回收算法有哪些?

Java 中的垃圾回收器采用不同的算法来回收不再使用的对象。

标记-清除算法是最基础的垃圾回收算法。过程包括:

  • 标记阶段:遍历所有对象,标记出那些可以被回收的对象。
  • 清除阶段:回收所有被标记为可回收的对象,释放其占用的内存空间。
  • 优点:算法简单,回收过程直观。
    缺点:清除时会产生“内存碎片”,无法有效利用内存。

标记-整理算法是标记-清除算法的改进版本。

  • 它在标记阶段和清除阶段后,将剩余的对象紧凑地移动到一起,避免内存碎片问题。
  • 优点:有效减少了内存碎片。
    缺点:回收时需要更多的计算资源和时间。

复制算法

  • 将内存分为两块,每次只使用其中的一块。
  • 当这一块的内存用尽时,将存活的对象从当前块复制到另一块,并清理当前块。
  • 优点:没有内存碎片,回收过程高效。
    缺点:需要两块内存空间,可能浪费一部分内存。

分代回收算法

  • 分代回收是最常用的垃圾回收算法,基于“对象的生命周期”进行优化。
  • 将内存划分为年轻代老年代
    年轻代的对象生命周期短,回收频繁,
    老年代的对象生命周期长,回收较少。
  • 年轻代:使用复制算法,回收频繁,效率高。
    老年代:使用标记-整理标记-清除算法,回收较少。
  • 优点:根据对象的生命周期进行优化,减少回收开销,提高性能。
    缺点:在老年代进行垃圾回收时可能会很耗时,产生Full GC

img

3.4⭐️分代收集算法

分代收集算法-堆的区域划分

  1. java8时,堆被分为了两份:新生代和老年代【1:2】,在java7时,还存在一个永久代
  2. 对于新生代,内部又被分为了三个区域。
    伊甸园 Eden 区,幸存者区 survivor (分成 from 和 to )【8:1:1】

img

分代收集算法-工作机制

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

为什么要用垃圾分代回收?

  • 但我们一般不单独使用某一种垃圾回收算法,由于对象的生命周期是不同的,有的产生后很快可以回收,有的会存活很久,对象的差异化决定了我们要差异化管理它们,所以我们通常把堆分成年轻代和老年代。
  • 年轻代中存储的是朝生夕死的对象,因此它们是会频繁发生GC回收的,所以年轻代一般采用的是复制算法
  • 而老年代中存储的是生命周期很长的对象,因此它们很久才会发生GC回收一次,所以老年代一般采用标记-清除标记-整理算法。

分代收集的优势:

  • 高效回收:通过将对象按存活时间分代,年轻代的回收可以频繁进行,而老年代则较少回收,这样可以显著提高回收效率。
  • 减少停顿时间:年轻代采用复制算法,效率高且停顿时间短,减少了GC对应用程序性能的影响。

Minor GC、 Mixed GC 、 Full GC的区别是什么?

Minor GC(年轻代 GC)

  • 触发条件:当 年轻代(Young Generation)中的内存空间用满时,会触发 Minor GC
  • 回收区域:主要回收的是 年轻代,包括 Eden 区 和两个 Survivor 区
  • 回收方式:通常使用复制算法,将存活的对象复制到另一个Survivor区,并清空Eden区
  • 性能影响:回收频繁,回收速度较快,影响较小。

Mixed GC(混合 GC)

  • 触发条件:当 年轻代老年代 一起需要回收时,会触发 Mixed GC。通常在 G1 垃圾回收器 中使用。
  • 回收区域:除了回收年轻代外,还会回收 老年代 的一部分。
  • 回收方式:结合了 标记-清除复制算法,同时对年轻代和老年代进行回收。
  • 性能影响:回收过程相对较长,影响较大,但回收效率较高。

Full GC(完全 GC)

  • 触发条件:当 老年代 内存用满,或者 JVM 内存空间不足时,会触发 Full GC。也可能因为 永久代元空间(JDK 8 之后)满而触发。
  • 回收区域:回收 年轻代老年代,包括 方法区(元空间)和 堆内存
  • 回收方式:通常使用 标记-整理标记-清除 算法来进行回收。
  • 性能影响:最耗时的GC类型,因为它涉及回收堆的所有区域,可能会导致长时间的停顿

STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成

3.5⭐️⭐️⭐️说下 JVM 有哪些垃圾回收器?

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

串行垃圾****回收器(Serial GC)

  • 核心特点:单线程执行垃圾回收,回收时会暂停所有应用线程(Stop The World, STW)
  • 适用场景:适用于单核或低内存的环境,通常用于 客户端应用小型应用
  • 优点:实现简单,开销小。
    缺点:垃圾回收时会导致应用程序暂停较长时间,不适合大规模应用。

并行垃圾****回收器(Parallel GC)

  • 核心特点:多线程执行垃圾回收,仍会 STW,但效率更高。
  • 适用场景:适用于 多核服务器,追求高吞吐量的应用(如批处理、数据分析)。

并发标记清除垃圾回收器(CMS GC)

  • 通过并发标记和清除,尽量减少应用程序的停顿时间。采用 标记-清除 算法。
  • 适用场景:需要低停顿的应用场景,比如需要响应速度较快的实时系统。
  • 优点:可以减少STW停顿时间,提高用户体验。
    缺点:性能可能不如 Parallel GC,并且会增加 CPU 使用率

优先垃圾回收器(G1 GC)

  • 工作原理:G1 将堆划分为多个区域,优先回收垃圾最多的区域,旨在提供可预测的停顿时间。适用于大内存、高并发场景。
  • 适用场景:适合大内存应用,要求停顿时间可控的系统。
  • 优点:提供可预测的停顿时间,适合大内存和低停顿场景。
    缺点:相对较复杂,调优比较困难。

串行垃圾收集器

img

并行垃圾收集器

img

CMS(并发)垃圾收集器img

3.6 详细说一下G1垃圾回收器

1. G1 的设计目标是 低停顿可预测的垃圾回收时间,特别适合内存非常大的应用程序。它是 JVM的默认垃圾回收器(从JDK 9开始),主要为 大内存应用和对延迟有要求的系统提供优化。

2. G1 GC 会将堆内存分为多个 区域(Region),每个区域的大小是固定的。堆被分成不同类型的区域,主要包括:

  • Eden 区:存放新创建的对象。
  • Survivor 区:用于存放从 Eden 区晋升过来的存活对象。
  • Old 区:存放长时间存活的对象(也称为老年代)。
  • Humongous 区:用于存放大对象(大于一定大小的对象)。

3.G1的关键点在于它通过优先回收垃圾最多的区域,从而 减少回收时的停顿时间。它会在多个并行线程之间分配任务进行 并行回收,并使用 并发标记阶段来确保较少的停顿

大对象的判定规则是,如果一个大对象超过了一个 Region 大小的 50%,比如每个 Region 是 2M,只要一个对象超过了 1M,就会被放入 Humongous 中。

img

G1 收集器的运行过程大致可划分为这几个步骤:

初始标记

  • 标记从 GC Roots 可以直接引用的对象,即标记所有直接可达的活跃对象。这一步会触发一次短暂停顿(Stop-The-World,STW)。

并发标记

  • 这一阶段与应用程序并行运行,垃圾回收器会扫描整个堆,标记堆中所有可达的对象。
  • 这个阶段的时间与堆的大小和对象的数量有关。

混合回收

  • 在并发标记完成后,G1 会计算出哪些堆区域包含最多垃圾,优先回收这些区域。
  • G1 会回收部分年轻代区域和老年代区域,减少停顿时间。
  • 通过选择回收成本低而收益高的区域,G1提高了回收效率,并尽量减少了停顿时间。

可预测的停顿时间

  • G1 允许用户通过 JVM 参数设置期望的最大停顿时间,
  • G1 会尽量在设定的时间内完成垃圾回收,提供可预测的停顿时间,即使在处理较大的内存时,也会控制停顿时间,以确保应用性能。

3.7⭐️JDK中有几种引用类型?分别的特点是什么?

在Java中,引用类型有不同的级别,它们控制着对象的生命周期以及垃圾回收的行为。强引用软引用弱引用虚引用都是Java中引用对象的方式,它们的区别主要体现在垃圾回收器回收对象的时机和条件上。

强引用(Strong Reference)

  • 这是最常见的引用类型,只要一个对象有强引用,它就不会被垃圾回收。
  • 即使内存不足,垃圾回收器也不会回收这个对象。
  • 只有当引用被显式置为null或者没有任何引用指向该对象时,垃圾回收器才会回收它
  • Object obj = new Object(); // 强引用
    obj = null; // 显式置为 null,对象才可能被 GC
  • 若强引用对象过多,可能导致内存泄漏或 OOM。

软引用(Soft Reference)

  • 软引用用于描述那些在内存充足时不应回收、但在内存不足时可以回收的对象。

弱引用(Weak Reference)

  • 弱引用比软引用更弱,每次垃圾回收时都会被回收,无论内存是否充足。

虚引用(Phantom Reference)

  • 虚引用是最弱的引用类型,不能单独使用,它与 引用队列ReferenceQueue 配合使用
  • 表示一个对象在垃圾回收之前的最后时机。当对象被回收时,虚引用会被加入到 ReferenceQueue 中。
  • 虚引用的主要作用是用来监控对象被回收的状态。

4. JVM实践(调优)

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

JVM 调优的参数设置本质上是在应用启动时传递配置,但具体的操作方式根据部署方式有所不同:

  • Spring Boot 项目(JAR 启动方式):直接在命令行启动时添加 JVM 参数。
    java -Xms512m -Xmx1024m -jar app.jar
    这会设置初始堆内存为 512MB,最大堆内存为 1024MB。

  • Tomcat 部署(WAR 包方式):需要在 Tomcat 配置文件中设置参数。
    在 Linux 系统中,编辑catalina.sh 文件:
    JAVA_OPTS=”-Xms512m -Xmx1024m -XX:+UseG1GC”

    -Xms 和 -Xmx 分别设置初始和最大堆内存。 -XX:+UseG1GC 配置使用 G1 垃圾回收器
    在 Windows 中,配置文件是 catalina.bat。

4.2⭐️⭐️常用的 JVM 调优的参数都有哪些?

嗯,这些参数是比较多的。

  • 设置堆内存大小
    -Xms:初始堆内存大小
    -Xmx:最大堆内存大小
    -XX:MaxPermSize(JDK 7 及之前):设置 永久代的最大大小
    -XX:MetaspaceSize(JDK 8 及之后):设置 元空间 初始大小。
  • 设置使用哪种垃圾回收器
    -XX:+UseSerialGC:设置串行收集器
    -XX:+UseParallelGC:设置并行收集器
    -XX:+UseParalledlOldGC:设置并行老年代收集器
    -XX:+UseConcMarkSweepGC:设置并发收集器
  • 设置年轻代中Eden区和两个Survivor区的大小比例
    -XX:NewSize=n:设置年轻代大小
    -XX:NewRatio=n:设置年轻代和年老代的比值。如:n 为 3 表示年轻代和年老代比值为 1:3,年轻代占总和的 1/4
    -XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。如 n=3 表示 Eden 占 3, Survivor 占 2,一个 Survivor 区占整个年轻代的 1/5

4.3⭐️⭐️⭐️JVM 调优的工具?

我们一般都是使用jdk自带的一些工具,比如

命令工具

jps

  • Java 进程监控,列出所有 Java 进程及 PID。

jstack

  • 打印 JVM 线程堆栈信息,查看线程状态。
  • 分析线程死锁、线程阻塞等问题。
  • jstack

jmap

  • 查看 堆内存 使用情况,生成 堆转储(heap dump)
  • 用于分析内存泄漏、堆内存使用情况。
  • jmap -heap

jstat

  • 查看 JVM 垃圾回收内存使用情况
  • 查看堆内存、垃圾回收的实时统计信息。
  • jstat -gcutil

可视化工具

jconsole

  • JDK 自带的图形化工具,用于监控 JVM 的 内存使用垃圾回收线程等。
  • 实时查看应用的运行状况和性能指标。
  • 打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行

img

jvisualvm:故障处理工具

  • 图形化工具,用于 性能监控内存分析垃圾回收分析等。
  • 深入分析 JVM 性能,查看内存、CPU、线程等详细信息。
  • 打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行

img

4.4⭐️Java内存泄露的排查思路?

内存泄漏通常指的是程序中已经不再使用的对象没有被垃圾回收器回收,导致内存得不到释放,最终可能导致应用程序内存溢出。

  • 使用 jmap 工具来获取 JVM 堆内存的 heap dump(堆转储文件)。该文件包含了堆内存中所有对象的信息,能够帮助我们分析内存泄漏的根本原因。
  • VisualVM 是一个图形化工具,能够加载堆转储文件,并提供详细的 内存使用情况对象的引用关系 等信息。VisualVM 可以帮助我们定位哪些对象占用了大量内存,或者哪些对象不应存在但仍然存在;
  • 通过查看堆信息的情况,可以大概定位定位泄漏源修复代码中的不当资源管理。

4.5⭐️CPU飙高排查方案与思路?

  • 使用 top 命令查看占用cpu的情况;
  • 通过top命令查看后,可以查看是哪一个进程占用cpu较高,记录这个进程id;
  • 使用 ps 命令查看进程中的线程信息,看看哪个线程的cpu占用较高;
  • 使用 jstack 命令查看进程中哪些线程出现了问题,最终定位问题代码的行号

JVM-Java虚拟机
https://blog.xirui.work/posts/2f0fd555.html
作者
xirui
发布于
2025年5月30日
许可协议