余声-个人博客


  • 首页

  • 分类

  • 归档

  • 标签

👌JVM的类命名空间如何理解?

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

👌JVM的类命名空间如何理解?

题目详细答案

JVM 的类命名空间是指 JVM 在运行时用来区分和管理不同类加载器加载的类的机制。

类命名空间的基本概念

在 JVM 中,每个类加载器都有自己的命名空间。一个类的完全限定名(即类的全路径名,例如com.example.MyClass)在 JVM 的命名空间中是唯一的,但同一个完全限定名的类可以由不同的类加载器加载,从而在不同的命名空间中存在多个版本。

类命名空间的工作原理

  1. 双亲委派模型:类加载器在加载一个类时,首先会将请求委派给父类加载器。如果父类加载器无法找到该类,才会由当前类加载器尝试加载。这种机制确保了核心类库的优先加载和安全性。
  2. 类的唯一性:在 JVM 中,一个类由其完全限定名和加载它的类加载器共同决定。即使两个类的完全限定名相同,但如果它们是由不同的类加载器加载的,那么它们在 JVM 中被认为是不同的类。

类命名空间示例

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
44
45
46
import java.lang.reflect.Method;

public class ClassLoaderNamespaceDemo {
public static void main(String[] args) throws Exception {
// 创建两个自定义类加载器
ClassLoader classLoader1 = new CustomClassLoader();
ClassLoader classLoader2 = new CustomClassLoader();

// 使用不同的类加载器加载同一个类
Class<?> class1 = classLoader1.loadClass("com.example.MyClass");
Class<?> class2 = classLoader2.loadClass("com.example.MyClass");

// 比较两个类对象是否相同
System.out.println(class1 == class2); // 输出 false,说明类对象在不同的命名空间中

// 实例化对象并调用方法
Object obj1 = class1.getDeclaredConstructor().newInstance();
Object obj2 = class2.getDeclaredConstructor().newInstance();

Method method1 = class1.getMethod("sayHello");
Method method2 = class2.getMethod("sayHello");

method1.invoke(obj1);
method2.invoke(obj2);
}
}

// 自定义类加载器
class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.startsWith("com.example")) {
// 自定义加载逻辑,例如从文件系统或网络加载类字节码
byte[] classData = getClassData(name);
if (classData != null) {
return defineClass(name, classData, 0, classData.length);
}
}
return super.loadClass(name);
}

private byte[] getClassData(String className) {
// 实现类加载的逻辑,例如从文件系统或网络加载类字节码
return null;
}
}

CustomClassLoader是一个自定义类加载器。我们创建了两个CustomClassLoader实例,并分别使用它们加载同一个类com.example.MyClass。由于这两个类加载器是不同的,因此它们各自的命名空间也是不同的,即使类的完全限定名相同,加载后的类对象也是不同的。

类命名空间的应用

  1. 模块化:通过使用不同的类加载器,可以实现模块化的类加载,每个模块有自己的命名空间,互不干扰。
  2. 隔离:在应用服务器(如 Tomcat)中,不同的应用程序使用不同的类加载器,从而实现类的隔离,避免类冲突。
  3. 插件系统:在插件系统中,每个插件可以使用自己的类加载器加载类,确保插件之间的独立性和隔离性。

👌JVM类何时可以卸载?

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

👌JVM类何时可以卸载?

题目详细答案

类加载器被卸载

类的卸载与类加载器的生命周期密切相关。只有当一个类加载器没有任何活动的引用时,JVM 才会考虑卸载由该加载器加载的所有类。因此,类的卸载通常发生在类加载器被卸载的时候。具体条件包括:

类加载器没有活动的引用:即没有任何线程或静态变量引用该类加载器。

类加载器加载的所有类都没有活动的引用:即这些类的实例、静态字段和方法都不再被引用。

没有对类的实例的引用

为了卸载一个类,JVM 需要确保没有对该类的实例的引用。

没有该类的对象实例在堆中。

没有对该类的静态字段的引用。

没有活动线程在执行该类的方法。

没有对类的静态方法和静态字段的引用

如果一个类的静态方法或静态字段仍然被引用,那么该类将不会被卸载。因此,JVM 必须确保:没有线程在执行该类的静态方法。没有对该类的静态字段的引用。

没有对类加载器的引用

类的卸载需要确保类加载器本身也没有被引用。这意味着:

没有其他类加载器或对象引用该类加载器。该类加载器加载的所有类都可以被卸载。

完成垃圾回收

类的卸载通常发生在垃圾回收过程中。垃圾回收器会检查类加载器及其加载的类是否符合卸载条件。如果符合条件,垃圾回收器会卸载这些类并释放相关内存。

示例代码

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
public class ClassUnloadingExample {
public static void main(String[] args) throws Exception {
// 创建一个新的类加载器
CustomClassLoader classLoader = new CustomClassLoader();

// 加载一个类
Class<?> clazz = classLoader.loadClass("MyClass");

// 创建类的实例
Object instance = clazz.newInstance();

// 清除对类加载器和类实例的引用
classLoader = null;
instance = null;

// 请求垃圾回收
System.gc();

// 让垃圾回收器有时间运行
Thread.sleep(1000);

System.out.println("Class unloading example completed.");
}

static class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if ("MyClass".equals(name)) {
byte[] classData = getClassData();
return defineClass(name, classData, 0, classData.length);
}
return super.loadClass(name);
}

private byte[] getClassData() {
// 模拟加载类数据
return new byte[]{/* class data */};
}
}
}

创建了一个自定义的类加载器CustomClassLoader,并使用它加载一个类MyClass。然后,我们清除对类加载器和类实例的引用,并请求垃圾回收。垃圾回收器在条件满足时会卸载MyClass类。

