面试题006-Java-JVM(下)
目录
- 面试题006-Java-JVM(下)
-
- 题目自测
- 题目答案
-
- [1. 为什么需要GC?](#1. 为什么需要GC?)
- [2. 有哪些常见的GC?](#2. 有哪些常见的GC?)
- [3. Minor GC 和 Full GC有什么区别?](#3. Minor GC 和 Full GC有什么区别?)
- [4. 如何判断一个对象是否死亡?](#4. 如何判断一个对象是否死亡?)
- [5. 讲一下可达性分析算法的流程?](#5. 讲一下可达性分析算法的流程?)
- [6. 如何判断一个常量是废弃常量? 如何判断一个类是无用的类?](#6. 如何判断一个常量是废弃常量? 如何判断一个类是无用的类?)
- [7. 垃圾收集有哪些算法,各自的特点?](#7. 垃圾收集有哪些算法,各自的特点?)
- [8. 默认的垃圾回收器是哪一个?](#8. 默认的垃圾回收器是哪一个?)
- [9. 说一下G1垃圾收集器的步骤,有什么缺点?](#9. 说一下G1垃圾收集器的步骤,有什么缺点?)
- [10. 什么是类加载?何时类加载?类加载流程?](#10. 什么是类加载?何时类加载?类加载流程?)
- [11. 知道哪些类加载器?类加载器之间的关系?](#11. 知道哪些类加载器?类加载器之间的关系?)
- 参考资料
题目自测
- 1. 为什么需要GC?
- 2. 有哪些常见的GC?
- 3. Minor GC 和 Full GC有什么区别?
- 4. 如何判断一个对象是否死亡?
- 5. 讲一下可达性分析算法的流程?
- 6. 如何判断一个常量是废弃常量? 如何判断一个类是无用的类?
- 7. 垃圾收集有哪些算法,各自的特点?
- 8. 默认的垃圾回收器是哪一个?
- 9. 说一下G1垃圾收集器的步骤,有什么缺点?
- 10. 什么是类加载?何时类加载?类加载流程?
- 11. 知道哪些类加载器?类加载器之间的关系?
题目答案
1. 为什么需要GC?
答:垃圾收集器通过自动内存管理,解决了手动管理内存时常见的问题(如内存泄漏和野指针)等,通过自动回收不在使用的对象、整理内存碎片等,极大的提升了程序的稳定性和开发效率。使得程序员可以专注业务逻辑的处理,而不必担心复杂的内存管理等问题。需要GC的主要原因如下
- 防止内存泄漏:内存泄漏是指程序在运行过程中,未正确释放不再使用的内存,导致系统内存逐渐减少,最终可能导致系统崩溃。
- 提高开发效率:GC自动化了内存管理,使得程序员可以更加专注业务逻辑的实现,不需要担心复杂的内存管理逻辑,从而提高了代码的开发效率和可维护性。
- 优化内存使用:GC会定期扫描堆内存,回收不再使用的对象所占的内存,释放系统资源,从而优化内存的使用。
- 提高程序稳定性:通过自动回收不再使用的对象,GC可以有效地防止内存耗尽,从而提高程序的稳定性和可用性。
2. 有哪些常见的GC?
答:垃圾收集器是实现垃圾回收操作的具体机制。常见的垃圾收集器有:
- Serial GC:
- 单线程的垃圾收集器,工作时会暂停所有的应用线程。
- 适用于单处理器机器或者小型应用程序,堆内存较小的情况下效果较好。
- 启动参数:-XX:+UseSerialGC
- Parallel GC:
- 多线程的垃圾收集器,可以利用多核处理器进行并行垃圾收集,提高吞吐量。JDK8的默认收集器。
- 适用于多处理器机器和对响应时间要求不高的应用,目标是最大化应用程序的吞吐量。
- 启动参数:-XX:+UseParallelGC 或者 -XX:+UseParallelOldGC。
- CMS GC:
- 低停顿的垃圾收集器,使用并发标记-清除算法,尽量减少垃圾收集导致的应用停顿时间。
- 用于对响应时间要求高的应用程序,特别是需要大堆内存且停顿时间敏感的服务,如Web服务器。
- 启动参数:-XX:+UseConcMarkSweepGC。
- G1 GC:
- 向服务器应用的垃圾收集器,采用分区算法,将堆划分为多个独立区域,优先回收垃圾最多的区域,能够提供更可预测的停顿时间。JDK9之后的默认收集器。
- 适用于大堆内存、高吞吐量和低停顿时间要求的应用,是CMS GC的替代者。
- 启动参数:-XX:+UseG1GC
- ZGC:
- 可扩展的低停顿垃圾收集器,设计目标是在拥有非常大的堆(TB级别)时,仍然能保持极低的停顿时间(通常在几毫秒以内)。
- 适用于需要处理非常大堆内存且对停顿时间要求极高的应用。
- 启动参数:-XX:+UseZGC
3. Minor GC 和 Full GC有什么区别?
答:Minor GC 和 Full GC是两种不同的垃圾收集操作,他们主要的区别是执行范围和触发条件。
- Minor GC:
- 作用范围:Minor GC 主要在新生代进行垃圾回收,新生代包括Eden区和两个Survivor区。
- 触发条件:当新生代Eden区空间满时,会触发Minor GC。Eden区中的对象会被回收或复制到Survivor区。
- 系统影响:Minor GC执行速度快,停顿时间短,对系统影响小。
- Full GC:
- 作用范围:Full GC 会回收整个堆、包括新生代和老年代,以及方法区或者元空间。
- 触发条件:老年代空间不足、调用System.gc()方法、永久代/元空间内存不足、特定的JVM行为或调优参数配置也可能触发Full GC。
- 系统影响:因为要清理整个堆的内存,开销较大,停顿时间长,对系统影响较大,需要避免频繁的Full GC。
4. 如何判断一个对象是否死亡?
答:如果一个对象不被任何变量或对象引用,那么它就是无效对象,需要被垃圾回收器回收。主要依据两种方式:引用计数法和可达性分析算法。
- 引用计数法:
- 在对象头维护着一个counter计数器,对象被引用一次计数器就加1,引用失效计数器减1。当计数器为0时就认为该对象死亡了。
- 这个方法实现简单,效率高。但无法解决循环引用问题,容易导致内存泄漏。
- 可达性分析算法:
- 这是Java中实际采用的方法判断对象是否可以被回收。它的基本思想是通过一系列称为"GC Roots"的对象作为根节点,向下搜索。节点走过的所有路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明该对象不可活,需要被回收。
5. 讲一下可达性分析算法的流程?
答:可达性分析算法是现代JVM中用来判断对象是否存活的主要方法。该算法通过从一组被称为"GC Roots"的根对象出发,沿着这些对象引用链,找到所有可达对象。那些不可达的对象会被认为是垃圾,可以被回收。
- 确定 GC Roots:首先需要确定一系列称为"GC Rooos"的对象集合。这些对象包括
- 虚拟机栈(栈帧中本地变量表)中引用的对象。
- 本地方法栈(Native方法)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 被同步锁持有的对象。
- 从根节点开始搜索:从GC Roots开始,JVM会通过深度优先搜索(DFS)或广度优先搜索(BFS),搜索过程中能被根节点引用的对象被认为是可达的,不会被回收。
- 标记不可达对象:搜索结束后,没有被搜索到的对象,被认为是不可达的。对这些不可达的对象进行标记。
- 二次标记与清理:在此阶段,JVM会进行筛选,判断这个对象是否有必要执行finalize()方法。如果没必要那么对象就会被直接回收。如果有必要执行finalize(),那么对象会被放入一个F-Queue队列中,等待后续处理。finalize()方法执行后,JVM会进行第二次可达性分析,如果对象仍然不可达,那么它将被彻底回收。
6. 如何判断一个常量是废弃常量? 如何判断一个类是无用的类?
答:
- 判断废弃常量:如果常量池中的常量没有被任何对象和变量引用,就会被判断为废弃常量。如一个字符串"abc"进入了常量池,系统中没有任何的String对象引用这个常量,也没有其他地方引用这个字面量,那么"abc"会被清除出常量池。
- 判断无用类:判断无用类的条件比较严格,要满足下面三个条件才会被认为是无用类
- 该类的所有对象都已经被清除,也就是堆中没有任何该对象的信息。
- 加载该类的ClassLoader已经被回收。
- 该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
7. 垃圾收集有哪些算法,各自的特点?
答:垃圾收集算法用于自动管理和回收不再使用的内存空间。常见的垃圾回收算法有:
- 标记-清除算法:首先遍历标记出所有不需要回收的对象,然后遍历堆中的对象,将标记的对象清除。
- 特点:优点是简单,能有效回收无用对象。缺点是标记和清除效率不高,标记清除后会产生大量不连续的内存碎片。
- 复制算法:将内存分成两块相等的区域,每次只使用其中的一块。当这一块使用完后,将还存活的对象复制到另一块。然后再把使用的空间全部清除。
- 特点:适合新生代,只复制存活对象,速度快,没有内存碎片。但缺点是内存利用率低,只有一半内存空间被使用。
- 标记-整理算法:第一阶段与清除标记算法一样。然后再移动存活对象,按照内存地址依次排序,然后清理掉端边界以外的内存。
- 特点:适合老年代,解决了内存碎片问题。但是因为需要整理对象,所以效率也不高。
- 分代收集算法:根据对象存活周期的不同,将内存划分为几块。一般Java堆分为新生代和老年代。新生代使用复制算法,老年代使用标记-清除算法、标记-整理算法。
8. 默认的垃圾回收器是哪一个?
答:在Java 8中,默认的垃圾回收器是Parallel GC。Java 9及之后,默认的垃圾回收器说G1 GC。
-
Parallel GC:主要追求高吞吐量,适用于需要大量数据处理、批处理或后台任务的应用。它通过多线程并行处理垃圾回收,能够充分利用多核处理器的能力。
-
G1 GC:设计目的是减少垃圾回收停顿时间,使其更适合需要低停顿时间的应用程序,例如需要较好响应时间的交互式应用。
-
查看默认的垃圾收集器:
bashjava -XX:+PrintCommandLineFlags -version
9. 说一下G1垃圾收集器的步骤,有什么缺点?
答:G1 是一款面向服务端应用的垃圾收集器,将内存区域分成多个独立的Region。首先估计每个Region中垃圾的数量,每次从垃圾回收价值最大的Region开始回收,因此可以获得最大的回收效率。
G1 GC的步骤如下:
- 初始标记:需要暂停应用线程,然后使用一条线程标记GC Roots直接可达的对象。
- 并发标记:使用一条标记线程和用户线程一起并发执行。在此过程进行可达性分析。
- 最终标记:暂停应用线程,使用多条标记线程,标记在并发标记阶段发生变动的对象。
- 筛选回收:计算每个区域垃圾比例,选择回收价值大的区域,将存活对象复制到新区域,并整理旧区域。
G1收集器的缺点:
- 调优复杂:G1 GC具有许多参数,调优过程复杂,可能需要大量的实验和监控。
- 额外的内存开销:G1需要维护额外的数据结构来跟踪每个区域的状态,这会增加一定的内存开销。
- 处理大对象性能较差:大对象会占用多个连续的区域,如果内存中没有足够连续的空闲区域,就可能导致GC频繁发生。
10. 什么是类加载?何时类加载?类加载流程?
答:类加载是JVM将字节码文件(.class)加载的内存中,并对其进行校验、解析、初始化,最终生成Class对象的过程。
类加载遵循"按需加载"的原则,即类在被首次主动使用时,JVM才会加载它。
- 在遇到 new、putstatic、getstatic、invokestatic 字节码指令时,如果类尚未初始化,则需要先触发其初始化。
- 对类进行反射调用时,如果类还没有初始化,则需要先触发其初始化。
- 初始化一个类时,如果其父类还没有初始化,则需要先初始化父类。
- 虚拟机启动时,需要指定一个包含 main() 方法的主类,虚拟机会先初始化这个主类。
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发其初始化。
类加载的流程分为5个阶段,分别为加载、验证、准备、解析和初始化。
- 加载:查找并加载类的二进制数据,创建一个代表该类的Class对象。
- 验证:确保类文件的字节码符合JVM规范,没有安全性和稳定性问题。
- 准备:为静态成员变量分配内存,并将其默认为初始化值。
- 解析:将类中的符号引用转换为直接引用。
- 初始化:执行类的静态初始化块和静态变量的初始化代码。
11. 知道哪些类加载器?类加载器之间的关系?
答:类加载器是负责加载类文件的对象,类加载器将字节码文件读取进内存,并将其转换为Class对象。Java中的类加载器体系具有层次结构,各种类加载器之间存在父子关系,遵循双亲委派模型。
- 启动类加载器(Bootstrap ClassLoader):最顶层的加载类,由C/C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库。
- 扩展类加载器(Extension ClassLoader):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类
- 应用程序类加载器(Application ClassLoader):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
- 自定义加载器(Custom ClassLoader):由用户自定义,适用于特殊需求,如加载加密的类文件、从网络加载类等。
类加载器之间的关系:
- 每个类加载器都有一个父类加载器,除了启动类加载器。
- 都遵循双亲委派模型。