当前位置: 首页 > news >正文

Java-面试八股文-JVM篇

JVM篇

一.在JVM中,什么是程序计数器?

JVM(Java Virtual Machine) 中,程序计数器(Program Counter Register,简称 PC 寄存器) 是一块较小的内存空间,用于记录 当前线程所执行的字节码的行号指示器


1. 程序计数器的作用

  • JVM 的字节码解释器在工作时,需要依靠程序计数器来 确定下一条需要执行的字节码指令
  • 程序计数器存储的内容可以看作是 当前线程所执行的字节码的地址(行号)
  • 如果执行的是 本地方法(native 方法),那么程序计数器的值为 未定义(Undefined)

2. 为什么需要程序计数器

  • 多线程环境下,JVM 通过 线程切换 来实现并发执行。
  • 每条线程都需要记录自己执行到哪里了,所以 程序计数器是线程私有的,每个线程都有独立的 PC 寄存器。
  • 当线程切换回来时,程序计数器能帮助 JVM 知道该线程应该 从哪条指令继续执行

3. 特点

  • 占用内存非常小,几乎可以忽略。
  • JVM 规范中唯一一个没有规定任何 OOM(OutOfMemoryError)情况的内存区域。
  • 属于线程私有(Thread-Private)内存。

二.你能详细给我介绍一下Java堆吗?

1. 什么是 Java 堆(作用)

Java 堆是 JVM 管理的 一块用于存放 Java 对象实例(以及数组)的运行时内存区域
它是 所有线程共享 的堆内存区(与线程私有的栈、程序计数器不同)。JVM 的垃圾回收器(GC)主要作用于堆:及时回收不再被引用的对象,防止内存泄漏/耗尽。


2. 堆的逻辑划分(世代/区域)

传统的“堆”按代(Generation)

  • Young(新生代)

    • Eden(伊甸区)
    • Survivor0(S0)/ Survivor1(S1)——两个幸存者区交换使用
      新生代主要承载新创建的对象。大多数对象短命,会在这里被回收(Minor GC)。
  • Old / Tenured(老年代 / 年长代)
    存放在多次 GC 后仍然存活、被晋升(promote)的对象。对老年代的回收通常更昂贵(Major/Full GC)。

注意:JDK 8 后的 Metaspace(方法区)已经移出堆(替代 PermGen)。Metaspace 存放类元数据,不属于堆空间。

在这里插入图片描述


三.什么是虚拟机栈? 垃圾回收机制是否涉及栈内存? 栈内存是越大越好吗 ?方法内的局部变量是否线程安全? 什么情况下会导致栈内存溢出?


1) 什么是虚拟机栈

  • 虚拟机栈(JVM Stack)是 JVM 为每个 Java 线程创建的私有内存区域。
  • 栈由若干**栈帧(StackFrame)**组成:每个方法调用对应一个栈帧,栈帧里保存方法的局部变量表、操作数栈、常量池引用和返回地址等。
  • 线程结束时其虚拟机栈被回收。
  • 举例:线程 A 调用 foo()bar(),会在栈中先后压入 foobar 的帧,bar 返回后其帧弹出。

2) 垃圾回收机制是否涉及栈内存

  • GC 主要回收堆(Heap)上的对象,栈上的局部变量本身不被 GC。
  • 但是,栈上的引用(局部变量指向的对象引用)会被当作 GC Roots,GC 会从这些根开始标记可达对象,从而间接影响回收。
  • 举例:方法中 Object o = new Object();,只要 o 仍在栈上可达,那个对象不会被回收;方法返回后 o 不再可达,对象就可能被回收。

3) 栈内存是越大越好吗?

  • 不是绝对越大越好,有利有弊:

    • 增大 -Xss 可以支持更深的调用深度或更大的栈帧(例如深递归),减少 StackOverflowError 风险。
    • 但每个线程都占用这个栈空间,栈越大可同时支持的线程数越少,一个栈对应一个线程,栈的内存过大,可能导致系统无法创建更多线程或出现 OOM。
  • 建议:一般使用默认大小,只有在确切需要(深递归或特殊 native 调用)时才调大,或在大量线程场景下调小。


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

  • 局部基本类型变量(如 int)和局部引用变量本身是线程私有的,所以它们的存取不会被多个线程同时修改——局部变量本身是线程安全的

  • 但局部变量引用的对象可能是共享的,如果该对象被多个线程访问则可能不安全。

  • 举例

    void f() {int a = 0;              // 线程安全List<String> list = new ArrayList<>(); // 如果不把 list 发布到其他线程,则安全sharedList.add("x");    // 如果 sharedList 是共享的,可能产生线程安全问题
    }
    
  • 若需要在不同线程间隔离数据,可使用 ThreadLocal<T>


5) 什么情况下会导致栈内存溢出(StackOverflow)

  • 典型原因:无限/过深递归(最常见)、极深的调用链、或每帧占用太多栈空间(极少见于 Java,但可发生在 JNI/native 代码中)。

  • 另外,创建大量线程(每个线程都占栈)也会因为总栈消耗过大而引发 OutOfMemoryError 或无法创建新线程。

  • 错误表现java.lang.StackOverflowError(单线程栈溢出);大量线程耗尽内存可能出现 OutOfMemoryError: unable to create new native thread

  • 举例(递归导致)

    void recurse() { recurse(); } // 调用会很快抛出 StackOverflowError
    

四.能不能解释一下方法区,介绍一下运行时常量池?

1.方法区(Method Area)

定义
方法区是Java虚拟机(JVM)规范中定义的运行时数据区的一部分,用于存储已被虚拟机加载的类信息、常量、静态变量等数据。它是线程共享的内存区域,所有线程都可以访问方法区中的数据。


