余声-个人博客


  • 首页

  • 分类

  • 归档

  • 标签

👌Jvm的垃圾回收是什么?

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

👌Jvm的垃圾回收是什么?

题目详细答案

什么是垃圾回收?

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

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

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

为什么需要垃圾回收?

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

优点:

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

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

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

缺点:

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

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

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

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

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

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

题目详细答案

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

优点

并发性:

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

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

精确性:

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

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

缺点

复杂性:

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

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

内存开销:

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

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

复杂的边界条件:

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

jvm 的四种引用区别?

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

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

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

口语化回答

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

题目详细答案

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

三色标记算法的基本概念

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

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

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

三色标记算法的工作流程

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

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

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

解决的问题

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

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

颜色不变性

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

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

写屏障

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

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

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

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌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-09-14 | 分类于 笔记
字数统计 | 阅读时长

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

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

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

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

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

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

题目详细答案

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

固定大小的限制

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


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

更好的内存管理

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


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

减少配置复杂性

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


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

兼容和扩展

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

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

👌什么是java内存模型

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

👌什么是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-09-14 | 分类于 笔记
字数统计 | 阅读时长

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

题目详细答案

栈溢出(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-09-14 | 分类于 笔记
字数统计 | 阅读时长

👌什么是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. 垃圾收集:方法区的垃圾收集主要针对废弃的类元数据和常量池中的无用常量。相比堆内存的垃圾收集,方法区的垃圾收集频率较低。
<i class="fa fa-angle-left"></i>1…345…12<i class="fa fa-angle-right"></i>

239 日志
22 分类
30 标签
GitHub
© 2025 javayun
由 Hexo 强力驱动
主题 - NexT.Gemini