JVM面试题

1. 如何定位垃圾/如何判断一个对象是否存活

  1. 引用计数法:给每一个对象设置一个引用计数器,当引用该对象时,引用计数器就+1,引用失效时,引用计数器就-1;当引用计数器为 0 时,就说明这个对象没有被引用,也就是垃圾对象,等待回收; 缺点:无法解决循环引用的问题;
  2. 根可达算法:从根对象向下搜索,如果一个对象到根对象没有任何引用链相连接时,说明此对象是垃圾;在 Java 中可作为 GC Roots 的对象有以下几种:
  • 虚拟机栈中引用的对象;
  • 方法区类静态属性引用的变量;
  • 方法区常量池引用的对象;
  • 本地方法栈 JNI 引用的对象;

2. 常见的垃圾回收算法

Mark-Sweep(标记清除):首先标记出所有可达的对象,然后清除所有未被标记的对象;特点:位置不连续,容易产生碎片;

Copying(复制算法):将内存分为两个区域,每次只使用其中一个区域,当这个区域满时,将活跃对象复制到另一个区域中,然后清空当前区域;特点:没有碎片,浪费空间;

Mark-Compact(标记压缩):首先标记所有可达的对象,然后将所有存活的对象向一端移动,然后清理掉边界之外的所有对象;特点:没有碎片,效率偏低;

3. JVM 中一次完整的 GC 是什么样

Java 内存划分/内存分代模型:

  • 对象优先在 eden 区分配,当 eden 区没有足够空间进行分配时,发起一次 YGC / MinorGC
  • 在 eden 区执行了第一次 YGC 之后,存活的对象会被移动到 survivor0 区;
  • eden 区再次 YGC,会采用复制算法,将 eden 和 survivor0 区一起清理,存活的对象会被复制到 survivor1 区;
  • 每移动一次,对象年龄加 1,年龄大于一定阀值(默认 15)会直接进入老年代;
  • survivor 区装不下,直接进入老年代;
  • 大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组);
  • 老年代满了而无法容纳更多的对象,通常就会进行 FGC(Full GC)/ MajorGC,FGC 清理整个内存堆 – 包括年轻代和老年代;
  • GC Tuning 是指对Java虚拟机中的垃圾回收机制进行调优,应尽量避免 FGC;

注意:GC 主要的作用区域是:方法区和堆;

3.1. MinorGC 和 FullGc 的触发条件

当年轻代中的 Eden 区满时,会触发 Minor GC;

触发 Full GC 的条件 3 种:

  1. 创建一个大对象,超过指定阈值后会存放在老年代。如果老年代空间不足,则会触发 Full GC;
  2. 根据空间分配担保机制:发生 Minor GC 之前,虚拟机会检查老年代的连续空间是否大于新生代对象总大小或者历次晋升的平均大小,如果大于就会进行 Minor Gc,否则将进行 Full GC;
  3. 代码中执行 System.gc() 时,会触发 Full GC,但是并不保证一定会触发!一般线上通过-XX:+DisableExplicitGC 参数禁用此方法;
  4. 方法区空间不足,JDK 8 之后方法区实现为元空间,存放在直接内存中。所以满足这点除非机器物理内存不够了;

4. 有哪几种垃圾回收器/收集器

4.1. Serial 收集器

        Serial 收集器是一个单线程收集器。它的单线程不仅仅意味着它只会使用一条 GC 线程去清理垃圾,更重要的是它在进行垃圾收集工作时必须暂停所有的工作线程,采用复制算法;

4.2. Serial Old 收集器

        Serial 收集器的老年代版本,同样是一个单线程收集器,采用标记-整理算法。它主要有两大用途:一种用途是与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案;

4.3. ParNew 收集器

        ParNew 收集器其实就是 Serial 收集器的多线程版本,采用复制算法,除了使用多线程进行垃圾收集外,其余和 Serial 收集器完全一样;

4.4. CMS 收集器(老年代)

        CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,实现了让 GC 线程与用户线程同时工作。基于标记清除算法实现的,整个过程分为四步:

  • 初始标记:暂停所有工作线程(会 STW),标记出与 GC Roots 直接可达的对象,速度很快;
  • 并发标记:从上一阶段标记出的对象,开始遍历整个老年代,标记出所有可达的对象,这里会跟踪记录那些发生引用更新的地方,耗时比较长;
  • 重新标记(会 STW):是为了修正并发标记期间,因为用户线程继续运行而导致标记产生变动的那部分对象的标记记录;
  • 并发清除:开启用户线程,同时 GC 线程开始清理垃圾;

