我叫老凡,是一名又老又平凡的程序员,今天作为面试官,我的目标是:不管坐在我对面的是谁,我都要把他问懵!
老凡(装作和蔼可亲):你好,请问你对JVM垃圾回收有什么了解?
面试者:面试官你好,JVM垃圾回收是JVM的核心功能之一,在JVM中,有一个垃圾回收线程,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描程序中不再使用的对象并进行回收,释放内存空间。
老凡:那怎么判断一个对象不再使用了呢?
面试者:主要是通过可达性分析算法来实现的。基本思路是以一些称为GC Roots的对象作为起始点,从这些节点开始向下搜索,如果一个对象没有任何引用链相连,即从GC Roots到该对象没有任何引用链相连,则认为该对象是不可达的,也就是可以被垃圾回收器回收的。
在Java中,GC Roots一般包括下面几种类型的对象:
1.虚拟机栈(栈帧中的局部变量)中引用的对象。
2.方法区中类静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法栈中 JNI(即一般所说的 Native 方法)引用的对象。
通过以上这些根节点,垃圾回收器可以追踪到所有可达对象,其余的对象则被认为是不可达的垃圾对象。这些不可达对象将被标记为垃圾,等待垃圾回收器进行回收。
老凡:你刚才提到了引用,Java中有哪些引用类型呢?
面试者:有强引用,软引用,弱引用和虚引用。
老凡:嗯,能具体介绍下这几种引用吗?
面试者:嗯,好的。
1. 首先是强引用,任何一个对象的赋值操作就产生了对这个对象的强引用。比如 Object obj = new Object(); 我们new了一个Object对象,并将其赋值给obj ,这个obj 就是new Object()的强引用。只要强引用存在,垃圾回收器就不会回收被引用的对象,即使内存不足时,JVM也会直接抛出OutOfMemoryError,而不会去回收。当然,如果想中断强引用与对象之间的联系,可以将强引用赋值为null (obj =null;),这样JVM就可以在适当的时间回收对象了。
2. 软引用:引用和对象通过SoftReference建立关联,例如:SoftReference sRef = new SoftReference(new Object());软引用只有在内存不足的情况下,new Object对象才会被回收,当内存够用就不会回收(即使发生了GC)。需要注意的是 new SoftReference对象是强引用。
3. 弱引用:弱引用和软引用有点类似,用WeakReference 来表示弱引用,WeakReference wRef = new WeakReference(new Object()); 不同的是只要垃圾回收执行,弱引用的对象就会被回收,而不管内存是否够用。
4. 虚引用:虚引用是最弱的一种引用关系,无法通过虚引用来获取对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
老凡:垃圾回收主要作用于JVM哪个内存区域?
面试者:垃圾回收主要作用于堆和方法区,因为堆和方法区中存储的是对象实例和类信息,是JVM中内存最大、使用最频繁的两个区域。虚拟机栈和程序计数器中存储的数据都是与线程有关的,因此不需要垃圾回收来管理。虽然虚拟机栈中也会存储一些对象引用,但是这些引用都是弱引用,垃圾回收器不会根据这些引用来判断对象是否可回收,因此虚拟机栈不是垃圾回收的重点。
老凡:介绍下垃圾回收算法
面试者:好的,目前主要有以下几种垃圾回收算法:
1.标记-清除算法:标记-清除算法是最基本的垃圾回收算法,其工作原理是先标记出所有活跃的对象,然后将不活跃的对象进行清除。标记-清除算法有两个缺点:一是无法解决内存碎片问题,二是清除后会留下大量的空闲内存无法利用。
2.复制算法:复制算法是将内存分为两个大小相等的区域,每次只使用其中一个区域。当一个区域中的对象使用完后,将所有存活的对象复制到另一个区域中,然后清除原来的区域。这种算法可以很好地解决内存碎片问题,但是需要消耗一定的内存空间。
3.标记-整理算法:在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率比较高,这样就会有较多的复制操作,导致效率变低。标记-清除算法倒是可以应用在老年代中,但是会产生大量的内存碎片。因此就出现了标记-整理算法,标记-整理算法与标记-清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。但这种算法仍然有一定的缺点,就是仍需要进行局部的对象移动,一定程度上降低了效率。
4.增量式垃圾回收算法:增量式垃圾回收算法将一次完整的垃圾回收过程分成多个阶段进行,每个阶段都会在回收后将处理的对象标记出来,下一个阶段则从上一个阶段的结束位置继续进行回收。这种算法可以将一次完整的垃圾回收过程分散到多个阶段进行,减少回收时的停顿时间。
5.分代回收算法:分代回收算法是一种基于对象的存活时间来划分内存区域的垃圾回收算法,将对象按照年龄划分到不同的区域中,根据各个年龄段对象的特点采用不同的垃圾回收算法,从而达到最优的垃圾回收效果。
老凡:详细说下分代回收算法
面试者:好的,分代回收算法的基本思想是根据对象的生命周期将堆内存分为不同的区域,并对这些区域采用不同的回收策略。
一般来说,堆内存被划分为新生代和老年代两个区域。新生代默认占总空间的1/3,老年代默认占2/3。新生代又被分为3个区域:Eden,To Survivor、From Survivor,他们的空间占比默认是8:1:1。新生代用于存放新创建的对象,大部分对象的生命周期都很短,很快就会被回收掉。因此,新生代采用的是复制算法,即将新生代分为两个大小相等的区域,每次只使用其中的一个区域存放对象,当这个区域被占满时,就将其中存活的对象复制到另一个区域中,然后清空原区域。这样,对象的生命周期较短的新生代中的对象就可以得到有效回收。
老年代则是存放生命周期较长的对象,一般采用标记-整理或标记清除算法进行回收。
老凡:那JVM具体怎么判断要给对象分配在新生代还是老年代的呢?
面试者:JVM对对象分配到新生代还是老年代的判断主要基于对象的年龄和大小。通常情况下,新创建的对象都会被分配到新生代的Eden区域。当Eden区域满时,JVM会触发一次Minor GC,将存活的对象移动到Survivor区域,并且将对象年龄设为1,对象在Survivor区域每经历一次Minor GC,年龄就增加1,当年龄增加到一定值(默认是15)对象就会晋升到老年代中。
当对象的大小超过了Eden区域的大小时,JVM会将该对象直接分配到老年代中,同时也会触发一次Full GC,清理掉老年代中无用的对象。
老凡:了解得很清楚嘛,那你知道哪些垃圾回收器呢?
面试者:了解过一些垃圾回收器:
1. Serial收集器:采用复制算法,是新生代的单线程垃圾收集器,优点是简单高效。
2. ParNew收集器:也是采用复制算法,是 Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
3. Parallel Scavenge收集器:采用复制算法,是新生代并行垃圾收集器。Parallel收集器最大的优点是它可以利用多核CPU的优势,使用多个线程进行垃圾回收,提高了回收效率。但缺点也很明显,由于使用多个线程进行垃圾回收,因此它的暂停时间可能会比较长。
4. Serial Old收集器:采用标记-整理算法,老年代的单线程收集器, Serial收集器的老年代版本。
5. Parallel Old收集器:采用标记-整理算法,老年代的并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本。
6. CMS收集器:是一种多线程的、基于标记-清除算法的垃圾收集器,它主要用于对响应时间要求较高的应用程序。CMS收集器将垃圾回收分为两个阶段,分别是初始标记和并发标记。在初始标记阶段,CMS收集器会暂停应用程序的线程,进行一些必要的标记操作。在并发标记阶段,CMS收集器会和应用程序的线程一起运行,对堆内存中的对象进行标记。
CMS收集器最大的优点是它的暂停时间比较短,可以满足对响应时间要求较高的应用程序。但它也有缺点,主要是它采用标记-清除算法,会产生大量的内存碎片,可能会导致堆内存的利用率降低。
7. G1收集器:是一种多线程的、基于标记-整理算法的垃圾收集器,它主要用于对延迟时间要求较高的应用程序。G1收集器将堆内存分成了多个大小相等的区域(Region),并根据垃圾分布情况动态确定每个区域的用途。此外它还有一个很重要的特点:G1回收的范围是整个Java堆(包括新生代和老年代)。
老凡:如何选择合适的垃圾收集器?
面试者:这个需要考虑多方面的因素,包括应用程序的特征、硬件环境、性能目标和限制等。 例如,如果应用程序具有大量的短暂对象,则适合使用新生代收集器。如果应用程序具有较长的生命周期和大量的长期存活对象,则应选择老年代收集器。 硬件环境也会影响垃圾收集器的选择。例如,如果系统具有多个CPU和大量内存,则可以使用并发垃圾收集器来提高垃圾回收的效率。 还有性能目标和限制也是选择垃圾收集器的重要因素。如果应用程序需要较低的垃圾回收延迟,则应该选择低延迟垃圾收集器。如果应用程序需要更高的吞吐量,则应该选择高吞吐量垃圾收集器。 JVM 版本:JVM 版本也会影响垃圾收集器的选择。例如,G1 收集器只在 JDK 1.7 及以上版本中可用。
老凡:知道怎么调整垃圾收集器的参数吗?有哪些要注意的?
面试者:我了解的垃圾收集器常见的参数有:
1.新生代收集器参数 :
-
-XX:NewSize:设置新生代初始大小
-
-XX:MaxNewSize:设置新生代最大大小
-
-XX:NewRatio:设置新生代与老年代的比例
2.老年代收集器参数:
-
-XX:NewSize:设置新生代初始大小
-
-XX:MaxNewSize:设置新生代最大大小
-
-XX:NewRatio:设置新生代与老年代的比例
3.并发收集器参数:
- -XX:+CMSParallelRemarkEnabled:开启并行标记
- -XX:+CMSParallelInitialMarkEnabled:开启初始标记的并行化执行
- -XX:+CMSClassUnloadingEnabled:开启CMS垃圾回收时对类元数据的回收
4.G1收集器参数:
- -XX:InitiatingHeapOccupancyPercent:设置触发G1垃圾收集的Java堆占用比例
- -XX:MaxGCPauseMillis:设置G1最大停顿时间
- -XX:G1HeapRegionSize:设置每个G1区域的大小
- -XX:ConcGCThreads:设置并发GC使用的线程数目
在调整垃圾收集器参数时,需要注意以下几个方面:
- 确定调整参数的依据,需要基于实际测试数据进行分析,以便达到更好的垃圾收集性能。
- 不要随意调整参数,需要根据实际场景进行逐步的调整。
- 调整时需要综合考虑整个系统的负载和资源情况,避免出现过度调优的情况,导致系统资源浪费。
- 需要根据不同的垃圾收集器进行调整,因为每种垃圾收集器有其特定的参数和使用方法。
- 在调整之前,应该先备份原始的JVM配置,以便在出现问题时可以快速恢复。
老凡:不错不错,最后一个问题,你一天能上几天班?
面试者:啊。。。。。。
我叫老凡,没想到他最后还是被我问懵了,嘿嘿嘿…