余声-个人博客


  • 首页

  • 分类

  • 归档

  • 标签

👌jvm垃圾回收算法有哪些?

发表于 2025-05-04 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌jvm垃圾回收算法有哪些?

题目详细答案

垃圾回收算法的核心在于解决两个问题:一是确定哪些对象能够被回收(引用计数法、可达性分析法),二是如何回收这些对象。

引用计数法

引用计数法(Reference Counting)是一种内存管理技术,用于跟踪对象的引用数量。每个对象都有一个引用计数器,记录着指向该对象的引用数量。

当一个对象被引用时,引用计数器加一;当一个引用被释放时,引用计数器减一。当引用计数器为零时,表示没有任何引用指向该对象,该对象可以被释放,回收其占用的内存。

可达性分析法

可达性分析算法是JVM垃圾回收中的一种算法,它通过分析对象的引用关系,判断对象是否可达,从而决定对象是否可以被回收。

标记-清除算法

垃圾收集器首先遍历对象图,标记所有可达的对象,然后清除未标记的对象。简单直接,不需要移动对象。但是会产生内存碎片,可能导致大对象分配失败。

标记-整理算法

在标记阶段标记所有可达的对象后,压缩阶段将存活的对象移动到内存的一端,整理出连续的可用内存空间。这种方式消除了内存碎片问题。但是对象移动需要额外的时间和资源。

复制算法

将内存分为两个相等的区域,每次只使用其中一个。当这个区域使用完时,将存活的对象复制到另一个区域,然后清空当前区域。这种方式简单高效,没有内存碎片问题。缺点就是需要双倍的内存空间。

分代收集算法

根据对象的生命周期将堆内存划分为几代(通常是新生代和老年代),新生代使用复制算法,老年代使用标记-整理或标记-清除算法。优化了垃圾收集性能,因为大部分对象在新生代被收集,减少了老年代的垃圾收集频率。不过需要额外的内存管理和调优。

分区算法

将堆内存划分为多个小的独立区域(Region),每个区域可以独立进行垃圾收集。这种方式提高了内存管理的灵活性和效率,适用于大堆内存的应用。缺点是实现较复杂,需要精细的内存管理。

具体垃圾收集器使用的算法

Serial GC:使用标记-整理算法。

Parallel GC:新生代使用复制算法,老年代使用标记-整理算法。

CMS GC:新生代使用复制算法,老年代使用标记-清除算法,并发标记和清除。

G1 GC:分区算法,结合标记-整理和复制算法。

ZGC:分区算法,使用染色指针和读屏障技术,实现并发标记和压缩。

Shenandoah GC:分区算法,使用并发标记和并发压缩技术。

👌Java类初始化时机

发表于 2025-05-03 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌Java类初始化时机?

题目详细答案

主动引用

创建类的实例

当使用new关键字创建类的实例时,类会被初始化。

1
MyClass obj = new MyClass();

访问类的静态变量或静态方法

当访问类的静态变量或调用静态方法时,类会被初始化。

1
2
System.out.println(MyClass.staticVar);
MyClass.staticMethod();

反射

通过反射 API 对类进行反射调用时,类会被初始化。

1
Class.forName("com.example.MyClass");

初始化子类

当初始化一个类的子类时,父类会被初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Parent {
static {
System.out.println("Parent initialized");
}
}

class Child extends Parent {
static {
System.out.println("Child initialized");
}
}

public class Main {
public static void main(String[] args) {
Child child = new Child(); // 输出:Parent initialized, Child initialized
}
}

Java 虚拟机启动时

包含main方法的类在虚拟机启动时会被初始化。例如:

1
2
3
4
5
6
7
8
9
public class Main {
static {
System.out.println("Main class initialized");
}

public static void main(String[] args) {
System.out.println("Hello, World!");
}
}

被动引用

通过子类引用父类的静态字段

通过子类引用父类的静态字段,不会导致子类初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Parent {
static int value = 42;
}

class Child extends Parent {
static {
System.out.println("Child initialized");
}
}

public class Main {
public static void main(String[] args) {
System.out.println(Child.value); // 输出:42,不会触发 Child 的初始化
}
}

定义对象数组

定义类的对象数组不会触发类的初始化。例如:

1
MyClass[] array = newMyClass[10]; // 不会触发 MyClass 的初始化

常量引用

引用常量不会触发类的初始化,因为常量在编译阶段会存入调用类的常量池中。例如:

1
2
3
4
5
6
7
8
9
class MyClass {
static final int CONSTANT = 42;
}

public class Main {
public static void main(String[] args) {
System.out.println(MyClass.CONSTANT); // 不会触发 MyClass 的初始化
}
}

👌Java类加载器的机制是什么

发表于 2025-05-03 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌Java类加载器的机制是什么?

题目详细答案

Java 的类加载机制是 JVM 负责将类文件加载到内存中,并将其转换为Class对象的过程。它包括三个主要步骤:加载(Loading)、链接(Linking)和初始化(Initialization)。以下是详细的描述:

类加载过程

加载(Loading)

加载阶段是将类文件读入内存,并创建一个Class对象的过程。具体步骤如下:

查找和导入类的二进制数据:从不同的来源(如文件系统、网络等)获取类的字节码。

创建Class对象:将字节码转换为 JVM 能够识别的Class对象。

加载阶段可以通过系统类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)等完成。

链接(Linking)

链接阶段将类的二进制数据合并到 JVM 运行时环境中。链接阶段包括三个步骤:

验证(Verification):确保类的字节码符合 JVM 规范,保证不会破坏 JVM 的安全性。

准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值。

解析(Resolution):将常量池中的符号引用转换为直接引用。

初始化(Initialization)

初始化阶段是执行类构造器方法的过程。该方法是由编译器自动收集类中的所有静态变量的赋值动作和静态代码块(static {})中的语句合并产生的。初始化阶段是类加载过程的最后一步。

类加载器(ClassLoader)

Java 的类加载器负责加载类文件。Java 中的类加载器遵循双亲委派模型(Parent Delegation Model),即类加载器在加载类时会先委托给父类加载器加载,如果父类加载器无法加载,再尝试自己加载。

双亲委派模型(Parent Delegation Model)

