1. Java 内存管理面试指南一
  2. Java 基础面试指南一
  3. Java 基础面试指南二
  4. Java 基础面试指南三
  5. Java 基础面试指南四
  6. Java 线程面试指南一
  7. Java 线程面试指南二
  8. Redis 面试指南一
  9. Kafka 面试指南一
  10. Spring 面试指南一
  11. SpringBoot 面试指南一
  12. 微服务面试指南一

1. 简介

在本文中,我们将探讨一些在 Java 开发人员面试中经常出现的内存管理问题。内存管理是一个很少有开发人员真正熟悉的领域。

实际上,开发人员通常不必直接处理这个概念,因为 JVM 会处理所有细节。除非出现严重问题,否则即使是经验丰富的开发人员,一旦遇到内存管理相关问题,也可能无法获得准确的信息。

另一方面,这些概念在面试中非常普遍。因此,让我们直接进入正题。

2. 问题

Q1. “用 Java 管理内存”是什么意思?

内存是应用程序有效运行所必需的关键资源,并且像任何资源一样,它是稀缺的。因此,在应用程序或应用程序的不同部分之间来回分配和重新分配内存,需要很多注意和考虑。

但是,在 Java 中,开发人员无需显式分配和取消分配内存。JVM(Java Virtual Machine,Java 虚拟机),更具体地说是垃圾回收器(Garbage Collector),负责处理内存分配,因此开发人员不必手动干预。

这与 C 语言形成了鲜明对比(在 C 语言中,程序员可以直接访问内存并在代码中直接引用内存单元),Java 的方式大大减少了内存泄漏的空间。

Q2. 什么是垃圾回收及其优势?

垃圾回收(Garbage Collection, GC)是查看堆内存、识别正在使用的对象和未使用的对象,以及删除未使用对象的过程。

  • 正在使用的对象(或引用的对象):意味着程序的某些部分仍维护着指向该对象的指针。
  • 未使用的对象(或未引用的对象):程序的任何部分都不再引用该对象。因此,未引用对象使用的内存可以被回收。

垃圾回收的最大优点是,它减轻了我们手动分配/释放内存的负担,因此我们可以专注于解决手头的问题。

Q3. 是否存在垃圾回收的缺点?

是。每当垃圾收集器运行时,它都会影响应用程序的性能。这是因为必须停止应用程序中的所有其他线程,以允许垃圾回收器线程有效地完成其工作。

根据应用程序的要求,这可能是客户无法接受的实际问题。但是,通过熟练的优化、垃圾收集器调整以及使用不同的 GC 算法,可以大大减少甚至消除此问题。

Q4. "Stop-The-World"一词的含义是什么?

当垃圾收集器线程正在运行时,其他线程也会停止,这意味着应用程序会立即暂停。这类似于房屋清洁或封闭消杀,在此过程完成之前,居民不得进入。

根据应用程序的需求,"Stop-The-World"垃圾收集可能会导致无法接受的冻结。这就是为什么进行垃圾收集器优化和 JVM 优化,以使遇到的冻结时间至少可以接受的原因很重要。

Q5. 什么是堆栈和堆?这些存储器结构中的每一个存储什么,以及它们如何相互关联?

  • 栈(Stack):是内存的一部分,其中包含有关嵌套方法调用的信息,这些信息直到程序中的当前位置。它还包含所有局部变量和对当前执行的方法中定义的堆上对象的引用。

    • 这种结构允许运行时从知道调用地址的方法返回,并在退出方法后清除所有局部变量。
    • 每个线程都有自己的栈。
  • 堆(Heap):是用于分配对象的大量内存。使用 new 关键字创建对象时,该对象将在堆上分配。但是,对该对象的引用仍然存在于栈中。

Q6. 什么是分代垃圾收集?什么使它成为流行的垃圾收集方法?

可以将分代垃圾收集(Generational Garbage Collection)大致定义为垃圾收集器使用的策略,在该策略中,将堆分为称为“分代”的多个部分,每个部分将根据对象在堆上的“年龄”保存对象。

每当垃圾收集器运行时,过程的第一步称为标记(Mark)。在这里,垃圾收集器会识别正在使用的内存块和未使用的内存块。如果必须扫描系统中的所有对象,这可能是一个非常耗时的过程。

随着分配的对象越来越多,对象列表越来越多,导致垃圾收集时间越来越长。但是,对应用程序的经验分析表明,大多数对象都是短暂的。

对于分代垃圾回收,根据对象的存活时间将其按照其“年龄”进行分组。这样,大部分工作分散在各个次要(Minor)和主要(Major)的收集周期中。

今天,几乎所有垃圾收集器都是分代的。这种策略之所以如此流行,是因为随着时间的推移,它已被证明是最佳解决方案。

Q7. 详细描述分代垃圾回收的工作方式

为了正确理解分代垃圾回收的工作原理,重要的是首先记住 Java 堆的结构如何促进分代垃圾回收。

