1. 概述
在本文中,我们将探讨一些在Java开发人员面试中经常出现的内存管理问题。
事实上,开发人员通常不必直接处理这个概念-因为JVM会处理细节。除非出现严重错误,否则即使是经验丰富的开发人员也可能无法轻松获得有关内存管理的准确信息。
2. 问题
Q1. “Java中的内存管理”这句话是什么意思?
内存是应用程序有效运行所需的关键资源,并且与任何资源一样,它是稀缺的。因此,它在应用程序或应用程序的不同部分之间的分配和释放需要大量的关注和考虑。
但是,在Java中,开发人员不需要显式分配和释放内存。JVM,更具体地说,垃圾回收器-有处理内存分配的职责,因此开发人员不必这样做。
这与C等语言中发生的情况相反,在C语言中,程序员可以直接访问内存并在其代码中直接引用内存单元,从而为内存泄漏创造了很大的空间。
Q2. 什么是垃圾回收及其优势?
垃圾回收是查看堆内存、识别哪些对象在使用中、哪些没有使用,并删除未使用的对象的过程。
使用中的对象或引用的对象意味着你的程序的某些部分仍然保留指向该对象的指针,未使用的对象或未引用的对象不再被程序的任何部分引用,因此可以回收未引用对象使用的内存。
垃圾回收最大的好处就是免除了我们手动分配/释放内存的负担,让我们可以专注于解决手头的问题。
Q3. 垃圾回收有什么缺点吗?
是的。每当垃圾回收器运行时,它都会对应用程序的性能产生影响。这是因为必须停止应用程序中的所有其他线程以允许垃圾回收器线程有效地完成其工作。
根据应用程序的要求,这可能是客户端无法接受的实际问题。但是,通过巧妙的优化和垃圾回收器调整以及使用不同的GC算法,可以大大减少甚至消除这个问题。
Q4. “Stop-The-World”一词的含义是什么?
当垃圾回收器线程运行时,其他线程将停止,这意味着应用程序暂时停止。这类似于房屋清洁或熏蒸,在过程完成之前,住户被拒绝进入。
根据应用程序的需要,“Stop-The-World”垃圾回收可能会导致无法接受的冻结。这就是为什么进行垃圾回收器调优和JVM优化很重要,这样遇到的冻结至少是可以接受的。
Q5. 什么是栈和堆?这些内存结构中都存储了什么,它们是如何相互关联的?
堆栈是内存的一部分,包含有关嵌套方法调用的信息,一直到程序中的当前位置。它还包含所有局部变量和对当前正在执行的方法中定义的堆上对象的引用。
此结构允许运行时从知道调用地址的方法返回,并在退出方法后清除所有局部变量。每个线程都有自己的堆栈。
堆是用于分配对象的大量内存,当你使用new关键字创建对象时,它会在堆上分配。但是,对该对象的引用存在于堆栈中。
Q6. 什么是分代垃圾回收,是什么让它成为一种流行的垃圾回收方法?
分代垃圾回收可以粗略地定义为垃圾回收器使用的策略,其中堆被分成许多称为代的部分,每个部分将根据对象在堆上的“年龄”来保存对象。
每当垃圾回收器运行时,该过程的第一步称为标记。这是垃圾回收器识别哪些内存正在使用,哪些没有使用的步骤。如果必须扫描系统中的所有对象,这可能是一个非常耗时的过程。
随着分配的对象越来越多,对象列表越来越大,导致垃圾回收时间越来越长。然而,对应用程序的实证分析表明,大多数对象都是短暂的。
使用分代垃圾回收,对象根据它们存活的垃圾回收周期的“年龄”进行分组。这样,大部分工作分布在各种次要和主要的回收周期中。
如今,几乎所有的垃圾回收器都是分代的。这种策略之所以如此受欢迎,是因为随着时间的推移,它已被证明是最佳解决方案。
Q7. 详细描述分代垃圾回收的工作原理
要正确理解分代垃圾回收的工作原理,首先要记住Java堆的结构是如何促进分代垃圾回收的,这一点很重要。
堆被分成更小的空间或代。这些空间是新生代、老年代或终身代以及永久代。
年轻代承载了大部分新创建的对象,对大多数应用程序的实证研究表明,大多数对象的生命周期很短,因此很快就有资格回收。因此,新的对象从这里开始它们的旅程,只有在达到一定的“年龄”后,才会被“提升”到老年代空间。
分代垃圾回收中的术语“年龄”是指对象存活的回收周期数。
新生代空间进一步分为三个空间:一个Eden空间和两个Survivor 1(s1)和Survivor 2(s2)这样的幸存者空间。
老年代存放在内存中存活时间超过某个“年龄”的对象,从年轻代垃圾回收中幸存下来的对象被提升到这个空间,它通常比年轻代大。由于它的大小更大,垃圾回收比年轻代更昂贵且发生频率更低。
永久代或更通常称为PermGen,包含JVM所需的元数据,用于描述应用程序中使用的类和方法。它还包含用于存储驻留字符串的字符串池。它由JVM在运行时根据应用程序使用的类填充。此外,平台库类和方法可能存储在这里。
首先,任何新对象都被分配到Eden空间,两个幸存者空间开始时都是空的。当Eden空间填满时,将触发minor GC。引用的对象被移动到第一个幸存者空间,未引用的对象被删除。
在下一次minor GC期间,Eden空间也会发生同样的事情。未引用的对象被删除,引用的对象被移动到幸存者空间。但是,在这种情况下,它们被移动到第二个幸存者空间(S2)。
此外,来自第一个幸存者空间(S1)中最后一次minor GC的对象的年龄增加并被移动到S2。一旦所有幸存的对象都被移动到S2,S1和Eden空间都被清除。此时,S2包含不同年龄的对象。
在下一次minor GC中,重复相同的过程。然而这次幸存者空间发生了变化,引用的对象从Eden和S2移动到S1,幸存的对象是老化的,Eden和S2被清除。
在每个minor GC周期之后,检查每个对象的年龄。那些已经达到某个任意年龄(例如8岁)的对象将从年轻代提升到老年代或终身代。对于所有后续的minor GC周期,对象将继续提升到老年代空间。
这几乎耗尽了年轻代的垃圾回收过程。最终,将对老年代执行一次major GC,清理并压缩该空间。对于每个major GC,都有几个minor GC。
Q8. 对象何时可以进行垃圾回收?描述一下GC是如何回收一个符合条件的对象的?
如果无法从任何活动线程或任何静态引用访问对象,则该对象有资格进行垃圾回收或GC。
一个对象符合垃圾回收条件的最直接的情况是它的所有引用都为空,没有任何实时外部引用的循环依赖也符合GC的条件。因此,如果对象A引用对象B,而对象B引用对象A,并且它们没有任何其他实时引用,那么对象A和B都将符合垃圾回收的条件。
另一个明显的情况是父对象设置为null。当厨房对象在内部引用冰箱对象和水槽对象,并且厨房对象设置为空时,冰箱和水槽都将有资格与其父厨房一起进行垃圾回收。
Q9. 如何从Java代码触发垃圾回收?
作为Java程序员,你不能在Java中强制进行垃圾回收;仅当JVM认为它需要基于Java堆大小的垃圾回收时,它才会触发。
在从内存中删除对象之前,垃圾回收线程会调用该对象的finalize()方法,并提供执行所需的任何类型清理的机会。你也可以调用目标代码的此方法,但是,不能保证调用此方法时会发生垃圾回收。
此外,还有System.gc()和Runtime.gc()等方法用于向JVM发送垃圾回收请求,但不能保证垃圾回收会发生。
Q10. 当没有足够的堆空间来容纳新对象的存储时会发生什么?
如果在堆中没有用于创建新对象的内存空间,Java虚拟机将抛出OutOfMemoryError或更具体的java.lang.OutOfMemoryError heap space。
Q11. 是否有可能“复活”一个符合垃圾回收条件的对象?
当一个对象符合垃圾回收条件时,GC必须对其运行finalize方法。finalize方法保证只运行一次,因此GC将对象标记为已完成并让它休息直到下一个周期。
在finalize方法中,你可以从技术上“复活”一个对象,例如,通过将其分配给静态字段。该对象将再次变为活动状态并且不符合垃圾回收的条件,因此GC不会在下一个周期中回收它。
但是,该对象将被标记为已完成,因此当它再次符合条件时,将不会调用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即将抛出OutOfMemory错误时,情况才会改变。换句话说,回收只有软引用的对象作为回收内存的最后手段。
可以使用WeakReference类以类似的方式创建弱引用。当sb设置为null并且StringBuilder对象只有一个弱引用时,JVM的垃圾回收器将毫不妥协地在下一个周期立即回收该对象。
虚引用类似于弱引用,并且无需等待即可回收仅具有虚引用的对象。但是,一旦回收到虚引用的对象,它们就会排队。我们可以轮询引用队列以准确知道对象何时被回收。
Q13. 假设我们有一个循环引用(两个相互引用的对象),这样的对象是否有资格进行垃圾回收?为什么?
是的,一对具有循环引用的对象可以成为垃圾回收的对象,这是因为Java的垃圾回收器处理循环引用的方式。它认为对象是活的,不是当它们有任何对它们的引用时,而是当它们可以通过从某个垃圾回收根(活动线程的局部变量或静态字段)开始导航对象图来访问时。如果一对具有循环引用的对象无法从任何根访问,则认为它符合垃圾回收条件。
Q14. 字符串在内存中是如何表示的?
Java中的String实例是一个具有两个字段的对象:char[] value字段和int hash字段。value字段是一个表示字符串本身的字符数组,hash字段包含字符串的hashCode,它初始化为0,在第一次hashCode()调用期间计算并从那时起缓存。作为一种奇怪的边缘情况,如果字符串的hashCode具有0值,则每次调用hashCode()时都必须重新计算它。
重要的是String实例是不可变的:你无法获取或修改底层的char[]数组。字符串的另一个特点是静态常量字符串被加载并缓存在字符串池中。如果源代码中有多个相同的String对象,则它们在运行时都由单个实例表示。
Q15. 什么是StringBuilder以及它的用例是什么?将字符串附加到StringBuilder和使用+运算符拼接两个字符串有什么区别?StringBuilder与StringBuffer有何不同?
StringBuilder允许通过添加、删除和插入字符和字符串来操作字符序列,这是一个可变的数据结构,与不可变的String类相反。
拼接两个String实例时,会创建一个新对象,并复制字符串。如果我们需要在循环中创建或修改字符串,这可能会带来巨大的垃圾回收器开销。StringBuilder允许更有效地处理字符串操作。
StringBuffer与StringBuilder的不同之处在于它是线程安全的,如果只需要在单个线程中操作字符串,请改用StringBuilder。
3. 总结
在本文中,我们涵盖了Java工程师面试中经常出现的一些最常见的问题。有关内存管理的问题主要是针对高级Java开发人员候选人提出的,因为面试官希望你构建的应用程序非常重要,而这些应用程序很多时候都受到内存问题的困扰。
这不应被视为详尽无遗的问题清单,而是进一步研究的起点。