👌Java会存在内存泄露吗

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

👌Java会存在内存泄露吗?

题目详细答案

内存泄漏指的是程序在运行过程中由于某种原因未能释放不再使用的内存,导致内存使用量不断增加,最终可能耗尽可用内存。通常是由于程序逻辑错误或不当的资源管理引起的。

常见的内存泄漏情况

静态集合类(如HashMap、ArrayList)持有对象引用

静态集合类会在整个应用程序生命周期内存在,如果没有及时清理不再使用的对象引用,这些对象就无法被垃圾回收。

1
2
3
4
5
6
7
public class MemoryLeakExample {
private static List<Object> list = new ArrayList<>();

public void addToList(Object obj) {
list.add(obj);
}
}

监听器和回调函数

如果注册的监听器或回调函数没有及时解除注册,它们持有的对象引用也会导致内存泄漏。

1
2
3
4
5
6
7
public class EventSource {
private List<EventListener> listeners = new ArrayList<>();

public void registerListener(EventListener listener) {
listeners.add(listener);
}
}

未关闭的资源

打开的文件、数据库连接、网络连接等资源如果没有及时关闭,会导致内存泄漏。

1
2
3
4
public void readFile(String filePath) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
// 忘记关闭 reader
}

内部类和匿名类持有外部类引用:

内部类和匿名类会持有外部类的引用,如果这些类的实例生命周期较长,会导致外部类无法被垃圾回收。

1
2
3
4
5
6
7
8
9
10
11
12
public class OuterClass {
private String data;

public void startThread() {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(data);
}
}).start();
}
}

缓存(Cache)没有清理:

使用缓存时,如果没有适当的清理策略(如 LRU 缓存),缓存中的对象会一直存在,导致内存泄漏。

1
2
3
4
5
6
7
public class CacheExample {
private Map<String, Object> cache = new HashMap<>();

public void addToCache(String key, Object value) {
cache.put(key, value);
}
}

检测和解决内存泄漏的方法

使用内存分析工具:

工具如 VisualVM、Eclipse MAT(Memory Analyzer Tool)可以帮助分析堆内存,找出可能的内存泄漏点。

代码审查:

仔细审查代码,确保没有不必要的对象引用,及时释放资源。

使用try-with-resources语句

确保资源在使用完毕后被及时关闭。

1
2
3
4
5
public void readFile(String filePath) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
// 读取文件内容
}
}

解除监听器注册

确保在不再需要监听器时解除注册。

1
2
3
4
5
6
7
8
9
10
11
public class EventSource {
private List<EventListener> listeners = new ArrayList<>();

public void registerListener(EventListener listener) {
listeners.add(listener);
}

public void unregisterListener(EventListener listener) {
listeners.remove(listener);
}
}

👌Java创建对象的主要流程?

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

👌Java创建对象的主要流程?

题目详细答案

类加载检查

当使用new关键字创建对象时,JVM 首先检查类是否已经被加载、链接和初始化。如果类还没有被加载,JVM 会先执行类加载过程。类加载过程包括以下步骤:

加载(Loading):从文件系统或网络中读取类的二进制数据,并创建一个Class对象。

链接(Linking):包括验证(Verify)、准备(Prepare)和解析(Resolve)三个阶段。

  • 验证:确保类的字节码符合 JVM 规范,没有安全问题。
  • 准备:为类的静态变量分配内存,并将其初始化为默认值。
  • 解析:将类的符号引用转换为直接引用。

初始化(Initialization):执行类的静态初始化块和静态变量的初始化。

内存分配

一旦类被加载并初始化,JVM 会在堆中为新对象分配内存。内存分配的具体方式取决于 JVM 的实现,一般有以下几种方式:

指针碰撞(Bump-the-pointer):如果堆内存是规整的,所有已使用的内存都在一边,空闲内存都在另一边,中间有一个指针作为分界线。分配内存时,只需将指针向空闲内存方向移动一段与对象大小相等的距离。

空闲列表(Free-list):如果堆内存不规整,JVM 会维护一个空闲列表,记录哪些内存块是可用的。分配内存时,从空闲列表中找到一个足够大的内存块进行分配。

初始化零值

在内存分配完成后,JVM 会将分配的内存空间初始化为零值。这一步确保了对象的实例变量在 Java 语言层面上有默认值(如0、false或null)。

设置对象头

JVM 会在对象的内存区域中设置对象头(Object Header),对象头包含以下信息:

Mark Word:存储对象的运行时数据,如哈希码、GC 分代年龄、锁状态等。

Class Pointer:指向对象的类元数据,JVM 通过它来确定对象是哪个类的实例。

执行构造器

最后,JVM 调用对象的构造器(Constructor)进行初始化。构造器初始化包括:

执行父类构造器:如果类有父类,首先会调用父类的构造器。

初始化实例变量:按照代码中定义的顺序初始化实例变量。

执行构造器代码:执行构造器中的代码。

返回对象引用

构造器执行完毕后,JVM 返回新创建对象的引用。此时,对象已经完全初始化,可以被程序使用。

示例代码

展示对象创建的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyClass {
private int value;

// 静态初始化块
static {
System.out.println("Class MyClass is being initialized.");
}

// 实例初始化块
{
value = 10;
System.out.println("Instance initialization block.");
}

// 构造器
public MyClass() {
System.out.println("Constructor is called.");
}

public static void main(String[] args) {
MyClass obj = new MyClass();
System.out.println("Object created with value: " + obj.value);
}
}

输出