缺点

  • 并发失败(Concurrent Mode Failure):如果在并发标记、并发清除过程中,由于用户线程同时执行,若有新对象要进入老年代,空间不够,就会导致并发失败,此时就会利用 Serial Old 收集器来做一次垃圾收集,就会做一次全局的 STW;
  • 无法处理浮动垃圾:并发清除阶段,可能产生新的垃圾,就是浮动垃圾,只能下次 GC 时清理;
  • 内存碎片问题:因为采用标记清除算法,这就意味着回收结束时会有内存碎片产生;

4.5. Parallel Scavenge 收集器

        Parallel Scavenge 是一个新生代、基于复制算法并发多线程的收集器,关注点是吞吐量,和 ParNew 的最大区别是 GC 自动调节策略,以提供最优的停顿时间和最大的吞吐量;

4.6. Parallel Old 收集器

        Parallel Scavenge 收集器的老年代版本,使用多线程标记整理算法;

4.7. G1 收集器

        G1 (Garbage-First) 是一款面向服务器的垃圾收集器,它具备以下特点:

  • 并行与并发:可以充分利用 CPU,缩短 STW 停顿时间;
  • 分代收集:不需要其它收集器配合就能独立管理整个 GC 堆,而且还保留了分代的概念;
  • 空间整合:从整体来看是基于标记-整理算法实现的,但从局部(两个 region 之间)上看又是基于复制算法实现的;
  • 可预测的停顿:可以通过 -XX:MaxGCPauseMills 来指定 STW 停顿时间,所以可能不会回收掉所有的垃圾对象,默认 200ms;

G1 收集器的运作过程如下:

        初始标记:暂停所有工作线程(会 STW),标记出与 GC Roots 直接可达的对象,速度很快;

        并发标记:从上一阶段标记出的对象,开始遍历整个老年代,标记出所有可达的对象,这里会跟踪记录那些发生引用更新的地方,耗时比较长;

        最终标记(会 STW):是为了修正并发标记期间,因为用户线程继续运行而导致标记产生变动的那部分对象的标记记录;

        筛选回收(会 STW):更新 region 的统计数据,对各个 region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划;

        G1 收集器在后台维护了一个优先级列表,优先选择回收价值最大的 region,在有限时间内获取更高的回收效率;

        Collection Set(CSet):一组可被回收的分区的集合;

        Remembered Set(RSet):记录了其它 region 中的对象到本 region 的引用;RSet 的价值在于垃圾收集器不需要扫描整个堆,就能找到谁引用了当前分区中的对象,只需要扫描 RSet 即可;

4.8. 解释一下垃圾收集器底层三色标记算法(颜色指针:colored pointer)

        在并发标记的过程中,因为用户线程还在继续运行,对象间的引用可能发生变化,多标漏标的情况就有可能发生。漏标的问题主要引入了三色标记算法来解决;三色标记算法把 Gc Roots 可达性分析遍历过程中遇到的对象,按照"是否访问过"这个条件标记成以下三种颜色:

  1. 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经被扫描。黑色的对象代表已经被扫描,它是安全存活的,如果有其它对象引用指向了黑色对象,无须重新扫描。黑色对象不可能直接(不经过灰色对象)指向某个白色对象;
  2. 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描;
  3. 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始时,所有的对象都是白色的,若在分析结束时,仍然是白色的对象,即代表不可达;

漏标发生的条件:黑色对象指向了白色对象,灰色对象指向白色对象的引用没了;

4.9. 解释下对象漏标的处理方案增量更新与原始快照

        漏标会导致被引用的对象被当成垃圾误删,有两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB);

        增量更新就是指新增加黑色对象指向白色对象的引用时,就将这个新插入的引用记录下来,在并发扫描结束之后,再以这些引用关系中的黑色对象为根,重新扫描一次。简化理解为:黑色对象一旦有引用指向白色对象时,白色对象就变回灰色对象了

        原始快照就是当灰色对象要删除指向白色对象的引用时,就将这个要删除的引用记录下来,在并发扫描结束之后,再以这些引用关系中的灰色对象为根,重新扫描一次,这样就能扫描到白色对象,将白色对象直接标记为黑色(目的就是让这种对象在本次 GC 清理中能存活下来,待下一轮 GC 时重新扫描,这个对象也有可能是浮动垃圾);

        CMS 漏标处理方式:增量更新;G1 漏标处理方式:原始快照;

5. 类加载过程

        类加载过程就是将类的二进制数据读入内存,并创建出 Java 类的过程。类加载过程分为三步:

1. 加载(loading):

  • 通过类的全限定名获取定义此类的二进制字节流;
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 在内存中生成一个代表这个类的 Class 对象;

