垃圾回收与内存分配
垃圾回收与内存分配
19. Java 对象的定位方式
回答:
Java 中对象的访问方式有两种主流方案:句柄访问和直接指针访问。HotSpot 虚拟机主要采用直接指针方式。
分析:
Java 中对象实例保存在堆内存中,程序通过栈上的引用(reference)来操作对象。对象的访问方式会影响对象在内存中的组织结构和访问性能。使用句柄方式时,JVM 在堆中划分出一块句柄池区域,reference 保存的是句柄地址,句柄中包含对象实例数据和类型元数据的具体地址。这种方式的优点在于引用的稳定性,即使对象在 GC 中被移动,引用地址不变,只需更新句柄内容。
另一种更常见的方式是直接指针访问,reference 直接保存对象在堆中的物理地址,对象中再包含指向类元数据的地址。该方式访问速度更快,节省了一次指针定位的开销。HotSpot 虚拟机正是基于此方式优化设计的,以提升运行效率。因此,虽然句柄提供更高的灵活性,但出于性能考虑,现代虚拟机普遍选择直接指针方案。
20. 如何判断一个对象是否存活?
回答:
主要有两种判断对象是否"死掉"的方法:引用计数法和可达性分析算法。现代 JVM 大都采用可达性分析。
分析:
在执行 GC 前,JVM 需要确定哪些对象已不再被使用。最早期的方式是引用计数法:每个对象维护一个引用计数器,引用增加时计数加一,引用失效时减一,计数为 0 的对象被判定为可回收。然而这种方法难以处理循环引用场景,例如两个对象互相引用,即使外部无引用,其计数也不为 0,造成内存泄漏。
为了解决此问题,现代 JVM 使用可达性分析算法(Reachability Analysis)。GC 会从一组被称为"GC Roots"的对象作为起点,通过一条条引用链向下搜索。只要一个对象能通过 GC Roots 直接或间接可达,它就是"活着"的,否则就可回收。GC Roots 包括常驻栈帧中的对象、本地方法栈引用、类静态属性引用等。可达性分析是当前主流 GC 判断策略,是分代回收等机制的基础。
21. GC 是什么?为什么要 GC?
回答:
GC(Garbage Collection)是 Java 虚拟机提供的自动垃圾回收机制,用于自动释放内存中不再使用的对象,提升开发效率和系统稳定性。
分析:
Java 程序在运行中会频繁创建大量对象,但这些对象并不总是永久存活。如果不手动清理,堆空间很快就会被耗尽,导致 OOM 错误。为了简化开发并提升安全性,Java 引入 GC 机制,由虚拟机在后台自动检测哪些对象已不可达,并释放它们所占用的内存空间。
GC 的优势显而易见:它屏蔽了底层内存管理的复杂细节,避免因忘记手动释放内存而导致内存泄漏、悬挂指针等问题,从而提高了程序的健壮性与安全性。虽然 GC 无法完全避免内存泄漏(如未释放的引用链),但它极大降低了出错概率,是 Java 相对于 C/C++ 的一大优势。此外,现代 JVM 还通过分代回收、区域划分、垃圾收集器优化等手段,使 GC 不仅自动而且高效。
22. 可作为 GC Roots 的对象有哪些?
回答:
GC Roots 是可作为垃圾回收起点的一组特殊对象。它们包括栈帧中的局部变量、方法区中的静态变量、常量引用、本地方法引用、JVM 内部的关键对象等。
分析:
在 Java 的可达性分析中,垃圾回收从 GC Roots 出发,沿着引用链搜索内存中可达的对象,判断哪些对象仍存活。GC Roots 通常包括以下几类:
(1)栈帧中的引用,包括方法参数、局部变量等,这是线程执行方法时所使用的直接引用;
(2)方法区中静态字段引用的对象,比如类的静态变量;
(3)方法区中的常量池中引用的对象,如字符串常量;
(4)本地方法栈中 JNI 引用的对象,即 native 方法中使用的引用;
(5)一些 JVM 内部的特殊引用,例如类加载器、异常对象、系统类加载器及常驻线程引用的对象;
(6)被 synchronized 锁住的对象;
(7)JMX Bean、JVMTI 注册的回调等反映 JVM 内部状态的引用对象。
这些对象构成了垃圾回收的"根",从它们出发遍历引用链,才能精准判断哪些对象可以回收。
23. 什么情况下类会被卸载?
回答:
类的卸载需要满足三个条件:该类的所有实例都已被回收;加载该类的类加载器已被回收;Class 对象本身没有被任何地方引用。
分析:
类的卸载过程由 JVM 控制,是一种谨慎而复杂的机制。类的生命周期由其类加载器控制,只有当一个类加载器不可达,才能进一步判断是否有机会卸载其加载的类。JVM 在进行类卸载时需要同时满足以下三点:
(1)类的所有实例都已被 GC 回收,内存中不存在该类的对象;
(2)类加载器自身也被回收,意味着该加载器已经不再被任何强引用指向;
(3)代表该类的 java.lang.Class 对象本身不再被使用,也就是没有被任何地方通过反射或者静态字段引用。
即使满足这三点,类是否真的被卸载还取决于 JVM 的具体实现,通常只有在动态模块加载(如 OSGi)、热部署、容器隔离等场景中才会显式触发类卸载。
24. 强引用、软引用、弱引用、虚引用有什么区别?
回答:
它们代表了对象的不同可达状态,决定了对象在 GC 时的行为。强引用最稳定,虚引用最弱。
分析:
Java 提供了四种引用类型,用于控制对象在 GC 中的行为:
强引用(Strong Reference):最常见的引用形式,例如 Object obj = new Object();。只要存在强引用,GC 就永远不会回收对象。
软引用(Soft Reference):表示对内存敏感的对象引用。只有在内存不足时,JVM 才会尝试回收软引用指向的对象,适合做缓存。可用 SoftReference<T> 实现。
弱引用(Weak Reference):无论内存是否紧张,只要 GC 发生,弱引用对象都会被回收。适合构建非强制关联结构,如 WeakHashMap。
虚引用(Phantom Reference):无法通过引用获得对象实例,唯一用途是配合 ReferenceQueue 追踪对象被回收的时机。是最"虚"的引用,不影响对象回收,仅作为通知机制。
这四种引用形式是构建内存敏感、可控缓存与资源释放机制的重要基础,在 JVM 内部、JDK 源码中应用广泛。
25. Java 中有哪些垃圾回收类型?作用范围是什么?
回答:
Java 的垃圾回收类型包括 Minor GC、Major GC、Full GC 和 Mixed GC。它们对应不同内存区域的回收操作。
分析:
在 JVM 中,堆内存被划分为新生代(Young Generation)和老年代(Old Generation),不同类型的 GC 作用范围不同:
Minor GC(Young GC):回收新生代内存,特别是 Eden 区和 Survivor 区。频率高,耗时短;
Major GC(Old GC):回收老年代内存。耗时较长,频率低;
Full GC:对整个堆(新生代 + 老年代)以及方法区进行垃圾回收,是最"全量"的 GC,代价也最大;
Mixed GC:G1 垃圾回收器引入的新模式,同时回收整个新生代和部分老年代,兼顾停顿时间和回收效率。
了解这些回收类型有助于分析 GC 日志、调优内存模型,提升系统响应能力。
| GC类型 | 作用范围 | 频率 | 耗时 | 特点 |
|---|---|---|---|---|
| Minor GC | 新生代 | 高 | 短 | 回收Eden和Survivor区 |
| Major GC | 老年代 | 低 | 长 | 回收老年代内存 |
| Full GC | 整个堆+方法区 | 最低 | 最长 | 最全面的GC |
| Mixed GC | 新生代+部分老年代 | 中等 | 中等 | G1收集器特有 |
26. Java 中堆内存的分配策略是什么?
回答:
JVM 在对象分配时主要遵循五大策略:对象优先分配在 Eden 区;大对象直接进入老年代;长期存活对象晋升;动态年龄判断;空间分配担保。
分析:
Java 堆通常被划分为新生代(Eden + 2 个 Survivor 区)与老年代。在实际分配时遵循以下逻辑:
(1)新建对象默认分配在 Eden 区,当 Eden 区满则触发 Minor GC,将仍存活对象转移至 Survivor 区;
(2)大对象(如大数组、长字符串)为了避免频繁复制,直接进入老年代,阈值通过 -XX:PretenureSizeThreshold 控制;
(3)对象在 Survivor 区中每经历一次 GC,年龄加一,达到 -XX:MaxTenuringThreshold 时晋升至老年代;
(4)若 Survivor 区内同年龄对象总大小超过空间一半,将该年龄及以上对象直接晋升;
(5)GC 过程中,若新生代回收后需要晋升的对象过多,JVM 会判断老年代是否有足够空间,若不满足担保条件则触发 Full GC。
这种动态分配策略兼顾内存利用率和回收效率,是 JVM 垃圾回收的核心之一。
27. new 创建的对象一定在堆吗?局部变量是基本类型时存储在哪?
回答:
new 出来的对象通常在堆中,但在满足逃逸分析条件下也可能在栈上分配。基本类型的局部变量分配在栈上,成员变量则随对象存在于堆中。
分析:
JVM 默认将 new 出来的对象分配到堆内存,以便 GC 管理和线程共享。然而,在开启逃逸分析优化(-XX:+DoEscapeAnalysis)后,JVM 能识别出作用域仅限当前线程、方法内的对象,将其分配在栈上,从而避免 GC 负担。此类优化适用于短生命周期、小对象场景。
对于基本类型变量,其行为区分明显:局部变量如 int x = 10; 会直接存在于当前线程的虚拟机栈帧中的局部变量表;而作为成员变量时,如 int age;,则存储在对象实例中,随对象一同分配在堆上。
因此,是否在堆上分配,取决于对象的引用范围和 JVM 优化策略,而变量是否是成员变量,则决定了其具体存储区域。
28. Java 堆内存详细说说,为什么要这么划分,用的什么垃圾回收算法?
回答:
Java 堆通常被划分为新生代和老年代,新生代又细分为 Eden 区和两个 Survivor 区。这种分区有助于针对不同生命周期的对象使用更高效的垃圾回收算法。
分析:
Java 的堆内存划分体现了对对象生命周期的深刻理解。大多数对象生命周期短,属于"朝生暮死"的类型,因此 JVM 将堆划分为新生代和老年代。新生代(Young Generation)负责处理新创建的对象,采用复制算法(Copying GC),将对象从 Eden 区复制到 Survivor 区以进行筛选。只要存活下来数次 GC 的对象,会被晋升到老年代(Old Generation)。
老年代存储生命周期较长的对象,采用标记清除(Mark-Sweep)或标记整理(Mark-Compact)算法以节省内存碎片和复制成本。通过这种代际划分,JVM 能实现更高效、更有针对性的垃圾收集策略:新生代频繁回收,但代价较低;老年代回收少,但回收过程更彻底。
此外,这一结构也是实现不同垃圾回收器(如 G1、CMS、Parallel GC)策略的基础。代际划分并非强制,而是 JVM 在性能与可维护性之间权衡的结果。
29. 垃圾回收算法有哪些?
回答:
常见的 GC 算法包括标记清除、复制算法、标记整理以及分代收集算法,各自适用于不同生命周期的对象和内存区域。
分析:
Java 垃圾回收算法主要有四类:
- 标记-清除(Mark-Sweep):先标记所有存活对象,再清除未被标记的对象。实现简单但可能导致内存碎片;
- 复制算法(Copying):将对象从一个内存区域复制到另一个区域(通常为新生代的两个 Survivor 区),适合回收短生命周期对象,效率高但浪费空间;
- 标记-整理(Mark-Compact):在标记后将存活对象压缩到内存一端,消除碎片,适合老年代使用;
- 分代收集(Generational GC):JVM 默认策略,结合以上算法,对新生代用复制算法、老年代用标记清除或整理算法。
这些算法构成了现代 GC 的基础。JVM 的不同垃圾回收器会基于这些原理进行改良,以在低延迟、高吞吐和最小内存占用之间取得平衡。
30. 有哪些垃圾回收器?
回答:
主流垃圾回收器包括 Serial、ParNew、Parallel Scavenge、CMS、G1、ZGC、Shenandoah 等,各自针对吞吐量、停顿时间或大堆管理进行了优化。
分析:
Java 提供了多种 GC 实现,它们的选择取决于应用场景:
- Serial GC:单线程收集,适合 Client 模式或小堆环境;
- ParNew:Serial 的多线程版本,常与 CMS 搭配使用;
- Parallel Scavenge(吞吐量优先 GC):在新生代中采用多线程并行收集,适合对吞吐量要求较高的后台服务;
- Parallel Old:Parallel Scavenge 的老年代版本,采用标记整理算法;
- CMS(Concurrent Mark Sweep):标记清除算法,强调低停顿,适合响应时间敏感的应用,但会产生碎片;
- G1(Garbage First):以 Region 为单位划分堆空间,实现预测性停顿控制。它结合标记-整理和增量式收集,适用于大内存、低延迟场景,是 JDK 9 以后的默认 GC;
- ZGC/Shenandoah:超低停顿、高并发的现代 GC,实现了子毫秒级别的暂停时间,适用于对响应时间要求极高的系统。
选择合适的 GC 器可显著提升系统性能,调优时建议结合内存使用曲线、GC 日志和应用特性综合考量。
31. G1 的特点和适用场景是什么?
回答:
G1 收集器具有区域化管理、低停顿、并发并行、停顿时间可预测等特性,适用于大堆、高并发、对响应时间敏感的服务系统。
分析:
G1(Garbage First)是面向服务端和大堆应用设计的垃圾回收器,它将堆划分为若干大小一致的 Region,打破传统的代际结构,支持并发和增量式回收。其最显著的特点是用户可设置最大停顿时间(如 -XX:MaxGCPauseMillis),G1 会依据该目标对回收区域进行优先级排序,回收"收益最大"的 Region,从而控制停顿时间。
G1 同时支持年轻代与老年代的混合回收(Mixed GC),并能避免 CMS 中的碎片问题,因为其本质上采用了标记-整理算法。其并发标记阶段允许用户线程继续运行,大大降低了"Stop The World"停顿对业务的影响。
G1 特别适合运行在内存容量大的系统中,如大型 Web 应用、在线交易平台、广告推荐服务等,同时对 GC 行为可控性有较高要求的场景,是 CMS 的理想替代者。
不过,G1 对计算资源(尤其是 CPU)要求较高,在资源受限的场景下不一定是最佳选择。
32. CMS 收集器的特点?适用场景?
回答:
CMS(Concurrent Mark Sweep)是一种以最小停顿为目标的垃圾收集器,适用于对响应时间敏感、用户体验要求高的中大型服务系统。
分析:
CMS 是一种以并发为核心设计理念的老年代收集器,其主要优势是:低停顿时间。它在垃圾回收过程中尽量与用户线程并发执行,避免长时间 "Stop-The-World"。CMS 的 GC 流程包括初始标记、并发标记、重新标记和并发清除四个阶段,其中只有初始标记和重新标记会短暂停顿,其他阶段均与应用线程并发进行。
CMS 基于"标记-清除"算法,避免了传统回收中对象移动带来的开销,但也引入了内存碎片问题,有可能导致"Concurrent Mode Failure",触发 Full GC。
CMS 适合对响应时间有严格要求、交互频繁的服务系统,如电商下单、在线支付等场景。不过,由于其对 CPU 资源占用高、无法整理碎片,JDK 9 之后已被 G1 收集器所取代。尽管如此,在 JDK 8 或早期版本中,CMS 仍被广泛使用。
33. ZGC 的特点?适用场景?
回答:
ZGC 是一种低延迟、高吞吐的垃圾回收器,其最大停顿时间通常低于 10ms,适合大内存、高并发、极低响应时间需求的应用。
分析:
ZGC(Z Garbage Collector)从 JDK 11 开始成为正式可用的低延迟 GC 方案,其主要设计目标是子毫秒级别的停顿控制,支持高达 TB 级堆内存。ZGC 的核心机制是:全部回收过程都以并发方式执行,避免全堆扫描引发长时间暂停。它基于"染色指针"机制和读屏障技术,实现可标记、可压缩、可移动的对象管理,而不会阻塞用户线程。
ZGC 在回收过程中分为 Mark、Relocate、Remap 三阶段,全程几乎无"Stop-The-World",非常适合对低延迟、稳定性要求极高的场景,如在线交易、广告系统、搜索引擎等。
ZGC 还具备可扩展性强、内存碎片极少、GC 日志清晰的优点。目前已在 JDK 17 中默认稳定,可通过 -XX:+UseZGC 启用。
| ZGC特点 | 具体表现 | 优势 |
|---|---|---|
| 低延迟 | 最大停顿时间<10ms | 子毫秒级别控制 |
| 高并发 | 全部回收过程并发执行 | 几乎无STW |
| 大内存支持 | 支持TB级堆内存 | 可扩展性强 |
| 染色指针 | 基于染色指针机制 | 高效对象管理 |
| 读屏障技术 | 实现可压缩可移动 | 内存碎片极少 |
| 三阶段回收 | Mark、Relocate、Remap | 回收过程清晰 |
34. 对象创建过程都做了哪些事情?
回答:
对象创建过程包括类加载检查、内存分配、对象初始化(零值、显式赋值、构造函数)以及最终引用赋值,构成完整的对象生命周期起点。
分析:
当通过 new 关键字创建对象时,JVM 会执行以下步骤:
(1)类加载检查:确认类是否已加载、解析和初始化,若未完成将先完成类加载过程;
(2)内存分配:在堆中为对象分配内存,采用指针碰撞或空闲列表等方式,同时考虑线程安全,采用 TLAB 或加锁方式保证分配正确;
(3)零值初始化:系统将对象内存块的每个字段设为默认值,确保对象状态干净;
(4)显式初始化:执行类中定义的字段赋值、代码块初始化;
(5)构造函数执行:调用构造方法完成最终业务逻辑初始化;
(6)引用赋值:变量栈中将引用指向该对象,正式可用。
这个流程设计为保证对象创建的一致性、安全性,尤其在并发创建和 GC 干扰下仍能保障正确性。
35. main 方法执行后发生了什么?JVM 是怎么执行 main 的?
回答:
当我们执行 main 方法时,JVM 首先通过类加载器加载主类,然后启动 main 线程并构建栈帧,在方法区查找并执行 main 函数指令。
分析:
main 方法是 Java 程序的入口。JVM 启动时,先通过 ClassLoader 加载主类(通常为命令行指定的类),并验证字节码安全性。接着通过 public static void main(String[] args) 方法签名识别入口方法。JVM 为该线程分配主线程栈、程序计数器、局部变量表等执行环境,初始化方法区常量池,准备执行 main 方法指令流。
main 方法执行过程与普通方法无异,只不过它运行在名为"main"的线程上,是用户线程,生命周期受 main 方法所控制。当 main 执行结束或发生异常时,JVM 也将随之关闭(除非存在存活的非守护线程)。因此,理解 main 方法的加载和执行,是掌握 Java 启动过程的关键。
36. 对象头结构?MarkWord 结构里放了什么?
回答:
Java 中每个对象在堆中都有一个对象头,包括 MarkWord 和类型指针。MarkWord 中保存了哈希码、GC 年龄、锁状态等运行时信息。
分析:
JVM 为了支持对象的运行时行为,在堆中为每个对象结构预留了对象头(Header)。这个对象头主要由两部分组成:
MarkWord(标记字段):包含对象的运行时数据,如哈希码(hashCode)、GC 年龄(用于晋升判断)、线程锁标记(偏向锁、轻量锁、重量锁)、分代信息等。MarkWord 结构是压缩设计的,不同状态下存储的信息也不同。例如对象处于未加锁、轻量锁、重量锁时,MarkWord 中的数据布局会发生变化;
类型指针(Klass Pointer):指向对象对应的类元数据,用于支撑反射、方法分派、类型判断等功能。
在数组对象中,还会多出一个字段:数组长度。对象头是 JVM 内部优化和运行时支持的关键组成,其结构紧密依赖于虚拟机实现(如 HotSpot)与位宽配置(如 32 位或 64 位压缩指针模式)。
37. 字符串常量池的位置,什么时候创建,怎么创建的?
回答:
字符串常量池位于堆内存中的专用区域,用于存储运行期间不可变的字符串。常量池中的字符串在类加载或运行时创建,并由 String.intern() 方法控制。
分析:
在 JDK 1.6 及以前,字符串常量池位于方法区的永久代(PermGen);自 JDK 1.7 起迁移到堆中,以减轻方法区压力。常量池中的字符串具备"全局唯一性":同一内容的字符串只会在池中存一份,避免重复创建和内存浪费。
当我们使用字面值(如 String s = "abc")声明字符串时,JVM 会在常量池中查找是否存在该字符串,若存在则直接引用;若不存在则新建后加入池中。而通过 new String("abc") 创建的字符串,会在堆中新建对象,除非调用 intern() 方法,才会尝试将其放入常量池中或返回已有引用。
这种机制提升了字符串的重用效率,尤其是在大量字符串比较、拼接等场景中大大节省了内存消耗。