1
2
3
4
Class MyClass is being initialized.
Instance initialization block.
Constructor is called.
Object created with value: 10

👌Java创建对象的几种方式?

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

👌Java创建对象的几种方式?

题目详细答案

使用new关键字

这是最常见和直接的方式。

1
MyClass obj = new MyClass();

使用反射

通过Class类的newInstance()方法(已过时)或Constructor类的newInstance()方法。

1
2
3
4
5
6
// 使用 Class.newInstance() 方法(已过时)
MyClass obj1 = MyClass.class.newInstance();

// 使用 Constructor.newInstance() 方法
Constructor<MyClass> constructor = MyClass.class.getConstructor();
MyClass obj2 = constructor.newInstance();

使用clone()方法

通过实现Cloneable接口并重写clone()方法。

1
2
3
4
5
6
7
8
9
public class MyClass implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

MyClass obj1 = new MyClass();
MyClass obj2 = (MyClass) obj1.clone();

使用反序列化

通过ObjectInputStream进行反序列化。

1
2
3
4
5
6
7
8
9
// 序列化对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.dat"));
out.writeObject(obj);
out.close();

// 反序列化对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.dat"));
MyClass obj = (MyClass) in.readObject();
in.close();

使用工厂方法

通过工厂方法模式创建对象。

1
2
3
4
5
6
7
public class MyClassFactory {
public static MyClass createInstance() {
return new MyClass();
}
}

MyClass obj = MyClassFactory.createInstance();

使用 Builder 模式

通过构建器模式创建对象。

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
public class MyClass {
private String field1;
private int field2;

private MyClass(Builder builder) {
this.field1 = builder.field1;
this.field2 = builder.field2;
}

public static class Builder {
private String field1;
private int field2;

public Builder setField1(String field1) {
this.field1 = field1;
return this;
}

public Builder setField2(int field2) {
this.field2 = field2;
return this;
}

public MyClass build() {
return new MyClass(this);
}
}
}

MyClass obj = new MyClass.Builder().setField1("value1").setField2(42).build();

通过Unsafe类

使用sun.misc.Unsafe类(不建议在生产代码中使用,因为它依赖于内部 API,且不安全)。

1
2
3
4
5
6
7
8
9
10
11
12
import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeExample {
public static void main(String[] args) throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

MyClass obj = (MyClass) unsafe.allocateInstance(MyClass.class);
}
}

总结

new关键字:最常用,适用于大多数情况。

反射:灵活但性能较差,适用于框架或工具类开发。

clone()方法:适用于需要精确复制对象的情况。

反序列化:适用于需要从持久化存储中恢复对象的情况。

工厂方法和 Builder 模式:适用于需要复杂对象创建逻辑的情况。

Unsafe类:不建议使用,除非在非常特殊的低级别操作中。

👌Java是解释语言还是编译语言

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

👌Java是解释语言还是编译语言

题目详细答案

Java 是一种既具有编译特性又具有解释特性的语言。它独特的运行机制使得它既不同于传统的编译语言(如 C 或 C++),也不同于传统的解释语言(如 Python 或 JavaScript)。

编译阶段

源代码编译:

Java 源代码文件(.java 文件)首先通过 Java 编译器(javac)编译成字节码文件(.class 文件)。这个编译过程将高层次的 Java 代码转换成一种中间表示形式,即字节码。这些字节码是平台无关的,可以在任何支持 Java 虚拟机(JVM)的系统上运行。

1
javac MyClass.java

字节码:字节码是一种中间语言,它并不是直接可执行的机器码,而是需要进一步解释或编译成机器码才能运行。

字节码的设计使得 Java 程序可以在不同的平台上运行,而无需重新编译源代码。

解释阶段

当运行一个 Java 程序时,Java 虚拟机(JVM)会加载字节码并解释执行。JVM 内部包含一个解释器,它将字节码逐行解释成机器指令,然后执行这些指令。

1
java MyClass

Just-In-Time (JIT) 编译

为了提高性能,现代 JVM 实现通常包含一个 Just-In-Time (JIT) 编译器。JIT 编译器在程序运行时,将热点代码(即频繁执行的代码)动态编译成本地机器码,从而提高执行效率。这种动态编译使得 Java 结合了解释语言的灵活性和编译语言的高性能。

结论

Java 既是一种编译语言,也是一种解释语言。它通过先编译成字节码,然后由 JVM 解释执行,并结合 JIT 编译优化性能。这种独特的机制使得 Java 具有跨平台性和高效性。

👌一次完整的垃圾回收过程是什么样的?

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

👌Java的类加载器有哪些?

题目详细答案

在 Java 中,类加载器(ClassLoader)是负责将类文件加载到 JVM 中的组件。Java 提供了几种标准的类加载器,每种类加载器都有特定的职责和加载范围。

启动类加载器(Bootstrap ClassLoader)

职责:加载 Java 核心类库,如java.lang.*、java.util.*等。

实现:由本地代码(通常是 C++)实现,不是java.lang.ClassLoader的子类。

加载路径:$JAVA_HOME/jre/lib/rt.jar或jrt:/modules(在模块化系统中)。

特点:是所有类加载器的顶层,没有父类加载器。

扩展类加载器(Extension ClassLoader)

职责:加载扩展库中的类。

实现:由sun.misc.Launcher$ExtClassLoader实现,是java.lang.ClassLoader的子类。

加载路径:$JAVA_HOME/jre/lib/ext目录或由java.ext.dirs系统属性指定的目录。

父类加载器:引导类加载器。

应用程序类加载器(Application ClassLoader)

职责:加载应用程序类路径(classpath)中的类。

实现:由sun.misc.Launcher$AppClassLoader实现,是java.lang.ClassLoader的子类。

