侧边栏壁纸
博主头像
5faith分享栈

憧憬未来

  • 累计撰写 7 篇文章
  • 累计创建 13 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

【面试题】JVM部分[2025/1/13 ~ 2025/1/19]

faith5
2025-01-20 / 0 评论 / 0 点赞 / 7 阅读 / 0 字

1. JVM 由哪些部分组成?

  1. 组成
    • 类加载器子系统(ClassLoader)
    • 运行时数据区(Runtime Data Area)
    • 执行引擎(Execution Engine)
    • 本地方法接口(Native Interface,JNI)
  2. 执行流程
    首先需要准备编译好的 Java 字节码文件(即class文件)。
    然后需要先通过一定方式(类加载器)将 class 文件加载到内存中(运行时数据区)。
    又因为字节码文件是 JVM 定义的一套指令集规范,底层操作系统无法直接执行。
    因此需要特定的命令解释器(执行引擎)将字节码翻译成特定的操作系统指令集交给 CPU 去执行。
    这个过程中会需要调用到一些不同语言为 Java 提供的接口(例如驱动、地图制作等),这就用到了本地方法接口(Native Interface)。

2. Java 的类加载过程是怎样的?

  1. 加载: 将二进制流读入内存中,为了生成一个 Class 对象。
  2. 连接
    a. 验证: 主要是验证加载进来的二进制流是否符合一定格式,是否规范,是否符合当前 JVM 版本等等之类的验证。
    b. 准备: 为静态变量(类变量)赋初始值,也即为它们在方法区划分内存空间。这里注意是静态变量,并且是初始值,比如 int 的初始值是 0。
    c. 解析: 将常量池的符号引用转化成直接引用

    符号引用可以理解为只是个替代的标签,比如你此时要做一个计划,暂时还没有人选,你设定了个 A 去做这个事。然后等计划真的要落地的时候肯定要找到确定的人选,到时候就是小明去做一件事。解析就是把 A(符号引用) 替换成小明(直接引用)。符号引用就是一个字面量,没有什么实质性的意义,只是一个代表。
    直接引用指的是一个真实引用,在内存中可以通过这个引用查找到目标。

  3. 初始化: 这时候就执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。

3. 请你介绍下 JVM 内存模型,分为哪些区域?各区域的作用是什么?

  1. 程序计数器(Program Counter Register):每个线程都有自己的程序计数器,用于指示当前线程执行的字节码指令的行号,以便线程执行时能够回到正确的位置。
  2. 虚拟机栈(JVM Stack):也称为 Java 方法栈,用于存储方法执行时的局部变量表、操作数栈、动态链接、方法出口等信息。每个线程在执行一个方法时,都会为该方法分配一个栈帧,并将该栈帧压入虚拟机栈,当方法执行完毕后,虚拟机会将其出栈。
  3. 本地方法栈(Native Method Stack):与虚拟机栈类似,用于存储本地方法的执行信息。
  4. (Heap):用于存放所有线程共享的对象和数组,是垃圾回收的主要区域。
  5. 方法区(Method Area):用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也是被所有线程共享的。方法区具体实现永久代/元空间

程序计数器、虚拟机栈、本地方法栈是线程私有的,堆、方法区、运行时常量池是线程共享的。

4. JVM 垃圾回收调优的主要目标是什么?

GC 调优的核心思路就是尽可能的使对象在年轻代被回收,减少对象进入老年代。

  1. 吞吐量调优:主要关注降低垃圾回收的总时间,通过 Parallel Scavenge 和 Parallel Old 提高 CPU 使用效率。
  2. 延迟调优:关注最大停顿时间,通过 CMS、G1、ZGC 等收集器降低 STW 停顿时间。
  3. 堆大小调优:通过合理的堆内存分配和分代比例调优,避免频繁的 Minor GC 和 Full GC。

5. 如何对 Java 的垃圾回收进行调优?

  1. 思路
    分析Young GC 和 Full GC 触发频率、原因、晋升的速率、老年代内存占用量, 可以使用命令(jstat -gc [pid] [频率] [总次数])或监控页面查看
    例如: 如果发现频繁会产生 Full GC,分析日志之后发现没有内存泄漏,只是 Young GC 之后会有大量的对象进入老年代,然后最终触发 Ful GC。所以就能得知是 Survivor 空间设置太小,导致对象过早进入老年代,因此调大 Survivor 。

6. 常用的 JVM 配置参数有哪些?

  • -Xms:初始化堆内存大小
  • -Xmx:最大堆内存大小
  • -Xss:设置每个线程的栈大小
  • -XX:MetaspaceSize:初始化元空间大小
  • -XX:MaxMetaspaceSize:最大元空间大小
  • -XX:+HeapDumpOnOutOfMemoryError:当发生 OutOfMemoryError 时,生成堆转储(heap dump)文件
  • -XX:HeapDumpPath: 生成堆转储文件路径
  • -XX:+PrintGCDetails:打印详细的垃圾回收日志
  • -XX:+UseG1GC:启用G1垃圾收集器
  • -XX:+UseConcMarkSweepGC:启用CMS垃圾收集器
  • -XX:+UseZGC:启用ZGC(低延迟垃圾收集器)

