Q1:Java 中有哪些垃圾回收算法?
A1:
-
标记-清除算法(Mark-Sweep)
- 工作原理:首先遍历堆中的对象,标记出所有的存活对象,接着清除未标记的对象。
- 优点:实现简单,能够处理堆中的所有对象。
- 缺点:标记和清除的过程会产生内存碎片,影响后续内存分配的效率。
-
标记-整理算法(Mark-Compact)
- 工作原理:首先标记出所有存活的对象,然后将存活的对象整理到一边,最后清除未标记的对象。
- 优点:避免了内存碎片问题。
- 缺点:整理阶段需要移动对象,会导致额外的开销。
-
复制算法(Copying)
- 工作原理:将内存分成两部分,每次只使用其中一半,垃圾回收时将存活的对象从一半复制到另一半,清除原区域的所有对象(朴素的复制算法是这样的,实际使用会分为两个survivor和一个eden区)。
- 优点:无需处理内存碎片,分配效率高。
- 缺点:需要双倍的内存空间,浪费了一半的空间。
-
分代收集算法(Generational Collection)
- 将堆分为新生代和老年代,每代使用不同的垃圾回收算法。
- 新生代一般使用复制算法,老年代一般使用标记-清除或标记-整理算法。
- 优势:利用了对象的生命周期长短不同的特点,新生代的对象生命周期短,老年代的对象生命周期长。
-
并行垃圾回收算法(Parallel Garbage Collection)
- 使用多个线程同时进行垃圾回收,提高回收效率。
- 主要包括并行标记-清除、并行复制等。
垃圾收集器
-
CMS(Concurrent Mark-Sweep)
- 在标记和清理阶段尽量与应用程序线程并发执行,减少停顿时间。
- 首先进行初始标记、并发标记、重新标记,最后是并发清理。
-
G1(Garbage First)
- 将堆划分为多个独立的区域,包括新生代、老年代和一部分混合区域。
- 根据垃圾回收的预期停顿时间优化,尽量在用户线程运行的同时进行垃圾回收。
Q2:Java 的TLAB(Thread-Local Allocation Buffer)是什么?
A2:
TLAB(Thread-Local Allocation Buffer)线程本地分配缓冲区,是JVM中为每个线程分配的一小块堆内存,用于加速对象的分配操作。 工作原理:
- 每个线程在执行过程中优先从自己的TLAB中分配内存。
- 当TLAB中的内存耗尽时,线程会重新向Eden区申请一个新的TLAB,或者直接从Eden区分配内存。
- 对象超过一定大小时(大对象),不会在TLAB中分配,而是直接在Eden区进行分配。
原理:
堆内存是共享的,新生代内存又是连续紧凑的,靠一个指针来划分,在新对象创建时只需要向右移动对应大小的指针即可,而在多线程同时分配对象时,就会出现多线程抢指针,此时需要加锁互斥,导致效率低下,而TLAB给每个线程一个独立的分配区域,避免了竞争
Q3:什么是 Java 中的直接内存(堆外内存)?
A3:
首先我们的java程序是不可以直接访问磁盘里的文件的,需要先从磁盘中读取文件拷贝到内核态,然后再从内核态拷贝到用户态才能被我们java程序读取。经历了两次拷贝。写入也是同理 而直接内存它利用内存映射让用户态和内核态"共享同一块物理内存,Java 程序读写用户态就等同于直接读写内核页,从而避免用户态与内核态之间的拷贝。这块共享的物理内存就是直接内存!

常规IO BIO:java本身不具备磁盘读写的能力,它要读写的话,必须调用操作系统提供的函数,即调用本地方法(用Native修饰的方法就是操作系统提供的方法),这个过程从java用户态切换到了内核态。在内核态中,把磁盘文件读到系统缓冲期,再从系统缓存区读到java缓冲区,才能用java代码操作,有了两次不必要的数据复制
NIO 主要是借助了直接内存,直接内存是java代码和系统都可以访问的访问的一块内存区域,因此只需要将磁盘的数据拷贝到直接内存即可