堆划分为几个部分。分别是:年轻代(Young Generation)、老年代(Old Generation)以及永久代(Permanent Generation)。

  • 年轻代:存储大部分新创建的对象。对大多数应用程序的经验研究表明,大多数对象寿命短,因此很快就有资格进行收集。因此,新对象在这里开始其旅程,并且只有在达到一定的“年龄”后才被“提升”到老年代空间。

    • 术语“年龄”在分代垃圾回收中,是指该对象已存活回收周期的数量。
    • 年轻代空间又分为三个空间:一个 Eden 空间和两个幸存者空间,例如 Survivor 1 (S1) 和 Survivor 2 (S2)。
  • 老年代:存储的对象不再住在内存中超过一定的“年龄”。幸免于年轻代垃圾收集的对象被提升到这个空间。它通常比年轻代大。由于垃圾收集的大小较大,因此与年轻代相比,垃圾收集更昂贵且发生频率更低。
  • 永久代(或更通常称为 PermGen):包含由 JVM 所需的元数据,用来描述应用程序使用的类和方法。它还包含用于存储内部字符串的字符串池。它由 JVM 在运行时根据应用程序使用的类来填充。另外,平台库类和方法可以存储在这里。

工作流程如下:

  1. 首先,将任何新对象分配给 Eden 空间。两个幸存者空间开始都是空的。
  2. 当 Eden 空间填满时,将触发次要垃圾回收(Minor GC)。引用的对象将移动到第一个幸存者空间。未引用的对象将被删除。
  3. 在下一个次要 GC 期间,Eden 空间也会发生同样的事情。删除未引用的对象,并将引用的对象移到幸存者空间。然而,在这种情况下,它们被移动到第二幸存者空间 (S2)。
  4. 另外,来自第一个幸存者空间 (S1) 中上一个次要 GC 的对象的年龄增加,并移动到 S2。将所有尚存的对象移至 S2 之后,将清除 S1 和 Eden 空间。此时,S2 包含具有不同年龄的对象。
  5. 在下一个次要 GC 中,重复相同的过程。但是,这次幸存者空间切换了。引用的对象从 Eden 和 S2 都移到 S1。幸存的对象会老化。Eden 和 S2 被清除。
  6. 在每个次要垃圾回收周期之后,将检查每个对象的寿命。那些达到某个任意年龄(例如 8)的对象从年轻代晋升为老年代。对于随后的所有 Minor GC 周期,将继续将对象提升到旧空间。

这几乎耗尽了年轻代中垃圾收集的过程。最终,将对老年代进行大规模的垃圾收集(Major GC),以清理并压缩该空间。对于每个 Major GC,都有几个 Minor GC。

Q8. 什么时候对象才有资格进行垃圾收集?描述 GC 如何收集合格对象?

如果无法通过任何活动线程或任何静态引用访问该对象,则该对象可以进行垃圾回收(GC)。

对象最有资格进行垃圾回收的最直接的情况是,如果其所有引用均为 null。没有任何实时外部引用的循环依赖项也可以使用 GC。因此,如果对象 A 引用对象 B 而对象 B 引用对象 A,并且它们没有任何其他实时引用,则对象 A 和 B 都将有资格进行垃圾回收。

另一个明显的情况是将父对象设置为 null。当一个 Kitchen 对象内部引用一个 Fridge 对象和一个 Sink 对象,并且该 Kitchen 对象设置为 null 时,FridgeSink 都将与父 Kitchen 一起进行垃圾收集。

Q9. 如何从 Java 代码触发垃圾回收?

作为 Java 程序员,您不能在 Java 中强制进行垃圾回收;仅当 JVM 认为它需要基于 Java 堆大小的垃圾回收时才会触发。

在从内存中删除对象之前,垃圾回收线程会调用该对象的 finalize() 方法,并提供执行所需的各种清理的机会。您也可以调用目标代码的此方法,但是,不能保证调用此方法时将发生垃圾回收。

此外,还有诸如 System.gc()Runtime.gc() 之类的方法,这些方法用于将垃圾收集请求发送到 JVM,但不能保证会发生垃圾收集。

Q10. 当没有足够的堆空间来容纳新对象时会发生什么?

如果在 Heap 中没有用于创建新对象的内存空间,则 Java 虚拟机将抛出 OutOfMemoryError,或更确切地说是 java.lang.OutOfMemoryError: Heap space

Q11. 是否有可能“复活”成为垃圾收集对象的对象?

当某个对象可以进行垃圾回收时,GC 必须在其上运行 finalize 方法。在 finalize 方法为该对象打上 GC 标识,直到下一个周期回收。

finalize 方法中,您可以从技术上“复活”对象,例如,通过将其分配给 static 字段。该对象将再次变为活动状态,并且不符合垃圾收集的条件,因此 GC 将不会在下一个周期中对其进行收集。

但是,该对象将被标记为 finalize,因此当它再次可回收时,将不会调用 finalize 方法。从本质上讲,您可以在对象的整个生命周期中仅一次旋转此“复活”技巧。请注意,只有在您真正知道自己在做什么的情况下,才应使用此丑陋的技巧。但是,了解此技巧可以使您对 GC 的工作原理有一些了解。