方法区的特点
  1. 线程共享:方法区是所有线程共享的内存区域。
  2. 逻辑内存区:方法区是JVM规范中的一个逻辑概念,具体实现依赖于JVM的实现方式。例如:
    • JDK 1.7及之前:方法区通过永久代(PermGen)实现,存储在Java堆的永久代中。
    • JDK 1.8及之后:方法区通过元空间(Metaspace)实现,使用本地内存(Native Memory)存储类的元数据,与Java堆分离。
  3. 内存回收:方法区的垃圾回收效率较低,主要回收废弃的类信息常量池中的无用常量
  4. 动态调整:方法区的大小可以动态调整(如元空间的默认大小不受限制,但可以通过参数配置)。

方法区存储的内容
  1. 类信息
    • 类的全限定名(如 java.lang.String)。
    • 类的父类、接口、修饰符(如 publicabstract)。
    • 字段(属性)的名称、类型、修饰符。
    • 方法的名称、参数、返回值、修饰符、字节码(方法体)。
  2. 运行时常量池:存储编译期生成的字面量和符号引用(后文详细说明)。
  3. 静态变量:被 static 修饰的类变量。

2.运行时常量池(Runtime Constant Pool)

定义
运行时常量池是方法区的一部分,用于存储类文件中的常量数据(如字面量和符号引用),并在运行时进行动态解析和扩展。它是每个类或接口的运行时数据,由JVM在加载类时从类文件的常量池解析而来。


运行时常量池的作用
  1. 存储字面量和符号引用
    • 字面量:如字符串("Hello")、整数(123)、浮点数(3.14)等。
    • 符号引用:类、字段、方法的符号名称(如 java/lang/Object.toString:()Ljava/lang/String;)。
  2. 支持动态链接:符号引用在运行时会被解析为直接引用(如内存地址)。
  3. 节省内存:相同的数据在常量池中只存储一份,避免重复。