Q4:什么是 Java 中的常量池?
A4:
Java中的常量池(Costant Pool)是一块存储用于运行时的常量或符号的区域。它主要存在两个地方:
- 运行时常量池:在每个类或接口的
Class文件中存储编译时生成的常量信息,并在类加载时进入JVM方法区(Java8之后是metaspace)。 - 字符串常量池:用于存储字符串字面量,位于堆内存中的一块特殊区域。通过
String类中的intern()方法可以将字符串加入到字符串常量池。
下图来自Java内存区域详解(重点) | JavaGuide
常量池的作用:
- 常量池主要用于减少重复对象的创建,节省内存并提高效率。在Java编译过程中,一些常用的常量值如字符串、基本类型等会存储在常量池中,避免重复场景相同的常量。
扩展
intern()方法: String.intern()方法会将字符串加入到字符串常量池中,如果池中已存在该字符串,则返回池中引用。如果没有,则将当前字符串添加大池中。
java
String s1 = new String("abc"); // 创建一个新的字符串对象
String s2 = "abc"; // 创建一个在常量池中的字符串对象
// 使用intern()方法将s1字符串对象的引用添加到常量池中
String s3 = s1.intern();
// 检查引用是否相等
System.out.println(s2 == s1); // false,不同的引用
System.out.println(s2 == s3); // true,引用相同
Q5:什么是 JIT(Just-In-Time)?
A5:
JIT是一种运行时将字节码动态编译为本地机器代码的技术,主要用于提高 Java 程序的执行性能
原理:
字节码执行:当 Java 程序运行时,JVM 首先将 .class 文件中的字节码加载到内存中,并通过解释器逐行执行这些字节码。
**热点代码检测:**在执行过程中,JVM 会监控代码的执行频率。频繁被执行的代码被视为"热点代码"
JIT编译:一旦某段代码被识别为热点代码,JIT 编译器会将这段字节码编译成本地机器代码,并存储在内存中
优化:可以使用内联扩展、循环展开、消除冗余计算等提高性能
**垃圾回收:**JIT 编译的本地代码会保持在内存中,直到 Java 程序运行结束或 JVM 执行垃圾回收
Q6:你了解 Java 的逃逸分析吗?
A6:
逃逸分析是分析一个对象是否只被一个线程或者只在一个方法内部使用。当一个对象在方法中被定义之后,如果只在方法内部进行使用,则认为没有发生逃逸;如果被外面的代码使用了,就认为发生了逃逸。 用处:
- (栈上分配)对于没有发生逃逸的对象,可以分配到栈上。栈上的对象不需要进行垃圾回收,当方法结束的时候,自动被回收,减少GC工作。
- (同步消除-锁消除)如果同步块使用的锁对象,只被一个线程(单线程)访问,那么编译器会取消这块代码的同步操作。
- (标量替换)如果一个对象不会被外界访问,就会被拆解成若干个其中包含的成员变量来代替。(省去对象头)
注意