2. 链接(linking)包含三步:

  • 验证(verify):确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全;
  • 准备(prepare):为类变量分配内存并设置该类变量的默认初始值(零值);
  • 解析(resolve):将常量池内的符号引用转换为直接引用的过程;

3. 初始化(initialization):调用类构造器的过程;

6. 类加载器

        类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。每个 Java 类都有一个引用指向加载它的 ClassLoader。数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的;

        简单来说,类加载器的主要作用就是加载 Java 类的字节码到 JVM 中(在内存中生成一个代表该类的 Class 对象);

        类加载器可以分为四种:

  • BootstrapClassLoader(启动类加载器) :最顶层的加载器,由 C++实现,通常为 null,主要用来加载 Java 核心类库;
  • ExtensionClassLoader(扩展类加载器) :主要负责加载 Java 的扩展库;
  • AppClassLoader(应用程序类加载器) :面向用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类;
  • 自定义类加载器:需要继承 ClassLoader 类;

7. 双亲委派模型

7.1. 什么是双亲委派模型

        双亲委派模型是 Java 中的一种类加载机制,它规定了在父子类加载器之间的工作分配方式。根据该模型,当一个类加载器被要求加载某个类时,它首先会将这个任务委托给它的父类加载器去完成,只有在父类加载器无法完成这个任务时,子类加载器才会尝试去加载这个类;

7.2. 双亲委派模型的执行流程

  • 在类加载时,首先会判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载;
  • 类加载器在进行类加载时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。这样的话,所有的请求最终都会传到顶层的启动类加载器中;
  • 只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试去加载;

7.3. 双亲委派模型的好处

        双亲委派模型可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改;

        如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行时,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 Jre 里的那个 Object 类,而不是你写的 Object 类。这是因为应用程序类加载器在加载你写的 Object 类时,会委托给扩展类加载器去加载,而扩展类加载器又会委托给启动类加载器启动类加载器发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类;

7.4. 打破双亲委派模型方法

        如果想打破双亲委派模型则需要重写 loadClass() 方法。为什么是重写 loadClass() 方法打破双亲委派模型呢?双亲委派模型的执行流程已经解释了:类加载器在进行类加载时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass() 方法来加载);

        我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其它目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委派机制。这也是 Tomcat 下 Web 应用之间,类实现隔离的原理;

8. 什么是 JVM 内存模型

        JVM 内存模型是 Java 虚拟机在运行时对内存管理方式的规范。它将内存分为不同的区域,包括方法区、堆、栈、本地方法栈和程序计数器等。每个区域都有不同的作用和特点,比如用于存储对象实例和数组,用于存储局部变量和方法调用,方法区用于存储类信息、常量、静态变量等,程序计数器用于记录当前线程执行的位置,本地方法栈用于存储本地方法(方法被 native 修饰)的调用和执行信息;

        JVM 内存模型还定义了多线程访问内存时的原子性、可见性和有序性等规则,以确保多线程同时访问共享资源时的正确性和稳定性;

        补充:栈、本地方法栈和程序计数器是线程私有的,而堆和方法区是公有的;

        栈中存放的是栈帧,一个方法对应一块栈帧内存区域;栈帧中包括:局部变量表、操作数栈、动态链接和方法出口;

9. JVM 调优

JVM 调优真正目的:减少 STW;

JVM 为什么要设计 STW 机制:如果没有 STW,在 GC 的过程中,一个对象的状态(是否是垃圾对象)会时刻发生改变;

吞吐量:用户代码执行时间 / (用户代码执行时间 + 垃圾收集执行时间);

响应时间:STW 越短,响应时间越好;

吞吐量优先:一般垃圾收集器选用 PS + PO(Parallel Scavenge + Parallel Old);

响应时间优先:G1;

9.1. 系统 CPU 经常 100%,如何调优

  1. 先通过 top 命令找到消耗 cpu 很高的进程 id;
  2. 再使用 top -p 命令单独监控消耗 cpu 很高的进程 id,然后按 H 键,找到消耗 cpu 很高的线程 ID;
  3. 对当前进程做 jstack,导出所有的堆栈信息;
  4. 将第 2 步得到的线程 ID 转成 16 进制;
  5. 根据得到的 16 进制的线程 ID 找到堆栈的具体信息;
  6. 解读堆栈信息,定位问题及代码位置;

9.2. 常用参数

-Xmn:堆内新生代的大小;

-Xms:堆内存的初始大小;

-Xmx:堆内存的最大大小;

top -Hp 进程号:用于观察进程中的线程,哪个线程 CPU 和内存占比高;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值