运行时常量池的存储内容
  1. 字面量(Literals)
    • 字符串常量(如 "Hello")。
    • 数值常量(如 int 42double 3.14)。
    • final 常量(如 static final int MAX = 100;)。
  2. 符号引用(Symbolic References)
    • 类和接口的全限定名:如 java/lang/String
    • 字段的符号引用:包含字段的类名、字段名、字段描述符(如 Ljava/lang/String;)。
    • 方法的符号引用:包含方法的类名、方法名、参数类型和返回值类型(如 main([Ljava/lang/String;)V)。
  3. 动态生成的常量
    • 通过 String.intern() 方法添加的字符串。
    • 动态语言支持(如 invokeDynamic 指令生成的调用点)。

运行时常量池的版本差异
  1. JDK 1.6及之前
    • 运行时常量池和字符串常量池都位于永久代(PermGen)
    • 如果常量池过大,可能导致 OutOfMemoryError: PermGen space
  2. JDK 1.7及之后
    • 字符串常量池被移到Java堆中。
    • 其他常量池数据(如符号引用)仍保留在方法区(元空间)。
  3. JDK 1.8及之后
    • 方法区通过**元空间(Metaspace)**实现,使用本地内存,不再受Java堆大小的限制。
    • 如果元空间内存不足,会抛出 OutOfMemoryError: Metaspace

示例:运行时常量池的作用
public class Example {public static void main(String[] args) {String str1 = "Hello"; // 字符串字面量,存储在运行时常量池String str2 = "Hello"; // 直接引用常量池中的"Hello"String str3 = new String("Hello"); // 堆中新建对象System.out.println(str1 == str2); // true(常量池引用)System.out.println(str1 == str3); // false(堆对象 vs 常量池)}
}
  • str1str2:都指向运行时常量池中的 "Hello"
  • str3:通过 new 创建的新对象,存储在堆中,与常量池无关。

常见问题
  1. 为什么需要运行时常量池?
    • 节省内存:共享相同的数据(如重复的字符串、类名)。
    • 支持动态链接:符号引用在运行时解析为直接引用,实现类、方法的动态绑定。
  2. 运行时常量池会导致内存溢出吗?
    • 在 JDK 1.6 及之前,如果常量池过大,可能导致 PermGen space 溢出。
    • 在 JDK 1.8 及之后,元空间使用本地内存,默认不限制大小,但仍需合理配置(如 -XX:MaxMetaspaceSize)。

总结

  • 方法区是JVM的逻辑概念,存储类信息、常量、静态变量等,JDK 1.8之后通过元空间实现。
  • 运行时常量池是方法区的一部分,存储编译期生成的字面量和符号引用,并在运行时动态解析。
  • 版本差异:JDK 1.7之后字符串常量池移至堆中,JDK 1.8之后元空间取代永久代,解决了固定内存限制的问题。

五.你听说过直接内存吗,解释一下?


什么是直接内存

  • 直接内存 指的是 JVM 通过 Unsafe 或者 NIO 中的 ByteBuffer.allocateDirect() 方法,直接向操作系统申请的内存。
  • 这块内存不受 JVM 堆大小参数(如 -Xmx)限制,而是受到 本机物理内存-XX:MaxDirectMemorySize 参数限制。

为什么要有直接内存

在这里插入图片描述

在这里插入图片描述

因为传统 Java 堆内存的读写需要 先复制到 JVM 内存,再复制到操作系统内核内存,效率低。
而直接内存避免了这层拷贝:

  • I/O 操作(比如网络传输、文件读写)可以直接操作这块内存,减少一次拷贝,提高性能。

典型场景:Java NIO 中的 零拷贝(Zero-Copy)


特点

  1. 分配和销毁成本比堆内存高。
  2. 访问速度通常比堆内存快,特别是在大数据量 I/O 场景下。
  3. 可能会导致 内存溢出(OutOfMemoryError: Direct buffer memory),即使堆内存还有空间,因为它不算在 -Xmx 里面。

举例

import java.nio.ByteBuffer;public class DirectMemoryDemo {public static void main(String[] args) {// 分配 100MB 直接内存ByteBuffer buffer = ByteBuffer.allocateDirect(100 * 1024 * 1024);System.out.println("分配了100MB直接内存");}
}

如果运行时不加参数 -XX:MaxDirectMemorySize=200m,但分配超过默认限制,就可能抛出:

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

六.什么是类加载器,类加载器的种类有哪些?


什么是类加载器(ClassLoader)

  • 类加载器的作用:把字节码文件(.class)加载到 JVM 内存中,并生成对应的 Class 对象

类加载器的种类

JVM 规范里,类加载器分为两大类:

  • 启动类加载器(Bootstrap ClassLoader)
  • 其他类加载器(继承自 ClassLoader 的加载器)

实际常见的类加载器有:

1. 启动类加载器(Bootstrap ClassLoader)

  • C++ 编写,属于 JVM 本地代码的一部分。
  • 主要加载 JDK 核心类库

2. 扩展类加载器(Extension ClassLoader)

  • Java 实现,父加载器是 Bootstrap
  • 加载 扩展目录(ext) 下的类(早期 JDK 是 jre/lib/ext,后来 JDK9 之后改为模块化)。
  • 负责加载一些非核心但又是 JDK 提供的扩展类库。

3. 应用类加载器(Application ClassLoader / System ClassLoader)

  • 也叫 系统类加载器
  • 由 Java 实现,父加载器是 扩展类加载器
  • 负责加载 classpath 下的类(我们自己写的代码一般都是它加载的)。

4. 自定义类加载器

  • 开发者可以继承 ClassLoader,实现自定义加载逻辑。

  • 常见应用场景:

    • 热部署 / 插件机制(如 Tomcat、Spring Boot DevTools)。
    • 字节码加密与解密(防止源码被反编译)。

类加载器的层次结构(双亲委派模型)

在这里插入图片描述


七.什么是双亲委派模型,JVM为什么要采用双亲委派机制?


1.什么是双亲委派模型(Parent Delegation Model)

定义:
在 JVM 中,类加载器在加载类时,并不是自己马上去尝试加载,而是把请求交给父类加载器去处理,父类加载器再交给更上层,直到 启动类加载器

  • 如果父类能完成加载,就直接返回结果。
  • 如果父类不能完成加载(即找不到对应的类),再由当前类加载器自己尝试去加载。

2.双亲委派模型的好处

  1. 避免类的重复加载

    • 保证同一个类只会由一个类加载器加载,避免不同类加载器重复加载相同类导致冲突。
    • 例如:java.lang.String 只能由 启动类加载器 加载,不会被用户自己写的类覆盖。
  2. 保证核心类库的安全性

    • 如果用户自己写了一个 java.lang.String 类,放在 classpath 下。
    • 由于双亲委派,应用类加载器在加载 String 时,会先交给父加载器,最终由 启动类加载器 加载真正的 String
    • 避免了用户恶意替换核心类库。
  3. 实现了类加载器的层级结构,模块化清晰

    • 每个加载器只关注自己职责范围内的类:

      • Bootstrap → JDK 核心类库
      • Extension → 扩展类库
      • Application → 应用程序类
    • 既有分工,又能保证统一性。


八. 说一下类加载的执行过程?


📌 JVM 类加载的执行过程

1. 加载(Loading)

  • 作用:将类的字节码文件(.class)读入内存,并创建一个 Class 对象。

  • 加载器:由 类加载器(ClassLoader) 完成,使用 双亲委派模型 来定位和加载类。


2. 链接(Linking)

分为三步:

  1. 验证(Verification)

    • 确保字节码文件格式正确,不会危害 JVM 安全。
  2. 准备(Preparation)

    • 为类的 静态变量(static 字段) 分配内存,并赋予默认值。

    • 注意:这里只赋默认值 0 / null / false,不会执行任何赋值语句。

    • 比如:

      public static int a = 10;
      

      准备阶段a 的值是 0,不是 10。

  3. 解析(Resolution)

    • 把常量池里的符号引用(字符串形式的类、方法、字段名)替换为 直接引用(内存地址)
    • 比如:"java/lang/String" → 变成真正的 String.class 对象引用。

3. 初始化(Initialization)

  • 真正执行类变量的初始化代码,以及执行 静态代码块

  • 按照源代码中定义的顺序执行。

  • 初始化子类前,必须先初始化父类,但 使用父类时,不会触发子类初始化。

  • 比如:

    public class Test {static int a = 10;         // ①static { a = 20; }        // ②
    }
    

    👉 初始化后 a = 20,因为静态代码块在变量赋值之后执行。


✅ 总结:
类加载过程 = 加载 → 链接(验证、准备、解析) → 初始化,其中初始化阶段才会执行静态变量赋值和静态代码块。

九.在类加载中,准备阶段 和 初始化阶段 对不同类型变量(普通、static、final)的处理过程是什么?


1. 普通成员变量(非 static

  • 准备阶段:不处理(因为普通成员变量属于对象实例,不属于类)。
  • 初始化阶段:在对象实例化时,随着构造方法一起执行赋值。
public class Demo {int a = 10; // 普通成员变量
}

👉 a 的赋值要等到 new Demo() 时才发生。


2. 静态变量(static

  • 准备阶段:分配内存并赋默认值(0falsenull)。
  • 初始化阶段:执行显式赋值语句、静态代码块,按代码顺序赋值。
public class Demo {static int a = 10;   // ① 显式赋值static { a = 20; }   // ② 静态代码块
}

👉 准备阶段:a = 0
👉 初始化阶段:先执行 ① → a = 10,再执行 ② → a = 20


3. final static 变量

  • 情况 1:编译期常量(基本类型或 String,值在编译期已确定)

    • 准备阶段:直接赋初始值(不会等到初始化阶段)。
    • 因为编译器在编译时就把值放进了 常量池
    public class Demo {public static final int A = 100;public static final String B = "Hello";
    }
    

    👉 在 准备阶段A = 100B = "Hello"

  • 情况 2:运行期才能确定的值(如 new 对象,方法返回值)

    • 准备阶段:赋默认值(0/null)。
    • 初始化阶段:执行赋值操作。
    public class Demo {public static final Integer C = Integer.valueOf(10); // 运行时决定
    }
    

    👉 准备阶段:C = null
    👉 初始化阶段:C = Integer.valueOf(10)


4. 普通 final 变量(非静态)

  • 属于对象实例变量,不在类加载阶段处理。
  • 必须在构造方法或声明时赋值。
public class Demo {final int x = 5;   // 声明时赋值final int y;       Demo(int y) {      // 或者构造方法里赋值this.y = y;}
}

十.对象什么时候能被垃圾器回收?


1. 引用计数法(Reference Counting)

原理

  • 给每个对象维护一个 引用计数器

    • 每当有一个地方引用它,计数器 +1。
    • 引用失效时,计数器 -1。
  • 当计数器 = 0 时,说明对象不可用,可以被回收。

优点

  • 实现简单,效率高。
  • 一旦计数为 0 就可以立即回收对象(不用等 GC 扫描)。

缺点

  • 无法解决循环引用问题
    两个对象互相引用,即使外部没有引用,它们的计数器也不是 0,无法被回收。

举例

class Node {Node next;
}
public class Test {public static void main(String[] args) {Node a = new Node();Node b = new Node();a.next = b;b.next = a;a = null;b = null; // 外部都断开了引用// 但 a 和 b 互相引用,计数不为 0 → 无法回收}
}

👉 因为这个问题,Java 没有采用引用计数法


2. 可达性分析法(Reachability Analysis)

原理

  • JVM 从一组称为 GC Roots 的对象出发,沿着引用链向下搜索。
  • 如果一个对象与 GC Roots 没有任何引用链相连,就判定为不可达对象 → 可以被回收。

GC Roots 包括:

  • 虚拟机栈中引用的对象(方法参数、局部变量等)
  • 方法区中静态变量引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈(JNI)引用的对象

GC Roots = JVM 里一切“根源性引用”的集合,比如线程、栈变量、静态变量、常量、JNI 引用等。

优点

  • 可以有效避免循环引用问题。
  • 更符合现代编程语言的需求,因此 Java 采用可达性分析来判断对象是否存活

示例

class Parent {static Parent p; // 静态变量(GC Root)int[] arr = new int[1024];
}public class Test {public static void main(String[] args) {Parent obj = new Parent(); // 局部变量 objobj = null; // 断开 obj// obj 没有任何 GC Roots 引用 → 可被回收// 但如果 Parent.p = obj; 那么对象就存活}
}

十一.JVM垃圾回收算法有哪些?

JVM 的垃圾回收算法主要有以下几类,每一种算法在不同的场景下各有优缺点:


1. 标记-清除(Mark-Sweep)

  • 过程

    1. 从 GC Roots 开始标记所有可达对象。
    2. 清除未被标记的对象,回收内存。
  • 优点:实现简单。

  • 缺点

    • 会产生 内存碎片,不连续的内存影响大对象分配。
  • 示例:假设堆内存像一张纸,标记可用的格子后,擦掉不用的内容,但留下了空洞。


2. 复制算法(Copying)

  • 过程

    1. 把内存分为两块(例如 Eden + Survivor0 / Survivor1)。
    2. 每次只用其中一块,当垃圾回收时,把存活对象复制到另一块,清空原来的区域。
  • 优点

    • 没有碎片问题。
    • 分配速度快(指针碰撞分配)。
  • 缺点

    • 需要 双倍内存 空间。
  • 示例:就像有两张纸,只写一张,用完后把重要的内容抄到另一张,再把旧纸扔掉。


3. 标记-整理(Mark-Compact)

  • 过程

    1. 标记存活对象。
    2. 将存活对象移动到内存的一端。
    3. 清理掉边界外的垃圾对象。
  • 优点:解决了内存碎片问题。

  • 缺点:移动对象需要额外开销,效率比复制算法低。

  • 示例:像书架整理,把要保留的书一本本挪到左边,右边空出来。



总结

  • 标记-清除:简单,但有碎片。
  • 复制算法:无碎片,速度快,但浪费内存。
  • 标记-整理:无碎片,但速度慢。

十二.说一下JVM中的分代回收?


一、JVM 堆的分代结构

JVM 堆通常分为:

  1. 新生代(Young Generation)

    • 包含 Eden 区Survivor0 (S0)Survivor1 (S1)
    • 特点:对象朝生夕死,存活率低。
    • 默认比例:Eden : S0 : S1 = 8 : 1 : 1
  2. 老年代(Old Generation)

    • 存放经过多次 GC 仍存活的对象。
    • 特点:对象存活率高,内存大。
  3. 永久代(PermGen)/ 元空间(Metaspace,Java 8+)

    • 存放类元数据(类结构、方法元信息等)。
    • JDK8 之后用本地内存实现 Metaspace,不再在堆里。

二、回收过程

1. 新生代回收(Minor GC / Young GC)

  • 触发条件:Eden 区满。

  • 算法复制算法(Copying)

  • 过程

    1. 存活对象从 Eden + 一个 Survivor 区,复制到另一个 Survivor 区。
    2. 清空 Eden 和用过的 Survivor 区。
    3. 如果 Survivor 放不下,部分对象会晋升到老年代(称为 晋升/提升)。

2. 老年代回收(Major GC / Old GC)

  • 触发条件:老年代空间不足。
  • 算法标记-清除(Mark-Sweep)或 标记-整理(Mark-Compact)
  • 特点:回收速度慢,可能会导致应用停顿时间长。

3. 整堆回收(Full GC)

  • 触发条件

    • 老年代空间不足;
    • 元空间不足;
    • System.gc() 调用;
    • 其他 GC 策略触发。
  • 过程:回收新生代 + 老年代 + 元空间。

  • 代价:非常昂贵,应尽量避免频繁 Full GC。


十三.说一下JVM有哪些垃圾和回收器?


一、JVM 垃圾回收的基本原理

  • 垃圾回收器是负责回收不再使用的对象的组件。
  • JVM 的垃圾回收主要关注 堆内存(Heap)方法区(MetaSpace) 的回收。
  • 回收策略基于 分代回收(Generational GC)垃圾收集算法,而不同的垃圾回收器实现了不同的回收策略和算法。

二、JVM 常见的垃圾回收器

1. Serial GC(串行回收器)

  • 特点:使用单线程进行垃圾回收,适用于单核 CPU 系统。

  • 使用场景:低内存和小型应用。

  • 回收过程

    • 新生代使用 复制算法(Copying)。
    • 老年代使用 标记-清除标记-整理(Mark-Compact)。
  • 启动方式
    -XX:+UseSerialGC


2. Parallel GC(并行回收器)

  • 特点:多线程进行垃圾回收,通过并行回收提高吞吐量,适用于多核 CPU 系统。

  • 使用场景:需要高吞吐量的应用。

  • 回收过程

    • 新生代使用 复制算法(Copying)。
    • 老年代使用 标记-整理(Mark-Compact)。
  • 启动方式
    -XX:+UseParallelGC


3. CMS GC(Concurrent Mark-Sweep, 并发标记-清除回收器)

  • 特点:通过并发回收减少停顿时间,适用于低停顿应用。

  • 使用场景:要求低延迟的应用。

  • 回收过程

    • 新生代使用 复制算法(Copying)。
    • 老年代使用 标记-清除(Mark-Sweep)+ 并发清除
  • 启动方式
    -XX:+UseConcMarkSweepGC


4. G1 GC(Garbage-First Garbage Collector)

  • 特点:适合多核 CPU 和大堆内存的环境,目标是实现高效的垃圾回收,同时降低停顿时间。

  • 使用场景:适合大内存、高并发、要求低延迟的应用。

  • 回收过程

    • G1 会将堆分成多个 Region,每个 Region 由 G1 回收器动态选择回收。
    • 新生代和老年代采用不同的回收策略,G1 会通过预测停顿时间来选择回收哪些区域。
  • 启动方式
    -XX:+UseG1GC


三、总结

  • Serial GC:适用于小型应用,单线程,回收效率较低。
  • Parallel GC:适用于多核 CPU,大型应用,追求高吞吐量。
  • CMS GC:适用于低停顿、高并发应用,减少停顿时间。
  • G1 GC:适合大内存、高并发应用,平衡吞吐量和停顿时间。

十四.请详细聊一下Java中的G1垃圾分类回收器?


1. G1的核心设计理念

G1的设计目标是通过灵活的分区管理优先级回收策略,解决传统垃圾回收器(如CMS)的痛点,例如:

  • 内存碎片化:CMS的标记-清除算法可能导致碎片,无法分配大对象。
  • 不可预测的停顿时间:CMS和Parallel Scavenge的停顿时间难以控制。
  • 全堆回收的开销:传统回收器需要对整个堆进行回收,效率低下。
G1的核心思想:
  • 分区(Region)管理:将堆划分为多个大小相等的独立区域(Region),每个区域可以动态分配给新生代或老年代。
  • 增量式回收:每次只回收部分区域(Collection Set),避免全堆回收。
  • 可预测的停顿时间:通过启发式算法和用户设定的停顿目标(如-XX:MaxGCPauseMillis),控制GC停顿时间。
  • 并发与并行结合:在标记和清理阶段充分利用多核CPU资源。

2. G1的内存模型

G1将堆划分为多个Region(默认大小1MB~32MB,可通过-XX:G1HeapRegionSize调整),每个Region可以属于以下类型之一:

  • Eden Region:存放新创建的对象(属于新生代)。
  • Survivor Region:存放年轻代GC后存活的对象。
  • Old Region:存放存活时间较长的对象(属于老年代)。
  • Humongous Region:专门存储巨型对象(大小超过Region的一半)。
逻辑分代 vs 物理分代
  • 传统分代(如CMS):新生代和老年代是物理上连续的内存区域。
  • G1逻辑分代:新生代和老年代是逻辑上的概念,Region可以动态分配到任意分代中。

3. G1的工作原理

G1的回收过程分为四个主要阶段,通过增量式回收优先级列表实现高效垃圾回收:

1. 年轻代回收(Young GC)
  • 触发条件:Eden区填满时触发。
  • 过程
    1. 复制算法:将Eden和Survivor中的存活对象复制到新的Survivor区域。
    2. 对象晋升:如果Survivor区域不足,部分存活对象晋升到老年代。
    3. 清理空Region:回收不再使用的Eden和Survivor Region。
2. 并发标记(Concurrent Marking)
  • 目标:标记老年代中的垃圾对象。
  • 步骤
    1. 初始标记(STW):标记从根节点直接引用的对象。
    2. 并发标记:与用户线程并发执行,遍历老年代对象图。
    3. 最终标记(STW):处理并发标记期间的剩余任务。
    4. 筛选回收(Mixed GC):选择垃圾最多的Region进行回收(混合回收新生代和部分老年代)。
3. 混合回收(Mixed GC)
  • 特点:在年轻代回收的基础上,回收部分老年代Region。
  • 优先级策略:基于Region的垃圾比例和回收成本,优先回收垃圾最多的Region(Garbage-First名称的由来)。
4. 完全GC(Full GC)
  • 触发条件:堆内存不足或G1无法回收足够空间时触发。
  • 实现方式:使用单线程的标记-整理算法,停顿时间较长,需尽量避免。

4. G1的关键特性

特性描述
分区管理堆被划分为多个Region,灵活分配到不同代,减少内存碎片。
可预测的停顿时间用户通过-XX:MaxGCPauseMillis设置目标停顿时间(默认200ms),G1会尽力满足。
并发与并行并发标记阶段与用户线程并行运行;并行阶段(如Young GC)利用多核CPU加速。
空间整合使用复制算法回收Region,避免内存碎片(对比CMS的标记-清除)。
动态调整Region的分配和回收策略动态调整,适应不同负载场景。

十五.强引用,软引用,弱引用,虚引用的区别是什么?


1. 强引用(Strong Reference)

  • 定义:最常见的引用类型,通过直接赋值(如 Object obj = new Object())创建。
  • 回收时机:只要存在强引用指向对象(GC Roots 能到达的对象),垃圾回收器永远不会回收该对象,即使内存不足。
  • 使用场景
    • 普通的业务对象(如业务实体、数据模型等)。
    • 需要长期存活的对象(如缓存中的关键数据)。
  • 示例
    Object strongRef = new Object(); // 强引用
    strongRef = null; // 显式置为null后,对象可被回收
    System.gc(); // 建议JVM回收
    

2. 软引用(Soft Reference)

  • 定义:通过 SoftReference<T> 类创建,表示“有用但非必需”的对象。
  • 回收时机
    • 在内存充足时,不会被回收
    • 一旦内存不足(OOM)时,会被回收以释放内存。
  • 使用场景
    • 内存敏感的缓存(如图片缓存、缓存池),在内存不足时自动清理。
    • 避免因缓存占用过多内存导致OOM。
  • 示例
    Object obj = new Object();
    SoftReference<Object> softRef = new SoftReference<>(obj);
    obj = null; // 移除强引用
    // 当内存不足时,softRef.get() 可能返回 null
    

3. 弱引用(Weak Reference)

  • 定义:通过 WeakReference<T> 类创建,表示“非必需”的对象。
  • 回收时机
    • 下一次垃圾回收时,只要没有强引用,就会被回收。
    • 与内存是否充足无关
  • 使用场景
    • 监听对象的回收(如监听某个对象是否被销毁)。
    • 避免内存泄漏(如缓存中临时对象)。
  • 示例
    Object obj = new Object();
    WeakReference<Object> weakRef = new WeakReference<>(obj);
    obj = null; // 移除强引用
    // 下一次GC后,weakRef.get() 会返回 null
    

4. 虚引用(Phantom Reference)

  • 定义:通过 PhantomReference<T> 类创建,不能通过 get() 方法获取对象
  • 回收时机
    • 对象被回收后,虚引用才会被加入引用队列,由Reference Handler线程执行相关内存的清理操作。
  • 使用场景
    • 资源清理(如关闭文件句柄、释放本地资源)。
    • 监控对象何时被回收(需配合 ReferenceQueue 使用)。
Reference Handler线程的作用
  • Reference Handler线程 是JVM启动时创建的一个守护线程,其核心职责是:
    1. 监控对象的回收状态:当JVM的垃圾回收器(如CMS、G1等)回收对象时,会将对应的引用(软引用、弱引用、虚引用)加入一个全局的 pending 队列。
    2. 将引用加入对应的引用队列:Reference Handler线程会从 pending 队列中取出引用,并根据其注册的 ReferenceQueue 将其加入到程序可见的队列中。
    3. 触发后续处理逻辑:程序可以通过轮询或阻塞方式从引用队列中取出引用,进而执行资源清理操作(例如关闭文件句柄、释放本地资源等)。

  • 示例
    Object obj = new Object();
    ReferenceQueue<Object> queue = new ReferenceQueue<>();
    PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
    obj = null; // 移除强引用
    // 调用 System.gc() 后,phantomRef 会被加入 queue
    

对比总结

特性强引用软引用弱引用虚引用
回收时机永远不回收(除非显式置为 null内存不足时回收下一次GC时回收对象被回收后加入引用队列
是否可获取对象✅ 通过 get() 获取✅ 通过 get() 获取✅ 通过 get() 获取❌ 无法通过 get() 获取
是否需要引用队列❌ 不需要❌ 不需要❌ 不需要✅ 必须配合 ReferenceQueue
典型用途普通对象、关键数据缓存(内存敏感)临时对象、监听回收资源清理、对象回收监控

十六.JVM调优参数可以在哪设置参数值?


1. 命令行启动参数

  • 适用场景:直接通过命令行启动Java应用(如 java -jar app.jar)。
  • 设置方法
    在启动命令中添加JVM参数,例如:
    java -Xms256m -Xmx256m -XX:+UseG1GC -jar app.jar
    
  • 参数类型
    • 标准参数(-X):如 -Xms(初始堆大小)、-Xmx(最大堆大小)。
    • 非标准参数(-XX):如 -XX:+UseG1GC(启用G1垃圾回收器)。

2. IDE配置(如 IntelliJ IDEA)

  • 适用场景:在开发环境中运行或调试Java应用。
  • 设置方法
    1. 通过运行/调试配置
      • 打开 Run/Debug Configurations(快捷键 Alt + Shift + F10 或菜单 Run > Edit Configurations)。
      • VM options 字段中输入参数,例如:
        -Xms256m -Xmx256m -XX:+PrintGCDetails
        
    2. 全局配置
      • File > Settings > Build, Execution, Deployment > Build Tools > [所选配置] 中设置全局的 VM options

3. 中间件配置(如 Tomcat)

  • 适用场景:部署在 Tomcat、WebLogic、WebSphere 等应用服务器中。
  • 设置方法
    • Tomcat
      • 编辑 setenv.sh(Linux/Mac)或 setenv.bat(Windows)文件,添加参数到 JAVA_OPTS,例如:
        JAVA_OPTS="-Xms256m -Xmx256m -XX:+UseG1GC"
        
      • 如果没有 setenv.sh,可以手动创建或修改 catalina.sh 中的 JAVA_OPTS

4. 容器环境(如 Docker/Kubernetes)

  • 适用场景:在容器化部署中(如 Docker、Kubernetes)。
  • 设置方法
    1. Docker
      • docker run 命令中通过 -e 设置环境变量 JAVA_OPTS,例如:
        docker run -e JAVA_OPTS="-Xms256m -Xmx256m" my-java-app
        
      • 或在 Dockerfile 中指定 ENV JAVA_OPTS
    2. Kubernetes
      • 在 Deployment 或 Pod 的 YAML 文件中通过环境变量设置 JAVA_OPTS,例如:
        env:- name: JAVA_OPTSvalue: "-Xms256m -Xmx256m -XX:+UseG1GC"
        

总结对比

场景设置位置典型参数示例
命令行启动启动命令-Xms256m -Xmx256m -XX:+UseG1GC
IDE(如 IntelliJ)运行/调试配置-XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
Tomcatsetenv.shsetenv.batJAVA_OPTS="-Xms256m -Xmx256m"
Docker/Kubernetes环境变量 JAVA_OPTS-Xms256m -XX:+UseContainerSupport

十七.常见的JVM调优的参数有哪些?

  • -Xms:设置JVM初始堆内存大小(如 -Xms2g 表示2GB)。

  • -Xmx:设置JVM最大堆内存大小(如 -Xmx4g 表示4GB)。

    • 推荐设置-Xms-Xmx 设置为相同值,避免堆动态扩容导致的性能抖动。
  • -XX:SurvivorRatio:设置Eden区与Survivor区的比例(如 -XX:SurvivorRatio=8 表示Eden占年轻代的80%)。

  • G1回收器(Garbage First)

    • 启用参数:-XX:+UseG1GC
    • 关键参数
      • -XX:MaxGCPauseMillis=200:目标最大停顿时间。
  • -XX:MaxTenuringThreshold=N:设置对象晋升到老年代的最大年龄(默认15)。


十八.说一下JVM的调优工具?


一、命令行工具(轻量级,适合线上快速排查)

  1. jps(Java Virtual Machine Process Status Tool)

    • 查看当前系统上运行的所有 Java 进程及其 pid
  2. jstat(JVM Statistics Monitoring Tool)

    • 监控 类加载、垃圾回收、内存、JIT 编译 等信息。

    • 示例:

      jstat -gc <pid> 1000
      

      每 1s 打印一次 GC 情况。

  3. jmap(Memory Map Tool)

    • 查看堆内存使用情况,导出堆转储(heap dump)。

    • 示例:

      jmap -heap <pid>
      jmap -dump:live,format=b,file=heap.hprof <pid>
      
  4. jhat(JVM Heap Analysis Tool)

    • 分析 jmap 生成的 heap dump 文件。
  5. jstack(Stack Trace Tool)

    • 打印指定进程的线程快照,定位 死锁、死循环、线程阻塞 问题。

    • 示例:

      jstack <pid> > threadDump.txt
      

二、图形化工具(直观,适合长期监控和分析)

  1. JConsole

    • JDK 自带,基于 JMX,实时监控内存、线程、类加载、MBean 等。
    • 缺点:性能一般,功能偏简单。
  2. VisualVM

    • 功能强大的分析工具,可以监控 CPU、内存、线程、GC,还能分析 heap dump。
    • 可安装插件(如 BTrace)增强功能。
    • 推荐作为调优首选工具。

十九.JVM内存泄漏的排查思路有哪些?


1️⃣ 导出内存快照(Heap Dump)

当怀疑内存泄漏(Heap 使用持续上涨,Full GC 频繁且效果不明显)时,可以先用 jmap 导出内存快照:

# 导出堆快照文件(hprof 格式)
jmap -dump:live,format=b,file=heap.hprof <pid>
  • live:只导出存活对象(减少无效数据)
  • file=heap.hprof:生成的快照文件
  • <pid>:Java 进程号,可以通过 jps 查看

⚠️ 注意:jmap dump 会造成 Stop The World,生产环境需要谨慎操作,最好在压力低时执行。


2️⃣ 使用 VisualVM 加载快照

打开 VisualVMFileLoad → 选择刚刚生成的 heap.hprof 文件,进入内存分析界面。

VisualVM 提供几个关键视角:

(1)Classes 视角

  • 按类展示实例数量和占用内存大小。

  • 排查思路:

    • 看哪些类的实例数量异常大(比如 HashMap$Nodebyte[]String 等)。
    • 判断是否符合业务预期(例如缓存对象是否被回收)。

(2)Instances 视角

  • 可以点进某个类,查看对象实例。

  • 排查思路:

    • 查看对象的生命周期是否合理。
    • 比如某个 Session 对象明明用户退出后应该被销毁,但还存活在内存中。

(3)References 视角(引用链分析)

  • 查看某个对象的 引用路径(Reference Chain)。

  • 排查思路:

    • 找出 GC Roots → 对象的保留链。
    • 如果对象本应释放,却因为被某个 静态集合、缓存、ThreadLocal 引用而无法回收,就说明有内存泄漏。

3️⃣ 常见内存泄漏场景(结合 VisualVM 分析)

  1. 静态集合持有对象

    • 例:static ListMap 没有清理,导致对象一直被引用。
    • 在 VisualVM 的 Reference Chain 中,可以看到对象被某个 static 字段强引用。
  2. 缓存未设置过期策略

    • 使用 HashMapConcurrentHashMap 缓存,但没清理过期数据。
    • 在 VisualVM 中看到大量缓存对象,引用路径来自缓存类。
  3. Listener / Callback 未释放

    • 注册的监听器没 remove,导致被引用。
    • 在 VisualVM 中,实例的引用路径显示来源是某个 listener 列表。
  4. ThreadLocal 泄漏

    • ThreadLocal 使用不当(没有调用 remove()),导致 value 不能被回收。
    • 在快照中可看到 ThreadLocalMap.Entry 引用了大量对象。
  5. 数据库连接 / IO 资源未关闭

    • 在快照中可能会看到大量的 SocketFileInputStream 对象。

二十.CPU飙高的排查方案和思路是什么?

假设你发现某个 Java 进程 CPU 很高,你想找出是哪个线程导致的:

✅ 步骤 1:用 top -Hp <pid> 找出高 CPU 的线程 ID(十进制)
top -Hp 12345

输出中看到某个线程 PID 是 12346,占用 98% CPU。

✅ 步骤 2:将线程 ID 转为 16 进制
printf "%x\n" 12346
# 输出:303a
✅ 步骤 3:用 jstack + grep 查找该线程的堆栈
jstack 12345 | grep -A 30 303a # 查看该线程的调用栈

注意:我们搜索的是 303a(16进制),因为 jstack 中的 nid 是 16进制格式。

✅ 输出示例:
"main" #1 prio=5 os_prio=0 tid=0x00007f8c8000a000 nid=0x303a runnable [0x00007f8c8556d000]java.lang.Thread.State: RUNNABLEat com.example.Calculator.compute(Calculator.java:45)at com.example.Service.handleRequest(Service.java:30)at com.example.ApiController.process(ApiController.java:20)at com.example.Main.main(Main.java:10)

这就定位到了:是 main 线程在执行 compute() 方法,可能是一个死循环或密集计算,导致 CPU 占用过高。


🔍 关键概念解释

名称说明
pidJava 进程的进程 ID(Process ID)
tidJava 线程对象 ID(java.lang.Thread 的 ID,jstacktid=...
nidNative Thread ID,操作系统线程 ID,16进制,jstacknid=0xabc
os_prio操作系统线程优先级
runnable / TIMED_WAITING / BLOCKED线程状态,反映当前线程在做什么


文章转载自:

http://aF2m3eRp.LLmhq.cn
http://nt6N24vD.LLmhq.cn
http://y2af0dPX.LLmhq.cn
http://7EG70hug.LLmhq.cn
http://WkV1jwOJ.LLmhq.cn
http://snWEeiFR.LLmhq.cn
http://iocduxGL.LLmhq.cn
http://z6wglcxy.LLmhq.cn
http://VfNTefB5.LLmhq.cn
http://VyLx6qvF.LLmhq.cn
http://hu6JuP2U.LLmhq.cn
http://blTxBlwp.LLmhq.cn
http://knAT1cXG.LLmhq.cn
http://4PObHwj5.LLmhq.cn
http://81zjQa19.LLmhq.cn
http://P25SGbAy.LLmhq.cn
http://ESH0fSTd.LLmhq.cn
http://TCW3Gedg.LLmhq.cn
http://17GajG27.LLmhq.cn
http://1EQAYbxj.LLmhq.cn
http://SwC7RvrN.LLmhq.cn
http://H0T996Da.LLmhq.cn
http://CwiaC5XG.LLmhq.cn
http://QNW5xZIV.LLmhq.cn
http://N3lyD2tM.LLmhq.cn
http://iZr2BQmn.LLmhq.cn
http://6DvMiKIY.LLmhq.cn
http://SwLuCOu7.LLmhq.cn
http://Q8ADSGcY.LLmhq.cn
http://8EHWcMlr.LLmhq.cn
http://www.dtcms.com/a/372518.html

相关文章:

  • Android开发-Activity附加信息
  • linux内核 - 获取单调计时时间戳的方法
  • 文献阅读 250907-Humans have depleted global terrestrial carbon stocks by a quarter
  • 装饰(Decorator)模式可以在不修改对象外观和功能的情况下添加或者删除对象功能
  • Java-Spring入门指南(三)深入剖析IoC容器与Bean核心机制
  • JavaScript 创建型设计模式详解
  • 【深度学习】CNN 卷积层
  • 洛谷 B4071 [GESP202412 五级] 武器强化
  • 0. 系统架构设计师考试大纲核心内容速览
  • [C/C++学习] 6.弹跳小球(B)
  • Easysearch 证书:Windows 上创建自签名证书的 7 种方法
  • Kafka基础理论
  • JavaScript 设计模式概览
  • Jenkins与Kubernetes集成部署流水线
  • arduino uno小车开发接线与程序记录
  • 【LeetCode 热题 100】128. 最长连续序列
  • 在object-c中方法多个参数怎么接收?
  • 蓓韵安禧DHA高含量好吸收特性深度解析
  • Pandas 合并数据集:merge 和 join
  • DINOv3 新颖角度解释
  • leetcode219.存在重复元素
  • 卷积神经网络CNN-part4-VGG
  • 【图像处理基石】图像处理中的边缘检测算法及应用场景
  • 项目中缓存雪崩,击穿,穿透的应对方法
  • AI推介-多模态视觉语言模型VLMs论文速览(arXiv方向):2025.06.10-2025.06.15
  • struct结构体内存对齐详解
  • 使用QLoRA 量化低秩适配微调大模型介绍篇
  • 变量与常量
  • 第7.10节:awk语言 exit 语句
  • 心路历程-权限的了解