逃逸的两种类型:
1)方法逃逸:对象被方法"扔出去"了(比如当返回值或传给其他方法)。
java
// 方法逃逸:对象被返回,可能被其他方法使用
public User createUser() {
User user = new User(); // 这个user逃逸了!
return user;
}
2)线程逃逸:对象被其他线程"看到"了(比如赋值给静态变量)。
java
static User globalUser;
public void saveUser() {
User user = new User(); // 这个user逃逸了!
globalUser = user; // 其他线程都能访问globalUser
}
逃逸分析的三种优化
1)栈上分配
场景 :对象只在方法内部用,没逃逸。
优化:直接在栈帧里分配,方法结束自动销毁(不用GC回收)。
java
public void printSum() {
Point point = new Point(1, 2); // 未逃逸对象
System.out.println(point.x + point.y);
} // point随栈帧弹出销毁,不占堆内存
解释:point 原本按照 Java 语义应该在堆上分配,但由于没有逃出方法(未被 return 或其他对象引用),JVM 通过逃逸分析判断它可以安全地在栈上分配或拆成局部变量,从而避免堆分配和 GC,这就是所谓的"栈上分配/线上分配"。 2)标量替换
场景 :对象没逃逸且能拆成基本类型(如int、boolean)。
优化:不创建对象,直接用字段的值。
java
public void calc() {
Point point = new Point(1, 2); // 未逃逸对象
int sum = point.x + point.y; // JVM可能直接换成:int sum = 1 + 2;
}
3)同步消除
场景 :对象没逃逸且加了synchronized。
优化:直接去掉锁(因为只有当前线程用这个对象)。
java
public void safeMethod() {
Object lock = new Object(); // 未逃逸的锁对象
synchronized(lock) { // 锁会被JVM删除
System.out.println("Hello");
}
}
4)栈上分配、标量替换的区别
java
栈上分配:
对象存在(只不过不是存堆了,而是存栈中),基本的对象属性还是存在的
属性访问还是通过对象引用。
如果对象的字段是复杂的(数组、对象、引用),会使用栈上分配
标量替换:
对象不存在,只有字段的局部变量了
字段变为了局部变量,直接计算。
如果对象的字段是简单的(基本类型),会使用标量替换
Q7:Java 中如何判断对象是否是垃圾?不同实现方式有何区别?
A7:
在Java中,垃圾回收(Garbage Collection, GC)使用**可达性分析算法(Reachability Analysis)**来判断对象是否仍然存活,决定哪些对象可以被回收。
1. 可达性分析的基本思想
核心思路:
- 以GC Roots为起点,沿着引用关系遍历对象图。
- 如果某个对象不可达(即不存在任何路径可以从GC Roots到达该对象),则认为该对象是垃圾,可以被回收。
2. GC Roots(垃圾回收根)
可达性分析的起点,即Java中特殊的一些引用,它们默认是"存活的",不会被回收:
- 当前执行方法的局部变量(栈帧中的变量)
- Java虚拟机栈中的引用(如方法参数、局部变量、临时变量)
- 方法区中的静态变量(静态字段,如
static变量) - 方法区中的常量(如字符串常量池中的引用)
- JNI(本地方法接口)引用(即Native方法引用的对象)
3. GC过程中的可达性分析
- 从GC Roots开始,递归遍历所有可达的对象,并标记它们为存活对象。
- 未被标记的对象,即不可达对象,判定为垃圾,等待回收。
示例:
javapublic class ReachabilityAnalysisDemo { private static Object staticObj; // 静态变量,GC Root private Object instanceObj; // 非GC Root public static void main(String[] args) { ReachabilityAnalysisDemo objA = new ReachabilityAnalysisDemo(); // 可达 ReachabilityAnalysisDemo objB = new ReachabilityAnalysisDemo(); // 可达 objA.instanceObj = objB; // objA -> objB objB.instanceObj = objA; // objB -> objA (循环引用) objA = null; // 断开objA的GC Root objB = null; // 断开objB的GC Root // objA 和 objB 形成循环引用,但由于没有GC Root指向它们,仍会被GC回收 System.gc(); // 触发垃圾回收 } }分析:
staticObj是静态变量,属于GC Roots。objA和objB互相引用,形成循环引用,但如果没有GC Roots指向它们,它们仍然会被GC回收(即Java的GC能处理循环引用)。
4. 经典GC算法的可达性分析
(1) 标记-清除(Mark-Sweep)
- 第一步:标记阶段
进行可达性分析,标记所有可达对象。 - 第二步:清除阶段
清理不可达对象,释放内存(但会导致碎片化)。
(2) 复制算法(Copying)
- 把所有可达对象复制到新的内存区域,回收旧区域。
- 适用于新生代(Minor GC)。
(3) 标记-整理(Mark-Compact)
- 先标记存活对象,然后移动存活对象,整理内存,最后清理不可达对象。
- 适用于老年代(Major GC / Full GC)。
(4) 分代回收(Generational GC)
- 新生代(Young Generation)
Eden+Survivor(S0, S1)- 采用复制算法,存活对象晋升到老年代。
- 老年代(Old Generation)
- 使用标记-整理 或标记-清除。
- 永久代/元空间(Metaspace)
- 存放类元数据、方法区,不属于堆。
5. 强、软、弱、虚引用的可达性
Java 提供了四种引用类型,影响GC的可达性判断:
| 引用类型 | 可达性分析中GC行为 | 典型使用场景 |
|---|---|---|
| 强引用(StrongReference) | 永不回收 | Object obj = new Object(); |
| 软引用(SoftReference) | 只有在内存不足时才会回收 | 缓存(如 SoftReference<T>) |
| 弱引用(WeakReference) | GC时一定回收 | WeakHashMap |
| 虚引用(PhantomReference) | 仅用于跟踪对象被GC的时间点 | PhantomReference<T> |
6. 可达性分析 vs 引用计数法
| 方法 | 特点 |
|---|---|
| 引用计数法 | - 记录对象被引用次数,引用次数为0则回收。 - 无法处理循环引用 (如 A → B → A)。 |
| 可达性分析法 | - 从GC Roots遍历对象图 ,无引用路径即回收。 - 能处理循环引用,适用于Java。 |
Java为什么不用引用计数法? 因为引用计数无法处理循环引用,而可达性分析能有效解决这个问题。
7. 总结
- Java GC采用可达性分析法,判断哪些对象可以被回收。
- GC Roots 是分析的起点,主要包括栈变量、静态变量、JNI引用等。
- **垃圾回收算法(Mark-Sweep、Copying、Mark-Compact)结合分代回收机制(新生代、老年代)**提高效率。
- Java使用可达性分析而非引用计数,避免循环引用问题。
Q8:为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)?
A8:
相比于永久代,元空间具有更好的灵活性和扩展性,可以更好地满足不同应用程序的需求。
永久代的大小是固定的,当加载的类信息、常量池等数据超过了永久代的大小时,就会导致内存溢出。
而元空间的大小可以根据需要进行调整,不再受到固定大小的限制。同时,元空间的数据可以存储在本地内存中,不再受到 Java 堆大小的限制。因此,使用元空间替代永久代可以提高程序的灵活性和稳定性。
Q9:young GC、old GC、full GC 和 mixed GC 的区别是什么?
A9:
young gc/minor gc在新生代空间不足时触发,这里的新生代指的是eden区,survivor区不会出触发gc。只回收新生代,不回收老年代。因为新生代对象都是朝生夕死的,所以发生的很频繁,回收速度也很快。
old gc/major gc只在老年代空间不足的时候触发,新生代晋升到老年代的对象过多,或者老年代的存活对象达到一定的阈值导致的。只回收老年代。
full gc当老年代空间不足 或者担保失败(即新生代的to区放不下从Eden和from区拷贝过来的对象,或者新生代对象gc年龄到达阈值需要晋升,但老年代放不下);还有调用system.gc建议触发full gc,但是不一定执行。会回收老年代和新生代。
mixed gc只存在于g1垃圾收集器,当对象非常多,达到了堆内存45%的时候(jdk 8默认InitiatingHeapOccupancyPercent是45%),进行MixedGC,会回收年轻代和部分老年代。 G1调优的一个目标就是尽量不要让FGC发生。可以调低MixedGC触发的阈值,让MixedGC尽早发生。
Q10:什么情况下会触发 Java 的 Full GC?
A10:
1.老年代空间不足
2.永久代或者元空间不足
3.调用system.gc(),建议触发,但是不一定执行
4.新生代的对象晋升到老年代时,老年代放不下(达到年龄了发生晋升,或者对象太大了eden区和surivor区放不下,就会放到老年代)
5.空间分配担保失败:只要老年代的连续空间大于新生代对象的总大小或者历次晋升到老年代的对象的平均大小就进行MinorGC,否则FullGC
如何减少fgc的频率: 调整堆内存大小(-Xms和-Xmx) + 调整新生代大小 + 合理设置元空间大小 + 优化代码,减少长时间存在的大对象