加载路径:由java.class.path系统属性指定的目录和 JAR 文件。

父类加载器:扩展类加载器。

自定义类加载器(Custom ClassLoader)

职责:满足特定需求的类加载器,通常在应用程序中自定义实现。

实现:继承java.lang.ClassLoader并重写findClass方法。

加载路径:由开发者自行定义,可以是文件系统、网络、数据库等。

父类加载器:可以指定,也可以继承应用程序类加载器。

以下是一个简单的自定义类加载器示例:

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);
}
}
}

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

👌java 加载 class 文件的几种方式?

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

👌java 加载 class 文件的几种方式?

题目详细答案

使用系统类加载器

Java的系统类加载器(ClassLoader.getSystemClassLoader())是默认的类加载器,可以用来加载类

1
2
3
4
5
6
7
8
public static void main(String[] args) {
try {
Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("com.example.MyClass");
System.out.println("Class loaded: " + clazz.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

使用自定义类加载器

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
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}

private byte[] loadClassData(String name) {
// 实现加载类数据的逻辑
return null; // 示例中返回null,实际应返回类的字节码数据
}

public static void main(String[] args) {
try {
CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> clazz = customClassLoader.loadClass("com.example.MyClass");
System.out.println("Class loaded: " + clazz.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

使用URLClassLoader

URLClassLoader可以从指定的URL加载类,适用于从JAR文件或远程位置加载类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.net.URL;
import java.net.URLClassLoader;

public class URLClassLoaderExample {

public static void main(String[] args) {
try {
URL[] urls = {new URL("file:///path/to/your/classes/")};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class<?> clazz = urlClassLoader.loadClass("com.example.MyClass");
System.out.println("Class loaded: " + clazz.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}

使用反射

使用反射机制的Class.forName()方法加载类:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("com.example.MyClass");
System.out.println("Class loaded: " + clazz.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

使用Thread.currentThread().getContextClassLoader()

获取当前线程的上下文类加载器来加载类:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Class<?> clazz = contextClassLoader.loadClass("com.example.MyClass");
System.out.println("Class loaded: " + clazz.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

👌jvm 的类缓存机制是什么?

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

👌jvm 的类缓存机制是什么

题目详细答案

JVM 的类缓存机制是指 JVM 在加载类时,将类的字节码和相关信息(如方法、字段、常量池等)存储在内存中的一种机制。这样做的目的是为了提高类加载的效率,避免重复加载相同的类。

类缓存机制的工作原理

  1. 类加载器缓存:每个类加载器都有一个自己的缓存,用于存储已经加载的类。当需要加载一个类时,类加载器首先会检查其缓存中是否已经有该类的Class对象。如果有,则直接返回该对象;如果没有,则加载该类并将其缓存起来。
  2. 双亲委派机制:在加载类时,类加载器会首先委派给其父类加载器进行加载。如果父类加载器无法加载该类,则由当前类加载器尝试加载。这种机制也有助于类缓存,因为父类加载器的缓存中可能已经有了该类。
  3. 常量池缓存:JVM 会将类的常量池(constant pool)中的符号引用解析为直接引用,并将这些引用缓存起来,以便后续使用时可以快速访问。

类缓存的好处

提高性能:通过缓存已经加载的类,可以避免重复加载相同的类,从而提高类加载的效率。

减少内存消耗:缓存机制可以减少类加载过程中重复分配和初始化内存的开销。

保证类的一致性:通过缓存机制,可以确保同一个类在 JVM 中只有一个Class对象,从而避免类加载冲突和不一致的问题。

类缓存的实现

在 JVM 的实现中,类缓存通常由ClassLoader类的内部数据结构实现。例如,在 Oracle 的 HotSpot JVM 中,类缓存通常是一个HashMap,其中键是类的全限定名,值是类的Class对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ClassLoaderCacheDemo {
public static void main(String[] args) throws ClassNotFoundException {
// 获取系统类加载器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();

// 加载类
Class<?> clazz1 = classLoader.loadClass("java.lang.String");
Class<?> clazz2 = classLoader.loadClass("java.lang.String");

// 比较两个类对象是否相同
System.out.println(clazz1 == clazz2); // 输出 true,说明类对象被缓存
}
}

在这个示例中,我们使用系统类加载器加载了两次java.lang.String类,并比较了两个Class对象。由于类加载器缓存的存在,两个Class对象实际上是相同的。

👌为什么将永久代替换成元空间?

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

👌为什么将永久代替换成元空间?

解决固定大小的限制:元空间可以动态扩展,减少内存不足的风险。

改进内存管理:元空间的垃圾收集更为高效,减少了内存碎片和停顿时间。

减少配置复杂性:默认动态扩展特性减少了对内存参数的调整需求。

改进类卸载机制:提高了类卸载的效率,减少内存泄漏风险。

提升兼容性和扩展性:更符合现代应用程序需求,支持未来 JVM 特性和优化。

题目详细答案

将永久代(PermGen)替换为元空间(Metaspace)是为了解决永久代在内存管理和性能方面的一些固有问题。

固定大小的限制

永久代的大小是固定的,必须在 JVM 启动时通过参数(如-XX:PermSize和-XX:MaxPermSize)进行配置。固定大小导致了内存管理的刚性,应用程序在运行过程中如果需要加载大量类(例如使用大量反射或动态生成类),可能会导致永久代空间不足,从而抛出OutOfMemoryError: PermGen space异常。


元空间使用本地内存(Native Memory),默认情况下可以根据需要动态扩展。动态扩展减少了内存不足的风险,提高了应用程序的稳定性和灵活性。

更好的内存管理

永久代的垃圾收集与堆内存的垃圾收集有所不同,通常频率较低,且在垃圾收集时可能会导致较长的停顿时间。由于永久代的固定大小,垃圾收集器在回收永久代时需要考虑更多的复杂性,特别是在内存紧张的情况下。


元空间的设计使得其垃圾收集更为高效,减少了内存碎片和停顿时间。元空间使用本地内存,减少了 JVM 堆内存的压力,使得堆内存的管理更加简单和高效。

减少配置复杂性

永久代需要在部署应用程序时仔细调整永久代的大小参数,以避免内存溢出,这增加了配置的复杂性和维护成本。


元空间的默认动态扩展特性减少了开发者对内存参数的调整需求。即使需要限制元空间的大小,也可以通过-XX:MaxMetaspaceSize参数进行简单配置。

兼容和扩展

永久代固定大小的永久代在面对不断变化的应用程序需求和新特性时,显得不够灵活。

元空间的设计更符合现代应用程序的需求,特别是在云计算和大规模分布式系统中。动态扩展和使用本地内存的特性使得元空间更具扩展性和兼容性,能够更好地支持未来的 JVM 特性和优化。

👌什么是java内存模型

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

👌什么是java内存模型?

题目详细答案

Java 内存模型(Java Memory Model, JMM)是 Java 虚拟机规范的一部分,定义了多线程环境下共享变量的访问规则以及不同线程之间如何通过内存进行交互。JMM 主要解决在多线程编程中可能出现的可见性、原子性和有序性问题。

关键概念

线程与主内存:

每个线程都有自己的工作内存(也称为本地内存),工作内存保存了该线程使用到的变量的副本。主内存是共享内存区域,所有线程都可以访问主内存中的变量。

可见性:

可见性问题是指一个线程对共享变量的修改,另一个线程是否能够立即看到。JMM 通过volatile关键字、锁机制(如synchronized)等来保证变量的可见性。

原子性:

原子性问题是指一个操作是否是不可分割的,即操作要么全部执行完成,要么完全不执行。JMM 保证了基本数据类型的读写操作的原子性,但对于复合操作(如 i++)则不保证。

有序性:

有序性问题是指代码执行的顺序是否与程序的顺序一致。编译器和处理器可能会对指令进行重排序,以提高性能。JMM 通过volatile关键字、锁机制等来保证必要的有序性。

内存模型中的同步机制

volatile关键字

volatile变量保证了对该变量的读写操作的可见性和有序性。

读volatile变量时,总是从主内存中读取最新的值。

写volatile变量时,总是将最新的值写回主内存。

synchronized关键字:

synchronized块或方法保证了进入临界区的线程对共享变量的独占访问。

退出synchronized块时,会将工作内存中的变量更新到主内存。

进入synchronized块时,会从主内存中读取最新的变量值。

final关键字:

final变量在构造器中初始化后,其他线程可以立即看到初始化后的值。

final变量的引用不会被修改,因此可以确保其可见性。

可见性问题示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class VisibilityExample {
private static boolean stop = false;

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (!stop) {
// busy-wait
}
});
thread.start();

Thread.sleep(1000);
stop = true; // 另一个线程可能不会立即看到这个修改
}
}

主线程修改了stop变量,但另一个线程可能不会立即看到修改,导致循环无法终止。可以使用volatile关键字解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class VisibilityExample {
private static volatile boolean stop = false;

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (!stop) {
// busy-wait
}
});
thread.start();

Thread.sleep(1000);
stop = true; // 另一个线程会立即看到这个修改
}
}

👌什么情况会发生栈溢出

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

👌什么情况会发生栈溢出?

题目详细答案

栈溢出(Stack Overflow)是指程序在运行过程中,由于调用栈(stack)空间被耗尽而导致的错误。调用栈是用来存储方法调用信息(如局部变量、方法参数和返回地址等)的内存区域。

递归调用过深

最常见的栈溢出情况是递归调用过深。递归函数在每次调用时都会在栈上分配新的栈帧,如果递归深度过大,栈空间很快就会耗尽。recursiveMethod方法无限递归调用自己,导致栈溢出。

1
2
3
4
5
6
7
8
9
public class StackOverflowExample {
public static void recursiveMethod() {
recursiveMethod(); // 无限递归调用
}

public static void main(String[] args) {
recursiveMethod();
}
}

无终止条件的递归

递归函数如果没有正确的终止条件,也会导致栈溢出。recursiveMethod方法的递归调用没有正确的终止条件,导致栈溢出。

1
2
3
4
5
6
7
8
9
10
11
12
public class StackOverflowExample {
public static void recursiveMethod(int num) {
if (num == 0) {
return;
}
recursiveMethod(num); // 无终止条件的递归
}

public static void main(String[] args) {
recursiveMethod(5);
}
}

遍历深度过大的数据结构

遍历深度过大的数据结构(如深度优先搜索一个非常深的树或图)也可能导致栈溢出。

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
class TreeNode {
int value;
TreeNode left;
TreeNode right;

TreeNode(int value) {
this.value = value;
}
}

public class StackOverflowExample {
public static void traverse(TreeNode node) {
if (node == null) {
return;
}
traverse(node.left);
traverse(node.right);
}

public static void main(String[] args) {
TreeNode root = new TreeNode(1);
TreeNode current = root;
for (int i = 2; i < 100000; i++) {
current.left = new TreeNode(i);
current = current.left;
}
traverse(root);
}
}

栈空间设置过小

程序运行时,栈空间的大小是有限的。如果栈空间设置过小,也会更容易发生栈溢出。

1
java -Xss128k

通过-Xss参数设置 JVM 栈空间大小为 128KB,可能导致栈溢出。

防止栈溢出的方法

优化递归:

确保递归函数有正确的终止条件。

使用尾递归优化(如果编译器或运行时支持)。

将递归转换为迭代。

增加栈空间:

通过 JVM 参数-Xss增加栈空间大小。

使用非递归算法:

对于深度优先搜索等场景,使用显式栈(如Stack类)代替递归调用。

检查数据结构:

确保遍历的数据结构不会过深或过大。

👌什么是JVM方法区?

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

👌什么是JVM方法区?

题目详细答案

JVM 方法区是 JVM 运行时数据区的一部分,用于存储与类和方法相关的元数据。它是所有线程共享的内存区域,包含了 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等。方法区的内容在 JVM 启动时创建,并在 JVM 运行期间动态扩展或收缩。

方法区的主要内容

1
2
3
4
5
6
7
8
+-----------------------------+
| 方法区 (Method Area) |
| - 类信息 |
| - 运行时常量池 |
| - 静态变量 |
| - 即时编译器编译后的代码 |
| - 字段和方法信息 |
+-----------------------------+
  1. 类信息:包括类名、父类名、访问修饰符、接口列表等的元数据。
  2. 运行时常量池:存储编译期生成的各种字面量和符号引用,这些引用在类加载后被解析为直接引用。
  3. 静态变量:类的静态字段,存储类级别的变量。
  4. 即时编译器编译后的代码:即时编译器(JIT)将热点代码编译为本地机器码,存储在方法区中。
  5. 字段和方法信息:包括字段描述、方法描述、方法字节码、方法的访问修饰符等。

方法区在不同 JVM 实现中的差异

HotSpot JVM(Java 7 及之前):方法区实现为永久代(Permanent Generation,PermGen)。永久代的内存空间固定,容易导致内存溢出(OutOfMemoryError)。

HotSpot JVM(Java 8 及之后):方法区实现为元空间(Metaspace)。元空间使用本地内存(Native Memory),默认情况下可以根据需要动态扩展,减少了内存溢出的风险。

方法区的内存管理

方法区的内存管理主要包括以下几个方面:

  1. 类加载:当一个类被加载时,其相关信息会被存储在方法区中。
  2. 类卸载:当一个类不再被使用且没有任何引用时,垃圾收集器可以回收方法区中的类元数据。
  3. 垃圾收集:方法区的垃圾收集主要针对废弃的类元数据和常量池中的无用常量。相比堆内存的垃圾收集,方法区的垃圾收集频率较低。

👌软引用和虚引用适用场景

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

👌软引用和虚引用适用场景

题目详细答案

软引用

软引用主要用于实现内存敏感的缓存。

软引用可以用于缓存那些可以在内存不足时安全回收的对象。例如,图片缓存、数据缓存等场景。在内存充足时,缓存的对象不会被回收;当内存不足时,缓存的对象会被回收以释放内存。这种机制可以在不影响应用程序功能的前提下,最大限度地利用可用内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;

public class SoftReferenceCache<K, V> {
private final Map<K, SoftReference<V>> cache = new HashMap<>();

public void put(K key, V value) {
cache.put(key, new SoftReference<>(value));
}

public V get(K key) {
SoftReference<V> ref = cache.get(key);
if (ref != null) {
return ref.get();
}
return null;
}
}

SoftReferenceCache使用软引用来缓存对象,当内存不足时,缓存的对象可能会被回收。

虚引用

虚引用主要用于跟踪对象被垃圾收集器回收的时间,通常用于实现特殊的清理机制。

虚引用可以用于管理那些需要在对象被回收后进行清理的资源,例如直接内存(Direct Memory)、文件句柄等。

当对象被垃圾收集器回收时,虚引用会被放入引用队列(ReferenceQueue),通过处理这个队列,可以执行必要的清理操作。

虚引用可以用于监控对象何时被回收,从而在对象回收时执行一些特定的操作,例如记录日志、更新状态等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceExample {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, refQueue);

obj = null; // 允许 obj 对象被垃圾收集器回收

// 强制垃圾收集
System.gc();

// 检查引用队列
if (refQueue.poll() != null) {
System.out.println("Object has been collected");
// 执行清理操作
}
}
}

当obj被垃圾收集器回收时,phantomRef会被放入refQueue中,可以通过检查refQueue来执行清理操作。

👌Gc root对象都包含哪些?

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

👌Gc root对象都包含哪些?

题目详细答案

什么是GC Root

GC Root是垃圾回收器确定对象是否可达的起始点。在Java中,GC Root是一组特殊的对象,GC Root对象保证了这些对象及其引用链不会被垃圾回收器回收,因为它们是程序的起始点,其他对象通过它们间接可达,它确保了内存中的对象能够正确地被管理和清理,避免内存泄漏和无效引用的问题。

gcroot 对象

虚拟机栈(栈帧中的本地变量表)中的引用:

每个线程都有一个虚拟机栈,栈帧中的本地变量表(Local Variable Table)包含了方法执行过程中用到的所有局部变量。这些局部变量可能包含对对象的引用。

方法区中的类静态变量引用:

方法区中存储了类的元数据,包括类的静态变量。这些静态变量可能引用对象。

方法区中的常量引用:

方法区还包含运行时常量池(Runtime Constant Pool),其中可能有对对象的引用。

本地方法栈中的 JNI(Java Native Interface)引用:

本地方法栈(Native Method Stack)用于本地方法的调用。本地方法可以通过 JNI 引用 Java 对象,这些引用也是 GC Roots。

活动线程:

所有正在运行的线程本身也是 GC Roots。

类加载器:

类加载器本身也是 GC Roots,因为它们负责加载类,而类加载器的引用链可以追溯到所有被加载的类及其静态变量。

系统类:

一些系统级的类,比如java.lang.Thread,java.lang.System等,也被视为 GC Roots。

JNI 全局引用:

JNI 中的全局引用(Global References)也是 GC Roots。

JVM 内部的某些数据结构:

JVM 内部的一些数据结构(如 JIT 编译器生成的代码中的引用)也可能被视为 GC Roots。

👌一次完整的垃圾回收过程是什么样的?

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

👌一次完整的垃圾回收过程是什么样的?

题目详细答案

Jvm 垃圾回收的基本过程可以分为以下三个步骤:

1719147802226-96af4d13-19d6-4cf6-bdc9-c3c028ccd29e.png

垃圾分类

首先我们的 jvm 在进行垃圾回收的过程,需要确定哪些对象是垃圾对象,哪些对象是存活对象。这个类似于我们在做一件事之前的规划。具体的分类方法一般情况下,垃圾回收器会从堆的根节点(如程序计数器、虚拟机栈、本地方法栈和方法区中的类静态属性等),也就是 gc root。开始遍历对象图,标记所有可以到达的对象为存活对象,未被标记的对象则被认为是垃圾对象。进过标记后,分类成功。

垃圾查找

分类后,已经知道了对象所处的一个状态,jvm 会根据分类后对象,先找出所有垃圾对象,以便进行清理。

不同的垃圾收集,其中的查找方式会产生相应的差异。随着现在 jdk 的 升级与发展,还会产生更加高效的算法,后面会有垃圾收集的算法详细介绍。

垃圾清理

标记完成后,进行最后的清理与删除。这里涉及不同的垃圾收集器,清理的方式也不同,常见的有

标记-清除算法,复制算法,标记-整理算法,分代算法。

需要注意的是,垃圾清理可能会引起应用程序的暂停,不同的垃圾回收器通过不同的方式来减少这种暂停时间,从而提高应用程序的性能和可靠性。

常见的垃圾收集器有

Serial GC

Parallel GC

CMS GC(Concurrent Mark Sweep)

G1 GC(Garbage First)

ZGC

👌如何优化减少 FULL GC?

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

👌如何优化减少 FULL GC?

题目详细答案

调整堆内存大小

增加堆内存大小:适当增加堆内存大小,可以减少老年代空间不足的情况,从而减少 Full GC 的发生。可以通过-Xmx和-Xms参数调整最大和最小堆内存大小。

调整新生代大小:适当增加新生代(Young Generation)的大小,可以减少对象晋升到老年代的频率,从而减少老年代的压力。可以通过-XX:NewSize和-XX:MaxNewSize参数调整新生代大小。

调整垃圾收集器参数

根据应用程序的具体需求,调整垃圾收集器的参数,可以优化垃圾收集行为,比如

G1 GC 参数:

-XX:MaxGCPauseMillis=:设置目标最大 GC 暂停时间,G1 GC 会尝试在这个目标时间内完成 GC。

-XX:InitiatingHeapOccupancyPercent=:设置触发混合回收的老年代占用比例。

CMS 参数:

-XX:CMSInitiatingOccupancyFraction=:设置触发 CMS GC 的老年代占用比例。

-XX:+UseCMSInitiatingOccupancyOnly:仅在老年代占用达到设定比例时触发 CMS GC。

优化对象分配和生命周期

减少对象分配和优化对象生命周期,可以减轻垃圾收集器的负担,从而减少 Full GC 的发生:

减少短生命周期对象:尽量减少短生命周期对象的创建,或将其分配在栈上而不是堆上。

缓存和重用对象:使用对象池(Object Pool)缓存和重用对象,减少对象分配和垃圾回收的频率。

避免显式调用System.gc()

显式调用System.gc()会请求 JVM 进行 Full GC,尽量避免在代码中使用System.gc(),除非有充分的理由和必要性。

调整元空间(Metaspace)大小

适当增加元空间大小,可以减少因元空间不足而触发的 Full GC

-XX:MetaspaceSize=:设置初始元空间大小。

-XX:MaxMetaspaceSize=:设置最大元空间大小。

👌如何破坏双亲委派模型

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

👌如何破坏双亲委派模型

题目详细答案

自定义类加载器

通过创建自定义类加载器并覆盖loadClass方法,可以实现不同于双亲委派机制的类加载策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 如果是特定的类,不使用双亲委派机制
if (name.startsWith("com.example")) {
// 自定义加载逻辑
byte[] classData = getClassData(name);
if (classData != null) {
return defineClass(name, classData, 0, classData.length);
}
}
// 否则,使用默认的双亲委派机制
return super.loadClass(name);
}

private byte[] getClassData(String className) {
// 实现类加载的逻辑,例如从文件系统或网络加载类字节码
return null;
}
}