7. Java 中常见的垃圾收集器有哪些?

  1. 新生代垃圾收集器
    • 1)Serial 收集器:
      • 单线程收集器,适合小型应用和单处理器环境。
      • 触发 Stop-The-World(STW)操作,所有应用线程在 GC 时暂停。
      • 适用场景:适用于单线程应用和客户端模式。
    • 2)ParNew 收集器:
      • 是 Serial 收集器的多线程版本,能够并行进行垃圾收集。
      • 与 CMS 收集器配合使用时,通常会选择 ParNew 收集器作为新生代收集器。
      • 适用场景:适用于多处理器环境,通常配合 CMS 收集器使用。
    • 3)Parallel Scavenge 收集器(吞吐量优先):
      • 也称为 "吞吐量收集器",追求最大化 CPU 时间的利用率。
      • 并行处理新生代垃圾回收,适合大规模后台任务处理,注重吞吐量而非延迟。
      • 适用场景:适用于大规模运算密集型后台任务,适合对吞吐量要求较高的场景。
  2. 老年代垃圾收集器:
    • 1)Serial Old 收集器:
      • Serial 收集器的老年代版本,使用标记-整理(Mark-Compact)算法进行垃圾回收。
      • 适用场景:适合单线程环境和低内存使用场景,通常配合 Serial 收集器一起使用。
    • 2)Parallel Old 收集器:
      • Parallel Scavenge 收集器的老年代版本,使用多线程并行标记-整理算法。
      • 适用场景:适合大规模并行计算的场景,适用于高吞吐量要求的任务。
    • 3)CMS(Concurrent Mark-Sweep)收集器:
      • 并发标记-清除收集器,追求低延迟,减少 GC 停顿时间。
      • 使用并发标记和清除算法,适合对响应时间有较高要求的应用。
      • 缺点:可能会产生内存碎片,并且在并发阶段可能会发生 Concurrent Mode Failure,导致 Full GC。
      • 适用场景:适用于对响应时间要求高的应用,如 Web 服务和电商平台。
    • 4)G1(Garbage First)收集器:
      • 年轻代采用类似于传统的复制算法, 老年代采用标记整理算法
      • 设计用于取代 CMS 的低延迟垃圾收集器,能够提供可预测的停顿时间。
      • 通过分区来管理内存,并在垃圾收集时优先处理最有价值的区域,避免了 CMS 的内存碎片问题。
      • 适用场景:适合大内存、多 CPU 服务器应用,尤其在延迟和响应时间敏感的场景中表现出色。
    • 5)ZGC(Z Garbage Collector)收集器:
      • 低停顿、高吞吐量的垃圾收集器,停顿时间一般不会超过 10 毫秒。
      • 适用场景:适用于需要管理大堆内存且对低延迟要求极高的应用。

8. 垃圾收集算法

  1. 标记-清除算法(Mark-Sweep):
    • 主要用于 CMS,标记存活对象后,清除不可达对象,但容易产生内存碎片。
    • 工作原理:首先遍历堆中的对象,标记出所有的存活对象,接着清除未标记的对象。
    • 优点:实现简单,能够处理堆中的所有对象。
    • 缺点:标记和清除的过程会产生内存碎片,影响后续内存分配的效率。
  2. 标记-整理算法(Mark-Compact):
    • 用于 G1 和 Parallel Old,标记存活对象后进行整理,避免内存碎片。
    • 工作原理:首先标记出所有存活的对象,然后将存活的对象整理到一边,最后清除未标记的对象。
    • 优点:避免了内存碎片问题。
    • 缺点:整理阶段需要移动对象,会导致额外的开销。
  3. 复制算法(Copying):
    • 工作原理:将内存分成两部分,每次只使用其中一半,垃圾回收时将存活的对象从一半复制到另一半,清除原区域的所有对象(朴素的复制算法是这样的,实际使用会分为两个 survivor 和一个 eden 区)。
    • 优点:无需处理内存碎片,分配效率高。
    • 缺点:需要双倍的内存空间,浪费了一半的空间。

9. 线上 CPU 飙高如何排查?

  1. 首先确认哪个进程占用 CPU 过高,登录服务器利用 top 命令查看。
    top 命令是 Linux 下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于 Windows 的任务管理器
  2. 确认 CPU 利用率很高的进程的 PID,假设为 1234 确实是 Java 进程,则通过 top -Hp 1234 查看具体的线程。
  3. 假设得到的线程 ID 是 5678,再将线程转为十六进制。可以使用 printf "%x\n" 5678
  4. 得到十六进制的 tid 为 162e,此时在利用 jstack 1234 | grep 162e -A 100 查看具体的栈信息。
  5. 根据堆栈信息就可以定位到具体是哪行代码导致了 CPU 飙高,对应分析修复即可!