Q12. 描述强、弱、软和幻影引用及其在垃圾收集中的作用

与使用 Java 管理内存一样,工程师可能需要在关键应用程序中执行尽可能多的优化,以最大程度地减少延迟并最大化吞吐量。尽管无法明确控制何时在 JVM 中触发垃圾回收但对于我们创建的对象来说,有可能影响垃圾回收的发生方式。

Java 为我们提供了引用对象,以控制我们创建的对象与垃圾收集器之间的关系。

默认情况下,我们在 Java 程序中创建的每个对象都由变量强引用:

StringBuilder sb = new StringBuilder();

在以上代码段中,new 关键字创建一个新的 StringBuilder 对象并将其存储在堆中。然后,变量 sb 存储对该对象的强引用。对于垃圾收集器来说,这意味着特定的 StringBuilder 对象由于 sb 对它的强烈引用而根本不符合收集条件。只有当我们这样使 sb 无效时,故事才会改变:

sb = null;

调用上述行后,该对象将有资格进行回收。

我们可以通过将对象显式包装在位于 java.lang.ref 包内的另一个引用对象中,来更改对象与垃圾收集器之间的关系。

可以为上述对象创建一个软引用,如下所示:

StringBuilder sb = new StringBuilder();
SoftReference<StringBuilder> sbRef = new SoftReference<>(sb);
sb = null;

在上面的代码片段中,我们创建了对 StringBuilder 对象的两个引用。第一行创建一个强引用 sb,第二行创建一个软引用 sbRef。第三行应该使对象符合收集条件,但是由于 sbRef,垃圾收集器将推迟收集对象。

只有在内存变紧并且 JVM 即将抛出 OutOfMemoryError 错误时,故事才会改变。换句话说,只回收具有软引用的对象是恢复内存的最后手段。

弱引用可以使用类似的方式来创建,使用 WeakReference 类。当 sb 设置为 nullStringBuilder 对象仅具有弱引用时,JVM 的垃圾收集器将完全没有妥协,并在下一个周期立即收集该对象。

幻象引用(Phantom Reference)类似于弱引用,并用仅虚引用的对象将无需等待被收集。但是,幻影引用在其对象被收集后就立即入队。我们可以轮询参考队列以确切了解对象何时被收集。

Q13. 假设我们有一个循环引用(两个互相引用的对象)。这样的对象可以成为垃圾收集的资格吗?为什么?

是的,一对具有循环引用的对象可以进行垃圾回收。这是因为 Java 的垃圾收集器如何处理循环引用。它不考虑对象是否存在,只要它们有任何引用,而是通过从某个垃圾回收根(活动线程或静态字段的局部变量)开始导航对象图来确定它们是否存在。如果无法从任何根目录访问具有循环引用的一对对象,则认为该对象可以进行垃圾回收。

Q14. 字符串在内存中如何表示?

String 在 Java 实例是具有两个字段的对象:一个 char[] value 和一个 int hash 字段。value 是表示字符串本身字符数组,hash 包含 字符串的 hashCode。其被初始化为零,如果字符串的 hashCode 值为零,则每次调用 hashCode() 时都必须重新计算它。

重要的是 String 实例是不可变的:您无法获取或修改 char[] value。字符串的另一个功能是将静态常量字符串加载并缓存在字符串池中。如果源代码中有多个相同的 String 对象,则它们在运行时都由单个实例表示。

Q15. 什么是 StringBuilder 及其用例?将字符串附加到 StringBuilder 和使用 + 运算符连接两个字符串之间有什么区别?StringBuilder 与 StringBuffer 有何不同?

StringBuilder 允许通过追加、删除和插入字符和字符串来操纵字符序列。与不可变的 String 类相反,这是一种可变的数据结构。

连接两个 String 实例时,将创建一个新对象,并复制字符串。如果我们需要在循环中创建或修改字符串,这可能会带来巨大的垃圾回收器开销。StringBuilder 允许更有效地处理字符串操作。

StringBufferStringBuilder 不同,它是线程安全的。如果需要在单个线程中处理字符串,请改用 StringBuilder

3. 结论

在本文中,我们讨论了 Java 工程师访谈中经常出现的一些最常见的问题。有关内存管理的问题通常是针对高级 Java Developer 候选人的,因为面试官希望您已经构建了很多琐碎的应用程序,而这些应用程序经常受到内存问题的困扰。

这不应被视为详尽的问题清单,而应作为进一步研究的起点。我们祝您在接下来的面试中取得成功。


说明:

  • 本文内容基于经典 Java 内存模型编写。在 Java 8 及更高版本中,永久代(PermGen) 已被 元空间(Metaspace) 取代。
  • Java 9 及更高版本中,String 类的内部实现已从 char[] 更改为 byte[](Compact Strings),以节省内存。
  • Object.finalize() 方法在 Java 9 中被标记为 deprecated,并在后续版本中计划移除,不建议在新代码中使用对象“复活”技巧。