通过反射机制

利用反射机制直接操作类加载器的父类加载器,绕过双亲委派机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.reflect.Field;

public class BreakParentDelegation {
public static void main(String[] args) throws Exception {
CustomClassLoader customClassLoader = new CustomClassLoader();
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();

// 获取系统类加载器的父类加载器(扩展类加载器)
Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(appClassLoader, customClassLoader);

// 现在系统类加载器的父类加载器被替换为自定义类加载器
Class<?> clazz = Class.forName("com.example.MyClass", true, appClassLoader);
System.out.println(clazz.getClassLoader());
}
}

OSGi 框架

OSGi 框架提供了一种模块化的类加载机制,允许每个模块(Bundle)有自己的类加载器,从而可以打破双亲委派机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// OSGi 中的 BundleActivator 示例
public class MyBundleActivator implements BundleActivator {
@Override
public void start(BundleContext context) throws Exception {
// 在 OSGi 环境中,每个 Bundle 有自己的类加载器
ClassLoader bundleClassLoader = getClass().getClassLoader();
Class<?> clazz = bundleClassLoader.loadClass("com.example.MyClass");
System.out.println(clazz.getClassLoader());
}

@Override
public void stop(BundleContext context) throws Exception {
// 停止 Bundle 时的清理工作
}
}