双亲委派模型的工作流程如下:

  1. 检查缓存:类加载器首先检查缓存中是否已经加载过该类,如果已经加载,则直接返回Class对象。
  2. 委托父类加载:如果缓存中没有,则委托父类加载器加载。
  3. 父类加载失败:如果父类加载器加载失败(抛出ClassNotFoundException),则由当前类加载器尝试加载。

这种模型的好处是避免类的重复加载,确保核心类库不会被自定义类加载器加载和覆盖。

常见的类加载器

Bootstrap ClassLoader:引导类加载器,负责加载核心类库,如rt.jar中的类。它是用原生代码实现的,不是java.lang.ClassLoader的子类。

Extension ClassLoader:扩展类加载器,负责加载JAVA_HOME/lib/ext目录中的类。

Application ClassLoader:应用程序类加载器,负责加载应用程序的类路径(classpath)中的类。

类加载器的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class MyClassLoader extends ClassLoader {

private String classPath;

public MyClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 将类名转换为文件路径
String fileName = classPath + name.replace('.', '/') + ".class";
// 读取类文件的字节码
byte[] classBytes = Files.readAllBytes(Paths.get(fileName));
// 将字节码转换为 Class 对象
return defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}

public class CustomClassLoaderDemo {
public static void main(String[] args) {
try {
// 创建自定义类加载器,指定类文件所在路径
MyClassLoader classLoader = new MyClassLoader("/path/to/classes/");
// 加载类
Class<?> clazz = classLoader.loadClass("com.example.MyClass");
// 创建类的实例
Object instance = clazz.getDeclaredConstructor().newInstance();
// 调用方法
clazz.getMethod("myMethod").invoke(instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}

👌新生代空间大小的比例及如何调整??

发表于 2025-05-03 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌jvm 新生代空间大小的比例及如何调整?

题目详细答案

在 JVM 中,堆内存通常被划分为新生代(Young Generation)和老年代(Old Generation)。新生代又进一步划分为 Eden 区和两个 Survivor 区(S0 和 S1)。调整新生代空间大小的主要目的是优化垃圾收集性能,减少应用程序的停顿时间。

新生代空间的默认比例

默认情况下,HotSpot JVM 使用的比例大致如下:

新生代(Young Generation):占整个堆内存的 1/3 到 1/4 左右。

老年代(Old Generation):占整个堆内存的 2/3 到 3/4 左右。

在新生代内部,默认的比例是:

Eden 区:占新生代的 8/10(即 80%)。

每个 Survivor 区(S0 和 S1):各占新生代的 1/10(即 10%)。

新生代空间大小的调整

调整新生代和老年代的比例

-Xms和-Xmx:设置堆内存的初始大小和最大大小。

-XX:NewSize和-XX:MaxNewSize:设置新生代的初始大小和最大大小。

1
2
-XX:NewSize=512m
-XX:MaxNewSize=512m

-XX:NewRatio:设置新生代和老年代的比例。例如,-XX:NewRatio=3 表示新生代占整个堆的 1/4,老年代占 3/4。

调整 Eden 区和 Survivor 区的比例

-XX:SurvivorRatio:设置 Eden 区和 Survivor 区的比例。例如,-XX:SurvivorRatio=8表示 Eden 区占新生代的 8/10,每个 Survivor 区占 1/10。

调整 Survivor 区的数量

-XX:SurvivorRatio:默认情况下,JVM 使用两个 Survivor 区(S0 和 S1)。你可以通过调整 Survivor 区的比例来优化内存使用和垃圾收集性能。

动态调整新生代大小

-XX:+UseAdaptiveSizePolicy:启用自适应大小策略,JVM 会根据应用程序的运行情况动态调整新生代和老年代的大小。

调整策略

在调整新生代空间大小时,需要考虑以下因素:

应用程序的对象生命周期:

如果应用程序创建了大量短生命周期对象(例如 Web 应用中的请求对象),则需要较大的新生代空间,以减少 Minor GC 的频率。

如果应用程序有较多长生命周期对象,则需要较大的老年代空间,以减少 Major GC 的频率。

GC 日志分析:

启用 GC 日志(例如-Xlog:gc*或-XX:+PrintGCDetails),分析垃圾收集的频率和停顿时间,调整新生代和老年代的大小以优化性能。

性能测试:

在调整 JVM 参数后,进行性能测试,观察 GC 行为和应用程序的响应时间,进一步调整参数以达到最佳性能。

配置 demo

假设你有一个堆内存大小为 4GB 的 JVM 实例,你希望新生代占 1GB,老年代占 3GB,并且 Eden 区占新生代的 80%,每个 Survivor 区占 10%。可以使用如下参数:

1
2
3
4
5
-Xms4g -Xmx4g
-XX:NewSize=1g
-XX:MaxNewSize=1g
-XX:NewRatio=3
-XX:SurvivorRatio=8

原文: https://www.yuque.com/jingdianjichi/xyxdsi/qd6qd8z80xv9420a

👌Java双亲委派机制的作用

发表于 2025-05-02 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌Java双亲委派机制的作用?

题目详细答案

保证 Java 核心库的安全性

通过双亲委派机制,Java 核心库(如java.lang.Object等)由启动类加载器(Bootstrap ClassLoader)加载。由于启动类加载器是在 JVM 启动时由本地代码实现的,并且它加载的类路径是固定的系统核心库路径,因此可以确保这些核心类不会被篡改或替换。这样系统的安全性和稳定性得到了保障。

避免类的重复加载

双亲委派机制确保了每个类只会被加载一次。如果一个类已经被父类加载器加载过,那么子类加载器就不会再重复加载这个类。这样可以避免类的重复加载,提高类加载的效率,并减少内存消耗。

保证类加载的一致性

通过双亲委派机制,可以确保同一个类在整个 JVM 中只有一个定义。这样可以避免类的冲突和不一致问题。例如,如果应用程序和第三方库中都定义了一个相同的类名,通过双亲委派机制可以确保最终加载的是位于更高层次的类加载器中的类,从而避免冲突。

提高类加载的效率

双亲委派机制通过将类加载请求逐级向上委派,可以利用已经加载的类,提高类加载的效率。父类加载器在加载类时,如果该类已经被加载过,那么直接返回该类的引用,从而减少了重复加载的开销。

支持动态扩展

双亲委派机制允许在不同的类加载器中加载不同的类,从而支持动态扩展。例如,应用程序类加载器(Application ClassLoader)可以加载应用程序特定的类,而扩展类加载器(Extension ClassLoader)可以加载扩展库中的类,这样可以方便地进行动态扩展和模块化开发。

👌Java虚拟机进程何时结束

发表于 2025-05-02 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌Java虚拟机进程何时结束?

题目详细答案

所有非守护线程(Non-Daemon Threads)结束

JVM 进程会在所有非守护线程结束后自动退出。非守护线程是默认的线程类型,通常用于执行主要任务。守护线程(Daemon Thread)则是辅助线程,通常用于执行后台任务,例如垃圾回收。

非守护线程:主要任务线程,JVM 会等待其执行完毕。

守护线程:辅助任务线程,JVM 不会等待其执行完毕。

当所有非守护线程都结束时,JVM 会自动退出,即使还有守护线程在运行。

调用System.exit(int status)

可以通过调用System.exit(int status)方法来显式终止 JVM 进程。status参数是一个整数,通常用于表示退出状态码。

1
2
3
4
5
6
public class ExitExample {
public static void main(String[] args) {
System.out.println("Program is exiting");
System.exit(0); // 正常退出
}
}

System.exit(0):表示正常退出。非零状态码:表示异常退出。

JVM 遇到未捕获的异常或错误

如果主线程或其他非守护线程中出现未捕获的异常或错误,且没有相应的异常处理机制,JVM 进程会终止。

1
2
3
4
5
public class UncaughtExceptionExample {
public static void main(String[] args) {
throw new RuntimeException("Uncaught exception");
}
}

通过外部命令强制终止

可以使用操作系统的命令或工具强制终止 JVM 进程,例如使用kill命令(在 Unix/Linux 系统上)或任务管理器(在 Windows 系统上)。

1
2
3
4
5
# 查找 JVM 进程 ID
ps -ef | grep java

# 强制终止 JVM 进程
kill -9 <pid>

主线程结束且没有其他非守护线程

如果主线程结束且没有其他非守护线程在运行,JVM 进程也会结束。

1
2
3
4
5
public class MainThreadExample {
public static void main(String[] args) {
System.out.println("Main thread is ending");
}
}

调用Runtime.halt(int status)

Runtime.halt(int status)方法会立即终止 JVM 进程,不执行任何关闭钩子(Shutdown Hook)或finalize方法。

1
2
3
4
5
public class HaltExample {
public static void main(String[] args) {
Runtime.getRuntime().halt(0); // 立即终止 JVM
}
}

关闭钩子(Shutdown Hook)

在 JVM 进程结束前,可以注册关闭钩子来执行一些清理操作。关闭钩子是在 JVM 关闭前执行的线程。

1
2
3
4
5
6
7
8
9
public class ShutdownHookExample {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Shutdown hook is running");
}));

System.out.println("Main thread is ending");
}
}

