面试 JVM 八股文五问五答第二期
作者:程序员小白条,个人博客
相信看了本文后,对你的面试是有一定帮助的!
⭐点赞⭐收藏⭐不迷路!⭐
1.JVM运行时数据区有几部分?(JVM内存布局)虚拟机栈和本地方法栈的区别?
Java虚拟机栈存储Java方法的调用帧,而本地方法栈则存储Native方法的调用帧。
2.堆中年轻代和老年代的垃圾回放算法?
新生代:复制算法,老年代:标记-整理算法。
标记-整理算法
标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。
优点:经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。
缺点:GC 暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。
下图为“标记-整理”算法的示意图:
下图为使用“标记-整理”算法回收前后的状态:
复制算法
复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。
复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。一种典型的基于复制算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。
优点:标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。
下图为复制算法的示意图:
下图为使用复制算法回收前后的状态:
3.JVM如何判断一个对象是否回收?
1)引用计数法
在这种算法中,假设堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并且初始化赋值后,该对象的计数器的值就设置为 1,每当有一个地方引用它时,计数器的值就加 1,例如将对象 b 赋值给对象 a,那么 b 被引用,则将 b 引用对象的计数器累加 1。
反之,当引用失效时,例如一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,则之前被引用的对象的计数器的值就减 1。而那些引用计数为 0 的对象,就可以称之为垃圾,可以被收集。
特别地,当一个对象被当做垃圾收集时,它引用的任何对象的计数器的值都减 1。
优点:引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
缺点:需要额外的空间来存储计数器,难以检测出对象之间的循环引用。
2)可达性分析法
可达性分析法也被称之为根搜索法,可达性是指,如果一个对象会被至少一个在程序中的变量通过直接或间接的方式被其他可达的对象引用,则称该对象就是可达的。更准确的说,一个对象只有满足下述两个条件之一,就会被判断为可达的:
对象是属于根集中的对象
对象被一个可达的对象引用
在这里,我们引出了一个专有名词,即根集,其是指正在执行的 Java 程序可以访问的引用变量(注意,不是对象)的集合,程序可以使用引用变量访问对象的属性和调用对象的方法。在 JVM 中,会将以下对象标记为根集中的对象,具体包括:
GC ROOTS 对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中的常量引用的对象
方法区中的类静态属性引用的对象
本地方法栈中 JNI(Native 方法)的引用对象
活跃线程(已启动且未停止的 Java 线程)
根集中的对象称之为GC Roots,也就是根对象。可达性分析法的基本思路是:将一系列的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果一个对象到根对象没有任何引用链相连,那么这个对象就不是可达的,也称之为不可达对象。
如上图所示,形象的展示了可达对象与不可达对象的示例,其中灰色的对象都是不可达对象,表示可以被垃圾收集的对象。在可达性分析法中,对象有两种状态,那么是可达的、要么是不可达的,在判断一个对象的可达性的时候,就需要对对象进行标记。关于标记阶段,有几个关键点是值得我们注意的,分别是:
开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。暂停应用线程以便 JVM 可以尽情地收拾家务的这种情况又被称之为安全点(Safe Point),这会触发一次 Stop The World(STW)暂停。触发安全点的原因有许多,但最常见的应该就是垃圾回收了。
安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生安全点。对于安全点,另一个需要考虑的问题就是如何在 GC 发生时让所有线程(这里不包括执行 JNI 调用的线程)都“跑”到最近的安全点上再停顿下来。两种解决方案:
抢先式中断(Preemptive Suspension):抢先式中断不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应 GC 事件。
主动式中断(Voluntary Suspension):主动式中断的思想是当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志地地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。
在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:
如果对象在进行根搜索后发现没有与根对象相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法(可看作析构函数,类似于 OC 中的dealloc,Swift 中的deinit)。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后 GC 将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
GC 判断对象是否可达看的是强引用。
当标记阶段完成后,GC 开始进入下一阶段,删除不可达对象。当然,可达性分析法有优点也有缺点,
优点:可以解决循环引用的问题,不需要占用额外的空间
缺点:多线程场景下,其他线程可能会更新已经访问过的对象的引用
4.Java 有哪些引用类型?
在 Java 中有四种引用类型,分别为:
强引用(Strong Reference):如Object obj = new Object(),这类引用是 Java 程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用(Soft Reference):它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2 之后提供了SoftReference类来实现软引用。
弱引用(Weak Reference):它也是用来描述非必须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK1.2 之后,提供了WeakReference类来实现弱引用。
虚引用(Phantom Reference):也称为幻引用,最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2 之后提供了PhantomReference类来实现虚引用。
5.哪些对象可以作为GC Root? 类加载器可以作为GC Root吗?
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
- JNI(Java Native Interface)引用的对象
- 活跃线程(已启动且未停止的 Java 线程)
- 类加载器
- 线程
类加载器可以作为GC root的一种。在Java中,GC root是指被认为是活动对象的起始点,垃圾收集器通过GC root来判断哪些对象是可达的,哪些对象是不可达的。
类加载器在Java中起到了重要的作用,它负责加载类的字节码并将其转换为可执行的Java对象。在类加载器的生命周期中,它们会创建和维护许多对象,例如类的定义、常量池、静态变量等。这些对象会被类加载器引用,并作为GC root的一部分。
因此,类加载器本身可以被认为是GC root的一种,因为它们可以直接或间接地引用许多其他对象,并且这些对象的可达性取决于类加载器的存在。当一个类加载器不再被引用时,它及其相关的对象可能会被垃圾收集器回收。