使用 SPI(Service Provider Interface)

某些服务提供者接口的实现中,可能需要打破双亲委派机制来加载服务实现类。

1
2
3
4
5
6
7
8
9
10
import java.util.ServiceLoader;

public class SPIDemo {
public static void main(String[] args) {
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (MyService service : loader) {
service.execute();
}
}
}

在META-INF/services目录下创建一个文件,文件名为接口的全限定名,文件内容为实现类的全限定名。通过这种方式,JVM 会使用Thread.contextClassLoader来加载服务实现类,从而可以打破双亲委派机制。

👌如何判断对象是否可以被回收?

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

👌如何判断对象是否可以被回收?

口语化回答

引用计数法和可达性分析法是两种不同的内存管理和垃圾回收算法。引用计数法通过维护引用计数器来跟踪对象的引用数量,具有实时性好、简单高效等优点,但存在循环引用等问题;而可达性分析法则通过分析对象的引用关系来判断对象是否可达,从而决定对象是否可以被回收,具有准确性高、效率好等优点,是JVM中常用的垃圾回收算法之一。

题目详细答案

引用计数法

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

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

优点:

实时性好:当没有引用指向一个对象时,该对象可以立即被回收,释放内存资源。

简单高效:引用计数法是一种相对简单的内存管理技术,实现起来较为高效。