👌java双亲委派机制是什么

发表于 2025-05-02 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌java双亲委派机制是什么?

题目详细答案

JVM 的双亲委派机制是一种类加载机制,用于确保 Java 类加载过程的安全性和一致性。它的主要思想是:每个类加载器在加载类时,首先将请求委派给父类加载器,只有当父类加载器无法完成加载时,才由当前类加载器尝试加载类。

1725898211982-d09b6999-76ec-4ab2-9720-b89c0a8fc87f.png

双亲委派机制的工作流程

启动类加载器(Bootstrap ClassLoader):负责加载 Java 核心库(位于JAVA_HOME/lib目录下的类库,如rt.jar)。

扩展类加载器(Extension ClassLoader):负责加载 Java 扩展库(位于JAVA_HOME/lib/ext目录下的类库)。

应用程序类加载器(Application ClassLoader):负责加载应用程序类路径(classpath)上的类。

加载类的具体步骤如下:

  1. 当前类加载器收到类加载请求:当一个类加载器收到加载类的请求时,它不会立即尝试加载该类。
  2. 将请求委派给父类加载器:当前类加载器首先将加载请求委派给父类加载器。
  3. 父类加载器处理请求:

如果父类加载器存在,则父类加载器会继续将请求向上委派,直到到达启动类加载器。启动类加载器尝试加载类,如果成功,则返回类的引用。

  1. 父类加载器无法加载类:如果启动类加载器无法加载该类,加载失败返回到子类加载器。
  2. 当前类加载器尝试加载类:如果父类加载器无法加载该类,则由当前类加载器尝试加载。

通过这种机制,可以确保核心类库不会被篡改,避免了类的重复加载和类的冲突问题。

双亲委派机制的优点

安全性:通过将类加载请求逐级向上委派,可以避免核心类库被篡改或替换,确保系统安全。

避免类的重复加载:确保每个类只被加载一次,避免类的重复加载和类的冲突问题。

提高加载效率:通过委派机制,可以利用已经加载的类,提高类加载的效率。

双亲委派机制的例外

尽管双亲委派机制是 Java 类加载的标准机制,但在某些情况下,这一机制会被打破。例如:

自定义类加载器:某些自定义类加载器可能会覆盖默认的双亲委派机制,直接加载类。

OSGi 框架:OSGi 框架中,类加载机制更加复杂,可能会打破双亲委派机制。

SPI(Service Provider Interface):在某些服务提供者接口的实现中,可能需要打破双亲委派机制来加载服务实现类。

👌jvm 运行时的数据区域如何理解?

发表于 2025-05-02 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌jvm 运行时的数据区域如何理解?

题目详细答案

Java 虚拟机(JVM)在运行时将内存划分为若干不同的数据区域,每个区域都有特定的用途。

1725852039025-ee339bb3-3b55-4c11-a3a7-23d21fd77305.png

JVM 运行时数据区域

JVM 运行时数据区域主要包括以下几个部分:

方法区 (Method Area)

堆 (Heap)

Java 栈 (Java Stacks)

本地方法栈 (Native Method Stacks)

程序计数器 (Program Counter Register)

方法区 (Method Area)

方法区是所有线程共享的内存区域,用于存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

功能:

存储类的结构信息(如类的名称、访问修饰符、字段描述、方法描述等)。

存储运行时常量池,包括字面量和符号引用。

存储静态变量。

存储编译后的代码。

在 HotSpot JVM 中,方法区的一部分实现为永久代(PermGen),在 Java 8 及以后版本中被称为元空间(Metaspace)。

堆 (Heap)

堆是所有线程共享的内存区域,用于存储所有对象实例和数组。

功能:

动态分配对象内存。

垃圾收集器主要在堆上工作,回收不再使用的对象内存。

堆通常分为年轻代(Young Generation)和老年代(Old Generation),年轻代又进一步划分为 Eden 区和两个 Survivor 区(S0 和 S1)。

Java 栈 (Java Stacks)

每个线程都有自己的 Java 栈,栈帧(Stack Frame)在栈中按顺序存储。

功能:

存储局部变量表、操作数栈、动态链接、方法返回地址等信息。

每调用一个方法,就会创建一个新的栈帧,方法执行完毕后栈帧被销毁。

栈帧包括:

局部变量表:存储方法的局部变量,包括参数和方法内部的局部变量。

操作数栈:用于操作数的临时存储。

动态链接:指向常量池的方法引用。

方法返回地址:方法调用后的返回地址。

本地方法栈 (Native Method Stacks)

本地方法栈与 Java 栈类似,但它为本地(Native)方法服务。

功能:

存储本地方法调用的状态。

一些 JVM 使用 C 栈来支持本地方法调用。

程序计数器 (Program Counter Register)

每个线程都有自己的程序计数器,是一个很小的内存区域。

功能:

当前线程所执行的字节码的行号指示器。

如果当前执行的是本地方法,这个计数器值为空(Undefined)。

👌本地缓存与分布式缓存的区别

发表于 2025-05-02 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌本地缓存与分布式缓存的区别?

题目详细答案

概念

本地缓存:是指将数据缓存在应用程序所在的服务器或客户端的内存中。本地缓存的数据存储在应用程序的单个节点上。比如大家启动的应用里面用 hashmap 存储的数据,或者一些三方缓存,caffine,guava cache 这些都是本地缓存,特性在于缓存只存在这台机器。其他机器不知道。本地缓存重起之后,缓存就会失效。

分布式缓存:像 redis 这种就是分布式缓存,一个 redis 存储后,多个应用都可以来进行访问。同时自身支持集群模式,缓存数据可以分散存储在多个节点上。

区别对比

本地缓存 cacffine/guava cache redis
速度 位于应用程序所在的内存中,因此访问速度非常快 在网络上进行数据传输,可能会增加额外的网络开销,导致访问速度略低于本地缓存。
存储 存储在一个节点中,多个应用实例之间无法共享缓存数据。数据随应用进程的重启而丢失。 通过将数据分片存储在多个节点上,提高了缓存的容量和可扩展性。部分数据会被复制到多个节点上,以提高数据的可靠性和可用性
容量 容量受到内存大小的限制,一旦超过容量限制,可能会导致性能下降或者数据丢失。无法动态扩展。 根据需求动态添加和删除节点,以适应数据量的变化和访问负载的增加。redis 水平扩展。

扩展

当并发巨大的时候,如果 redis 的网络和 cpu 成为了瓶颈,一般可以增加一层本地缓存来进行缓冲。也就是我们说的多级缓存。

👌Java如何实现自己的类加载器

发表于 2025-05-01 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌Java如何实现自己的类加载器?

题目详细答案

在 Java 中,类加载器(ClassLoader)是负责将类文件加载到 JVM 中的组件。实现自定义类加载器可以让你控制类加载的过程,例如从非标准位置加载类文件、解密类文件等。

实现自定义类加载器的步骤

继承ClassLoader类:自定义类加载器需要继承java.lang.ClassLoader类。

重写findClass方法:重写findClass(String name)方法,这是自定义类加载器的核心方法,用于定义类的加载逻辑。

调用defineClass方法:在findClass方法中,通过defineClass方法将字节数组转换为Class对象。

从文件系统加载类 Demo

创建自定义类加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class MyClassLoader extends ClassLoader {

private String classPath;

public MyClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 将类名转换为文件路径
String fileName = classPath + name.replace('.', '/') + ".class";
// 读取类文件的字节码
byte[] classBytes = Files.readAllBytes(Paths.get(fileName));
// 将字节码转换为 Class 对象
return defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}