10. 怎么分析 JVM 当前的内存占用情况?

  1. 利用 jstat 监控和分析 JVM 内部的垃圾回收、内存等运行状态。可以用它来查看堆内存、非堆内存等的实时状态。使用命令 jstat -gc [pid] [频率] [总次数]
  2. 可以使用 jmap 查看, JVM 堆的详细信息(包括堆的配置、内存使用情况、GC 活动等)。\
  3. 阿里 arthas 使用
    • dashboard: 可以查看当前系统的实时数据面板。数据面板默认5秒刷新一次。可以 输入Q 后回车 或 Ctrl+C 退出dashboard命令。
    • thread: 后面加上线程ID会打印线程的栈。Arthas支持管道,可以用 thread 1 | grep ‘main(’ 查找到arthas-demo应用的main class。
    • sc: 用来查找JVM里已加载的类。例如查看名称为MathGame的类:sc -d *MathGame
    • jad: 用来反编译代码 。例如查看MathGame类的源代码:jad demo.MathGame
    • watch: 可以查看函数的参数/返回值/异常信息。例如查看MathGame类中primeFactors函数的返回值:watch demo.MathGame primeFactors returnObj ,每次函数被调用都会打印返回值,可以 输入Q 后回车 或 Ctrl+C 退出watch命令。

11. OOM 后怎么分析?

在发生 OOM 时,可以根据 jmap 得到堆转储文件(建议增加JVM启动参数,-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,在发生 OOM 后自动生成转储文件),再导入到 MAT、VisualVM、GCeasy等工具中分析文件,找出哪些对象占用了大量的内存,再定位到具体的代码解决问题。

OOM内存泄露常见问题:

  1. 堆内存溢出(Java Heap Space)
    • Java 堆用于存放对象实例,如果创建了过多对象,或有内存泄漏导致对象无法被垃圾回收,堆内存就会耗尽。 如果有大量创建对象或集合类的场景,持续增加数据但未释放就会产生堆内存溢出。
    • 错误信息:java.lang.OutOfMemoryError: Java heap space
    • 解决方法:检查对象创建逻辑,确保及时释放无用对象,或增大堆内存大小(-Xmx 参数)。
  2. 栈内存溢出(StackOverflowError)
    • 每个线程都有独立的栈空间,栈用于存储方法调用的信息(局部变量、方法参数、返回地址等)。如果方法调用层次过深或存在无限递归,栈空间耗尽就会导致栈溢出。 常见于递归方法没有正确的退出条件、深层嵌套的方法调用场景。
    • 错误信息:java.lang.StackOverflowError
    • 解决方法:检查递归条件,优化递归算法或增加栈空间(-Xss 参数)。
  3. 方法区或元空间溢出(Metaspace / PermGen space)
    • 在 Java 8 之前,方法区被实现为永久代(PermGen),用于存放类的元数据(类信息、方法信息、常量池等)。在 Java 8 之后,永久代被替换为元空间(Metaspace),用本地内存实现。在频繁加载和卸载类的情况下(如使用大量动态生成的代理类或频繁热部署),可能导致方法区或元空间溢出。常见于使用动态代理频繁生成类、大量反射调用或频繁热部署场景。
    • 错误信息: Java 7 及之前:java.lang.OutOfMemoryError: PermGen space;Java 8 及之后:java.lang.OutOfMemoryError: Metaspace
    • 解决方法:增加元空间大小(-XX:MaxMetaspaceSize);优化代码以减少类加载和反射的频率。
  4. 直接内存溢出(Direct Buffer Memory)
    • Java NIO 使用直接内存(Direct Memory)来加快 I/O 操作,该内存不受 JVM 堆内存的限制。如果分配过多的直接内存,超过了设置的最大值,也会导致内存溢出。 常见于使用 NIO 操作 ByteBuffer 分配大量直接内存,或者 Netty 等框架中频繁使用直接内存场景。
    • 错误信息:java.lang.OutOfMemoryError: Direct buffer memory
    • 解决方法:检查直接内存的分配和释放情况,增加直接内存大小限制(-XX:MaxDirectMemorySize),或避免过多使用直接内存。
  5. 线程数过多导致的内存溢出(Unable to Create New Native Thread)
    • 每个线程都需要栈空间和一定的操作系统资源。如果创建过多线程而超出操作系统的资源限制,可能无法再创建新的线程,导致 OOM。 常见于创建大量线程或线程池大小过大。
    • 错误信息:java.lang.OutOfMemoryError: Unable to create new native thread
    • 解决方法:减少线程数,合理设置线程池的大小,避免无限制地创建新线程。
  6. GC 执行耗时过长导致的 OOM(GC Overhead Limit Exceeded)
    • 当 JVM 在垃圾回收上花费的时间过多且回收的内存不足以满足需要,JVM 会抛出 GC Overhead Limit Exceeded 错误,以避免长时间的垃圾回收循环。通常发生在堆内存接近耗尽但又无法完全释放的情况下。常见于对象频繁创建和销毁导致 GC 频繁触发,内存不足导致 GC 效率低下场景。
    • 错误信息:java.lang.OutOfMemoryError: GC overhead limit exceeded
    • 解决方法:增大堆内存,优化代码以减少短生命周期对象的创建,或调整垃圾回收策略。
0

评论区