无需沿指针查找:与GC标记-清除算法不同,引用计数法无需从根节点开始沿指针查找。

缺点

循环引用问题:当存在循环引用的情况下,对象之间的引用计数可能永远不会为零,导致内存泄漏的发生。

额外开销:每个对象都需要维护一个引用计数器,这会带来一定的额外开销。

不支持并发:在多线程环境下,引用计数法需要进行额外的同步操作,以确保引用计数的准确性,可能导致性能损失。

可达性分析法

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

工作原理

  1. GC Roots:在Java中,GC Roots通常包括虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区(静态变量)中引用的对象、本地方法栈中JNI(Native方法)引用的对象等。
  2. 搜索过程:可达性分析算法从GC Roots开始,递归地访问所有可达的对象,并给它们打上标记。这个过程可以使用深度优先搜索(DFS)或广度优先搜索(BFS)等图遍历算法来实现。
  3. 回收判定:如果一个对象到GC Roots没有任何引用链相连(即该对象从GC Roots不可达),则证明该对象是不可用的,可以判定为可回收对象。

特点

准确性:通过从GC Roots开始搜索引用链,可以准确地判断哪些对象是可回收的。

效率:结合现代JVM的优化技术,如增量标记、并发标记等,可以提高可达性分析算法的效率。

灵活性:可达性分析算法可以与不同的垃圾回收策略(如标记-清除、标记-整理等)结合使用,以适应不同的应用场景和硬件环境。