使用自定义类加载器加载类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CustomClassLoaderDemo {
public static void main(String[] args) {
try {
// 创建自定义类加载器,指定类文件所在路径
MyClassLoader classLoader = new MyClassLoader("/path/to/classes/");
// 加载类
Class<?> clazz = classLoader.loadClass("com.example.MyClass");
// 创建类的实例
Object instance = clazz.getDeclaredConstructor().newInstance();
// 调用方法
clazz.getMethod("myMethod").invoke(instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}

从网络加载类 Demo

创建自定义类加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;

public class NetworkClassLoader extends ClassLoader {

private String baseUrl;

public NetworkClassLoader(String baseUrl) {
this.baseUrl = baseUrl;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 将类名转换为 URL
String url = baseUrl + name.replace('.', '/') + ".class";
// 从网络读取类文件的字节码
InputStream inputStream = new URL(url).openStream();
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
while ((nextValue = inputStream.read()) != -1) {
byteStream.write(nextValue);
}
byte[] classBytes = byteStream.toByteArray();
// 将字节码转换为 Class 对象
return defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}

使用自定义类加载器加载类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class NetworkClassLoaderDemo {
public static void main(String[] args) {
try {
// 创建自定义类加载器,指定类文件所在的基 URL
NetworkClassLoader classLoader = new NetworkClassLoader("http://example.com/classes/");
// 加载类
Class<?> clazz = classLoader.loadClass("com.example.MyClass");
// 创建类的实例
Object instance = clazz.getDeclaredConstructor().newInstance();
// 调用方法
clazz.getMethod("myMethod").invoke(instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}

👌JVM堆的内部结构是什么

发表于 2025-04-30 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌JVM堆的内部结构是什么

题目详细答案

JVM 堆是 Java 虚拟机用于存储对象实例和数组的内存区域。堆内存是 JVM 管理的主要内存区域之一,堆内存的管理和优化对 Java 应用程序的性能至关重要。堆内存的内部结构通常分为几个不同的区域,以便更高效地进行内存分配和垃圾回收。

新生代(Young Generation)

新生代用于存储新创建的对象。大多数对象在新生代中创建,并且很快就会被垃圾回收。新生代进一步分为三个区域:

Eden 区(Eden Space):大多数新对象首先分配在 Eden 区。当 Eden 区填满时,会触发一次轻量级的垃圾回收(Minor GC)。

幸存者区(Survivor Spaces):新生代中有两个幸存者区,称为 S0(Survivor 0)和 S1(Survivor 1)。在一次 Minor GC 之后,仍然存活的对象会从 Eden 区和当前的幸存者区复制到另一个幸存者区。两个幸存者区会在每次 GC 后交替使用。

老年代(Old Generation)

老年代用于存储生命周期较长的对象。那些在新生代经历了多次垃圾回收仍然存活的对象会被移动到老年代。老年代的垃圾回收相对较少,但每次回收的时间较长,称为 Major GC 或 Full GC。

永久代(Permanent Generation)和元空间(Metaspace)

永久代(Permanent Generation):在 JDK 8 之前,永久代用于存储类的元数据、常量池、方法信息等。永久代的大小是固定的,容易导致OutOfMemoryError错误。

元空间(Metaspace):从 JDK 8 开始,永久代被元空间取代。元空间不在 JVM 堆中,而是使用本地内存。元空间的大小可以动态调整,减少了OutOfMemoryError的风险。

堆内存的垃圾回收

JVM 使用不同的垃圾回收算法来管理堆内存。

标记-清除(Mark-Sweep):标记活动对象,然后清除未标记的对象。

标记-整理(Mark-Compact):标记活动对象,然后将它们整理到堆的一端,清理掉不活动的对象。

复制算法(Copying):将活动对象从一个区域复制到另一个区域,清理掉旧区域的所有对象。新生代垃圾回收通常使用这种算法。

分代收集(Generational Collection):基于对象的生命周期,将堆分为新生代和老年代,分别进行垃圾回收。

堆内存的配置

JVM 提供了多个参数来配置堆内存的大小和行为:

-Xms:设置堆内存的初始大小。

-Xmx:设置堆内存的最大大小。

-XX:NewSize:设置新生代的初始大小。

-XX:MaxNewSize:设置新生代的最大大小。

-XX:SurvivorRatio:设置 Eden 区与幸存者区的比例。

1
2
3
4
5
6
7
-Xms512m 
-Xmx1024m
-XX:NewSize=256m
-XX:MaxNewSize=512m
-XX:SurvivorRatio=8
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m

👌JVM对象的访问模式有哪些?

发表于 2025-04-30 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌JVM对象的访问模式有哪些?

题目详细答案

在 JVM 中,对象的访问模式主要指的是 JVM 如何通过引用来访问对象的具体字段和方法。不同的 JVM 实现可能会采用不同的访问模式,但主要有以下两种常见的模式:

句柄访问模式(Handle Access Mode)

在句柄访问模式下,每个对象引用指向一个句柄池中的句柄。句柄本身包含两个指针,一个指向对象实例数据(实际对象),另一个指向对象的类型数据(如类元数据)。

句柄结构

1
引用 -> 句柄 -> [对象实例数据指针 | 类型数据指针]

访问过程

  1. 引用指向句柄:对象引用首先指向句柄池中的一个句柄。
  2. 句柄指向对象:句柄包含指向实际对象实例数据的指针和类型数据的指针。
  3. 访问对象:通过句柄中的指针访问对象实例数据和类型数据。

优点

对象移动:在垃圾回收过程中,如果对象被移动,只需更新句柄中的指针,而不需要更新所有引用。

访问灵活:通过句柄可以灵活地管理对象的访问和元数据。

缺点

间接访问:每次访问对象都需要通过句柄进行间接访问,增加了访问开销。

直接指针访问模式(Direct Pointer Access Mode)

在直接指针访问模式下,每个对象引用直接指向对象实例数据。对象实例数据中包含指向类型数据的指针(通常在对象头中)。

直接指针结构

1
引用 -> 对象实例数据 -> [对象头 | 实例字段]

访问过程

  1. 引用指向对象:对象引用直接指向对象实例数据。
  2. 对象头包含类型数据指针:对象实例数据的头部包含指向类型数据的指针。
  3. 访问对象:直接通过引用访问对象实例数据和类型数据。

优点

高效访问:直接指向对象实例数据,访问速度更快。

简单结构:对象引用和对象实例数据之间的关系更简单。

缺点

对象移动:在垃圾回收过程中,如果对象被移动,所有引用都需要更新,增加了垃圾回收的复杂性。

选择与权衡

不同的 JVM 实现会根据具体的需求和优化目标选择合适的对象访问模式。现代 JVM(如 HotSpot)通常采用直接指针访问模式,因为它在访问速度和实现复杂性之间取得了较好的平衡。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ObjectAccessExample {
public static void main(String[] args) {
MyObject obj = new MyObject(); // 创建对象
obj.setValue(42); // 设置对象字段
int value = obj.getValue(); // 访问对象字段
System.out.println("Value: " + value);
}
}

class MyObject {
private int value;

public void setValue(int value) {
this.value = value;
}

public int getValue() {
return value;
}
}

MyObject类的实例通过直接指针访问模式进行访问。JVM 会直接通过引用访问对象的实例字段value。

👌JVM的直接内存是什么?

发表于 2025-04-29 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌JVM的直接内存是什么?

题目详细答案

JVM 的直接内存是指通过java.nio包中的ByteBuffer类直接分配的内存。这种内存分配方式绕过了 JVM 的堆内存管理,直接使用底层操作系统的内存分配机制。直接内存的使用可以提高 I/O 操作的性能,因为它减少了数据在 JVM 堆内存和本地操作系统内存之间的复制开销。

直接内存的特点

非堆内存:

直接内存不属于 JVM 的堆内存区域,因此不会受到堆内存的垃圾回收机制的影响。

直接内存的分配和释放由操作系统管理,而不是由 JVM 的垃圾回收器管理。

高效的 I/O 操作:

直接内存特别适合用于频繁的 I/O 操作(如文件读写、网络通信等),因为它可以减少数据在 JVM 堆内存和操作系统内存之间的复制次数。

例如,在使用java.nio中的FileChannel进行文件读写时,通过直接缓冲区(Direct Buffer)可以显著提高性能。

手动管理:

由于直接内存不受 JVM 垃圾回收机制的管理,因此需要手动释放内存。如果不及时释放,可能会导致内存泄漏和系统性能问题。

直接内存的分配

直接内存的分配通过ByteBuffer类的allocateDirect方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.nio.ByteBuffer;

public class DirectMemoryExample {
public static void main(String[] args) {
// 分配 1 MB 的直接内存
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024);

// 使用直接缓冲区进行读写操作
directBuffer.put((byte) 1);
directBuffer.flip();
byte value = directBuffer.get();

System.out.println("Value: " + value);
}
}

直接内存的释放

直接内存的释放并不像堆内存那样由垃圾回收器自动管理。为了更好地控制直接内存的使用,可以使用以下方法:

  1. 显式释放:

使用第三方库(如 Netty)提供的工具类进行显式释放。例如,Netty 提供了PlatformDependent.freeDirectBuffer方法来释放直接缓冲区。

  1. 依赖垃圾回收:

虽然直接内存不受 JVM 垃圾回收器的直接管理,但ByteBuffer对象本身仍然受垃圾回收器管理。当ByteBuffer对象被垃圾回收时,其底层的直接内存也会被释放。但是,这种方式不够及时和可靠,可能会导致内存泄漏。

直接内存的配置

JVM 允许通过启动参数来配置直接内存的最大使用量:

-XX:MaxDirectMemorySize:用于设置直接内存的最大值。如果不设置,默认值为堆内存大小。

1
java -XX:MaxDirectMemorySize=256m DirectMemoryExample

原文: https://www.yuque.com/jingdianjichi/xyxdsi/foc8d6a9to7on1og

👌Jvm的垃圾回收是什么?

发表于 2025-04-29 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌Jvm的垃圾回收是什么?

题目详细答案

什么是垃圾回收?

所谓垃圾回收机制(Garbage Collection, 简称GC),指自动管理动态分配的内存空间的机制,自动回收不再使用的内存,不定时去堆内存中清理不可达对象,以避免内存泄漏和内存溢出的问题。最早是在1960年代提出的。

垃圾回收是 java相较于c、c++语言的优势之一。其他编程语言,如C#、Python和Ruby等,也都提供了垃圾回收机制。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,程序员唯一能做的就是通过调用System.gc 方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。

这也是垃圾收集器的最主要的缺点。

为什么需要垃圾回收?

如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。

优点:

减少了程序员的工作量,不需要手动管理内存

动态地管理内存,根据应用程序的需要进行分配和回收,提高了内存利用率

避免内存泄漏和野指针等问题,增加程序的稳定性和可靠

缺点:

垃圾回收会占用一定的系统资源,可能会影响程序的性能

垃圾回收过程中会停止程序的执行,可能会导致程序出现卡顿等问题

不一定能够完全解决内存泄漏等问题,需要在编写代码时注意内存管理和编码规范

三色标记法的优点和缺点?

发表于 2025-04-29 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌jvm 三色标记法的优点和缺点?

题目详细答案

三色标记法是一种用于垃圾回收的标记算法,通过将对象分为三种颜色(白色、灰色和黑色)来管理垃圾收集过程。它在并发垃圾收集器中尤其有用,因为它能够有效处理对象引用的变化。

优点

并发性:

减少停顿时间:三色标记法允许垃圾收集器在应用线程运行的同时进行标记,这大大减少了应用程序的停顿时间,提高了应用的响应性。

并发标记:通过颜色标记和写屏障技术,三色标记法能够在并发环境下准确标记存活对象,避免遗漏或错误标记。

精确性:

准确标记存活对象:三色标记法通过颜色状态和处理队列,确保所有存活对象都能被正确标记,从而避免存活对象被错误回收。

处理对象引用变化:在并发标记阶段,三色标记法能够处理对象引用的变化,确保引用变化不会导致存活对象被错误回收。

缺点

复杂性:

实现复杂:三色标记法的实现相对复杂,尤其是在维护颜色不变性和处理并发标记时,需要额外的机制(如写屏障)来确保正确性。

写屏障开销:维护颜色不变性需要使用写屏障技术,这会增加一定的运行时开销,可能对性能产生影响。

内存开销:

颜色状态维护:三色标记法需要为每个对象维护颜色状态,这会增加一定的内存开销。

处理队列:需要额外的内存来维护灰色对象的处理队列。

复杂的边界条件:

颜色不变性维护:在并发环境下,维护颜色不变性(如强三色不变性或弱三色不变性)可能会遇到复杂的边界条件,需要仔细处理以确保算法的正确性。

jvm 的四种引用区别?

发表于 2025-04-29 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌jvm 的四种引用区别?

题目详细答案

在 Java 中,引用类型的不同决定了垃圾收集器如何处理对象的生命周期。Java 提供了四种引用类型:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。这些引用类型在java.lang.ref包中定义,它们的区别如下:

强引用

强引用是 Java 中最常见的引用类型。通过new关键字创建的对象引用就是强引用。只要强引用存在,垃圾收集器永远不会回收被引用的对象。强引用是默认的引用类型,通常使用最广泛。

1
String str=new String("Hello, World!");

str是一个强引用,只要str不被置为null或超出作用域,对象”Hello, World!”就不会被垃圾收集器回收。

软引用

软引用是一种相对较强但仍允许垃圾收集器回收的引用类型,适用于实现内存敏感的缓存。只有在内存不足的情况下,垃圾收集器才会回收被软引用关联的对象。软引用通常用于实现缓存,当内存充足时对象不会被回收,当内存不足时对象会被回收以释放内存。

1
2
3
4
5
import java.lang.ref.SoftReference;

String str = new String("Hello, World!");
SoftReference<String> softRef = new SoftReference<>(str);
str = null; // 允许 str 对象被垃圾收集器回收

softRef是一个软引用,当内存不足时,对象”Hello, World!”可能会被回收。

弱引用

弱引用是一种比软引用更弱的引用类型,适用于实现非强制性缓存。只要垃圾收集器运行,不管内存是否充足,都会回收被弱引用关联的对象。弱引用通常用于实现规范化映射(canonical mappings),例如WeakHashMap。

1
2
3
4
5
import java.lang.ref.WeakReference;

String str = new String("Hello, World!");
WeakReference<String> weakRef = new WeakReference<>(str);
str = null; // 允许 str 对象被垃圾收集器回收

weakRef是一个弱引用,垃圾收集器在下一次运行时可能会回收对象”Hello, World!”。

虚引用

虚引用是一种最弱的引用类型,它仅用于跟踪对象被垃圾收集器回收的时间。虚引用本身不会决定对象的生命周期,垃圾收集器回收对象时会将虚引用放入引用队列中。虚引用通常用于实现一些特殊的清理机制,例如管理直接内存(Direct Memory)。

1
2
3
4
5
6
7
8
9
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

String str = new String("Hello, World!");
ReferenceQueue<String> refQueue = new ReferenceQueue<>();
PhantomReference<String> phantomRef = new PhantomReference<>(str, refQueue);
str = null; // 允许 str 对象被垃圾收集器回收

// 当垃圾收集器回收 str 对象时,phantomRef 会被放入 refQueue

phantomRef是一个虚引用,当垃圾收集器回收对象”Hello, World!”时,phantomRef会被放入refQueue中。

👌CMS收集器和G1收集器的区别

发表于 2025-04-27 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌CMS收集器和G1收集器的区别?

题目详细答案

CMS(Concurrent Mark-Sweep)和 G1(Garbage-First)是两种不同的垃圾收集器。

CMS 收集器

CMS 是一种低停顿的垃圾收集器,设计目标是减少应用程序的停顿时间。它适用于对响应时间要求高的应用程序,例如 Web 服务器。

工作原理

CMS 收集器的工作过程主要分为以下几个阶段:

  1. 初始标记(Initial Mark):标记直接可达的对象,这个阶段需要暂停所有应用线程(Stop-the-world,STW)。
  2. 并发标记(Concurrent Mark):在应用线程运行的同时,标记从初始标记阶段开始的所有可达对象。
  3. 重新标记(Remark):再次暂停所有应用线程,标记在并发标记阶段发生变化的对象。
  4. 并发清除(Concurrent Sweep):在应用线程运行的同时,清除不可达的对象。

G1 收集器

G1 是一种面向服务端应用的垃圾收集器,设计目标是提供可预测的停顿时间,同时具备较高的吞吐量。G1 收集器适用于大内存、多处理器的环境。

工作原理

G1 收集器将堆划分为多个大小相等的区域(Region),每个区域可以作为 Eden、Survivor 或 Old 区。G1 的工作过程包括:

  1. 初始标记(Initial Mark):标记从根对象直接可达的对象,需要暂停应用线程(STW)。
  2. 并发标记(Concurrent Mark):在应用线程运行的同时,标记从初始标记阶段开始的所有可达对象。
  3. 最终标记(Final Mark):再次暂停应用线程,标记在并发标记阶段发生变化的对象。
  4. 筛选回收(Live Data Counting and Cleanup):计算每个区域的存活对象数量,并根据回收收益选择要回收的区域。回收过程包括复制存活对象和清理区域。

比较总结

特性 CMS 收集器 G1 收集器
设计目标 低停顿时间 可预测的停顿时间,较高吞吐量
内存管理 标记-清除,可能产生碎片 标记-复制,减少内存碎片
并发性 并发标记和清除 并发标记,区域化回收
停顿时间 较短,但不确定 可配置的停顿时间目标
适用场景 对响应时间敏感的应用 大内存、多处理器、需要可预测停顿时间的应用

选择建议

CMS 收集器:适用于对响应时间要求高、内存较小、应用负载相对稳定的场景。

G1 收集器:适用于大内存、多处理器环境,需要可预测停顿时间的应用,特别是那些需要处理大量数据和高并发请求的服务端应用。

👌JVM创建对象的时候,如何进行并发处理?

发表于 2025-04-27 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌JVM创建对象的时候,如何进行并发处理?

题目详细答案

在 JVM 中,为了支持高效的并发处理,特别是在创建对象时,JVM 采用了多种技术和优化策略。这些技术和策略旨在确保在多线程环境下对象创建的安全性和效率。

TLAB(Thread-Local Allocation Buffers)

TLAB 是 JVM 中的一种优化技术,用于减少线程间的内存分配冲突。每个线程都会被分配一个小的、私有的堆内存空间,称为 TLAB。对象首先尝试在 TLAB 中分配内存。如果 TLAB 中有足够的空间,内存分配可以在没有锁竞争的情况下完成,从而提高性能。

TLAB 工作流程

  1. 分配 TLAB:每个线程在创建时都会被分配一个 TLAB。
  2. 对象分配:当线程需要分配对象时,首先尝试在其 TLAB 中分配内存。
  3. TLAB 用尽:如果 TLAB 中没有足够的空间,线程会请求分配新的 TLAB。如果堆内存不足以分配新的 TLAB,线程将直接在全局堆中分配内存,这时可能需要进行同步操作。

锁机制

在某些情况下,特别是当 TLAB 用尽或者需要直接在全局堆中分配内存时,JVM 需要使用锁机制来确保线程安全。常见的锁机制包括:

轻量级锁(Lightweight Locking):通过使用 CAS(Compare-And-Swap)操作来实现快速锁定和解锁,适用于竞争不激烈的场景。

偏向锁(Biased Locking):当锁倾向于被同一个线程持有时,JVM 会减少锁的开销,适用于单线程访问的情况。

重量级锁(Heavyweight Locking):当锁竞争激烈时,JVM 会使用操作系统的互斥锁(如synchronized关键字),这会导致较高的开销。

并发垃圾回收器

并发垃圾回收器(如 G1、ZGC 和 Shenandoah)在进行垃圾回收时,尽量减少对应用线程的暂停时间。它们通过并发标记、并发清理和增量压缩等技术,确保在多线程环境下高效地管理堆内存。

内存屏障(Memory Barriers)

JVM 使用内存屏障来确保内存操作的顺序性和可见性。内存屏障是一种低级同步原语,用于防止编译器和 CPU 重新排序内存操作,从而确保在多线程环境下的内存一致性。

CAS(Compare-And-Swap)

CAS 是一种无锁的同步机制,广泛用于 JVM 的并发处理。它允许线程在不使用锁的情况下进行原子操作,从而提高并发性能。CAS 操作包括以下步骤:

  1. 读取内存位置的当前值。
  2. 比较当前值和预期值。
  3. 如果当前值等于预期值,则写入新值。

如果在写入过程中发现当前值已被其他线程修改,CAS 操作会失败,线程可以选择重试或采取其他措施。

👌JVM堆和栈的区别?

发表于 2025-04-27 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌JVM堆和栈的区别?

题目详细答案

在 JVM(Java Virtual Machine)中,堆(Heap)和栈(Stack)是两种不同的内存区域,它们在内存管理和程序执行中扮演着不同的角色。

堆 栈
存储内容 用于存储所有的对象实例和数组,都在堆中分配内存。 用于存储方法的局部变量、方法调用的参数和方法的调用信息(如返回地址)。每个线程都有自己的栈,栈中的数据与线程一一对应。
内存管理方式 由垃圾收集器进行自动管理,负责分配和回收对象内存。堆内存是全局共享的,所有线程都可以访问堆中的对象。 由编译器自动管理,内存分配和释放按照方法调用的顺序进行。栈内存是线程私有的,每个线程都有自己的栈,互不干扰。
生命周期 对象在堆中的生命周期由垃圾收集器决定,只要有引用指向对象,对象就会存在。
对象的生命周期可以跨越多个方法调用,直到没有引用指向它时才会被垃圾收集器回收。 局部变量的生命周期与方法调用的生命周期一致,方法调用结束时,栈帧被销毁,局部变量也随之销毁。
栈中的数据在方法调用结束后立即释放。
内存大小 通常较大,可以通过 JVM 参数(如-Xms和-Xmx)进行配置。适合存储需要较长生命周期的大量对象。 通常较小,每个线程的栈大小可以通过 JVM 参数(如-Xss)进行配置。适合存储短生命周期的小数据。
线程安全 由于是全局共享的,堆中的对象在多线程环境下需要进行同步控制,以避免线程安全问题。 线程私有的,栈中的数据天然是线程安全的,不需要额外的同步控制。
访问速度 访问速度相对较慢,因为需要通过引用进行访问,并且涉及到垃圾收集器的管理。 访问速度相对较快,因为栈中数据直接通过栈帧进行访问,且栈的内存分配和释放效率高。
内存溢出 如果堆内存不足,会抛出OutOfMemoryError(如java.lang.OutOfMemoryError: Java heap space)。 如果栈内存不足,会抛出StackOverflowError(如java.lang.StackOverflowError)。

👌JVM的三色标记算法是什么?解决了什么问题?

发表于 2025-04-27 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌JVM的三色标记算法是什么?解决了什么问题?

口语化回答

三色标记算法通过颜色标记和处理队列,解决了并发垃圾收集过程中对象引用变化导致的标记不准确问题。它确保所有存活对象都能被正确标记,从而避免了存活对象被错误回收。同时,写屏障技术的使用进一步保证了颜色不变性,确保算法在并发环境下的正确性。

题目详细答案

三色标记算法是一种用于垃圾回收的标记算法,通过将对象分为三种颜色(白色、灰色和黑色)来管理垃圾收集过程。它主要解决了在并发垃圾收集过程中如何正确标记存活对象的问题,避免了遗漏存活对象或错误标记对象的情况。

三色标记算法的基本概念

白色:表示对象尚未被垃圾收集器访问到。如果垃圾收集过程结束时对象仍然是白色,它将被视为不可达对象,随后被回收。

灰色:表示对象已被访问到,但其引用的对象尚未全部访问。灰色对象需要进一步扫描其引用的对象。

黑色:表示对象及其引用的对象都已被访问到。黑色对象不需要再扫描。

三色标记算法的工作流程

  1. 初始化:所有对象开始时都是白色的。
  2. 标记根对象:将根对象(GC Roots)标记为灰色,并将它们放入一个待处理队列。
  3. 处理灰色对象:

从队列中取出一个灰色对象,将其标记为黑色,将该对象引用的所有白色对象标记为灰色,并将这些灰色对象加入队列。

  1. 重复步骤 3,直到队列为空。
  2. 清除白色对象:所有剩余的白色对象都是不可达的,可以被回收。

解决的问题

三色标记算法主要解决了以下问题:

  1. 并发标记的准确性:在并发垃圾收集过程中,应用线程可能会修改对象引用,导致垃圾收集器标记不准确。三色标记算法通过颜色标记和处理队列,确保所有存活对象都能被正确标记。
  2. 避免重复标记:通过将对象分为三种颜色,垃圾收集器能有效避免重复标记对象,提高标记效率。
  3. 处理对象引用变化:在并发标记阶段,应用线程可能会增加或删除对象引用。三色标记算法通过维护颜色状态和处理队列,确保引用变化不会导致存活对象被错误回收。

颜色不变性

为了确保三色标记算法在并发环境下的正确性,通常需要维护以下两种不变性之一:

  1. 强三色不变性:黑色对象不能直接引用白色对象。这意味着如果一个黑色对象引用了一个白色对象,白色对象必须先被标记为灰色。
  2. 弱三色不变性:在标记阶段,白色对象只能通过灰色对象直接或间接引用。这意味着如果一个白色对象被引用,它一定会通过一个灰色对象。

写屏障

为了维护颜色不变性,垃圾收集器通常会使用写屏障技术。在对象引用发生变化时,写屏障会执行特定的操作,确保颜色不变性不被破坏。例如,在引用赋值操作时,写屏障可能会将目标对象标记为灰色,确保其不会被错误回收。

<i class="fa fa-angle-left"></i>1234…12<i class="fa fa-angle-right"></i>

232 日志
18 分类
28 标签
GitHub
© 2025 javayun
由 Hexo 强力驱动
主题 - NexT.Gemini