👌方法区和永久代以及元空间有什么区别

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

👌方法区和永久代以及元空间有什么区别

题目详细答案

方法区(Method Area)

方法区是 JVM 运行时数据区的一部分,用于存储类元数据、常量、静态变量、即时编译器编译后的代码等信息,是 JVM 规范的一部分,但规范并未规定其具体实现方式,是所有线程共享的内存区域。

永久代(Permanent Generation, PermGen)

永久代是 HotSpot JVM 在 Java 7 及之前版本中对方法区的一种具体实现。

永久代的内存空间是固定的,默认情况下不能动态扩展,容易导致内存溢出(OutOfMemoryError)。

主要存储类元数据、运行时常量池、静态变量、即时编译器编译后的代码等。

由于固定大小,容易出现内存不足的情况,尤其是在大量动态生成类或使用大量反射的应用中。

元空间(Metaspace)

元空间是 HotSpot JVM 在 Java 8 及之后版本中对方法区的一种新的实现方式,替代了永久代。

元空间使用本地内存(Native Memory)而不是 JVM 堆内存。默认情况下,元空间可以根据需要动态扩展,减少了内存溢出的风险。可以通过 JVM 参数(如-XX:MaxMetaspaceSize)来控制元空间的最大大小。

与永久代类似,元空间也存储类元数据、运行时常量池、静态变量、即时编译器编译后的代码等。由于使用本地内存并且可以动态扩展,元空间更灵活,减少了内存溢出的风险。

对比总结

特性 方法区 (Method Area) 永久代 (PermGen) 元空间 (Metaspace)
定义 JVM 规范的一部分 方法区的实现之一 方法区的实现之一
存储内容 类元数据、常量、静态变量、即时编译器编译后的代码 类元数据、常量、静态变量、即时编译器编译后的代码 类元数据、常量、静态变量、即时编译器编译后的代码
内存类型 JVM 内存的一部分 JVM 堆内存的一部分 本地内存
内存大小 规范未定义 固定大小 动态扩展
垃圾收集 规范未定义 有,但频率较低 有,但频率较低
适用 JVM 版本 所有版本 Java 7 及之前 Java 8 及之后
内存管理 规范未定义 固定大小,容易溢出 动态扩展,减少溢出风险
<i class="fa fa-angle-left"></i>1…345…12<i class="fa fa-angle-right"></i>

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