Java 八股文:JVM 篇

6 - JVM 篇

6.1 JVM 组成

6.1.1 JVM 组成 / Java 架构的核心组件?
  • 类加载器: 负责加载类的字节码,分为启动类加载器、扩展类加载器和应用类加载器。
  • 内存模型: 管理 JVM 内存的分配和回收。
    1. 栈: 每个线程有独立的栈,存储局部变量和方法调用信息。(非静态变量)
    2. 堆: 存储所有的对象实例,GC 主要在此区域进行。(非静态变量)
    3. 方法区: 存储类信息、常量、静态变量等数据。(静态变量)
    4. 程序计数器: PC 是线程私有的,用于记录正在执行的字节码指令的地址。
    5. 本地方法栈: 用于存储本地方法的信息。
  • 执行引擎: 负责执行字节码,包括解释执行和即时编译 (JIT)。
    • 垃圾回收器: 自动管理内存,回收不再使用的对象。
6.1.2 介绍一下堆?
  • 堆主要用来保存对象实例,数组等,内存不够则抛出 OutOfMemoryError 异常。
  • 组成:年轻代 + 老年代
    • 年轻代:Eden区、两个Survivor区。如果对象在Survivor区中经历了多次GC仍然存活,会被晋升到老年代。
    • 老年代:用于存放长期存活的对象。这些对象在年轻代中经历了多次垃圾收集后仍然存在,被认为生命周期较长。
  • jdk1.7 和 1.8 的区别:
    • 1.7 中有一个永久代,存储的是类信息、静态变量、常量、编译后的代码
    • 1.8 移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出
6.1.3 什么是虚拟机栈?
  • 虚拟机栈是JVM运行时数据区的一部分,它描述的是JVM栈的内存模型。
  • 每个线程 在创建时都会创建一个虚拟机栈,其内部包含了多个 栈帧(Stack Frame),每个栈帧对应一个方法的调用。
  • 栈帧中存储了局部变量表、操作数栈、动态链接信息和方法返回地址等。
    1. 局部变量表:存储方法的参数和局部变量。
    2. 操作数栈:用于存储操作数,支持方法中字节码指令的操作。
    3. 动态链接:用于确定方法调用时的动态连接,即方法的具体实现。
    4. 方法返回地址:存储方法执行完毕后的返回地址。
6.1.4 垃圾回收是否涉及栈内存?

垃圾回收(Garbage Collection, GC)主要涉及的是堆(Heap)内存,而不是栈(Stack)内存。

栈内存主要用于存储方法调用的信息,这些数据在方法执行完毕后会 自然释放,因此不需要垃圾回收。

垃圾回收主要处理的是堆内存中的不再被引用的对象,以释放内存空间。

6.1.5 栈内存分配越大越好吗?

栈内存分配并不是越大越好。虽然增加栈内存可以减少线程因栈溢出而崩溃的风险,

但是会占用更多的物理内存,导致内存资源的浪费,并且增加 线程创建的开销

6.1.6 方法内的局部变量是否线程安全?

方法内的局部变量是线程安全的。每个线程都有自己的虚拟机栈,因此局部变量是线程私有的,不同线程之间不会共享局部变量。

但是,如果局部变量引用了 共享对象,那么对这个共享对象的访问就需要考虑线程安全。

6.1.7 内存溢出(栈溢出、堆溢出)、内存泄漏?
  1. 内存溢出(Out of Memory):内存溢出是一次性的内存分配失败。例:对象的创建太多、递归的调用太深。
    • 指程序在申请内存时,没有足够的空间供其使用,抛出内存溢出异常。最常见的是java.lang.OutOfMemoryError
    • 栈溢出(调用堆栈)和堆溢出(调用堆)都是内存溢出的特殊情况。栈溢出抛出java.lang.StackOverflowError
  2. 内存泄漏(Memory Leak):内存泄漏是长期累积的内存分配问题。例:不再使用的对象没有被垃圾回收。
    • 指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏可能危害不大,但堆积的后果就是内存溢出。
    • 内存泄漏通常是由于疏忽或错误导致程序未能释放不再使用的内存。
6.1.8 JVM 堆和栈的区别?
  1. 栈内存用来存储 局部变量 和 方法调用,而堆内存用来存储 对象 和 数组。
  2. 栈内存是线程 私有的,而堆内存是线程 共有的
  3. 栈不会垃圾回收,而堆会 垃圾回收。 栈溢出StackOverFlowError,堆溢出 OutOfMemoryError
  4. 栈的内存分配和回收速度比堆快得多。栈使用的是连续的内存空间,并且遵循LIFO(后进先出)原则。
  5. 堆的内存分配和回收涉及到更复杂的算法,如标记-清除、复制算法等,这些算法需要额外的时间来管理内存。
6.1.9 介绍一下方法区?

方法区是JVM中的一块内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

方法区是所有线程共享的内存区域,通常也被称为“永久代”,不过在Java 8中,永久代已经被 元空间 所取代。

6.1.10 介绍一下运行时常量池?

运行时常量池是 方法区 的一部分,用于存放编译期生成的各种字面量和符号引用。

这些数据从 Class文件的常量池 中提取出来,并在运行期解析或计算后存储在运行时常量池中。

常量池中的数据包括类和接口的全限定名、字段名称和描述符、方法名称和描述符等。

6.1.11 介绍一下直接内存?

直接内存并 不是 JVM 运行时数据区的一部分,而是指Java程序通过NIO(New Input/Output)类直接申请的堆外内存。

直接内存的申请不受 JVM 堆内存大小的限制,这部分内存的申请和释放需要程序员 手动管理,否则可能导致内存泄漏。

直接内存主要用于 大文件 的读写和网络通信,可以提高性能,因为它减少了JVM堆到本地内存之间的数据复制次数。

6.1.12 String 底层原理?字符串常量池?

1. 常量池

  • 类文件常量池:存储编译期确定的字面量和符号引用。

  • 运行时常量池:存放类文件常量池内容,并可在运行时添加新变量。

  • 字符串常量池:位于堆中,用于存储字符串字面量,以节省内存和避免重复。

    • 在Hotspot JVM中,字符串常量池是一个HashTable,使用数组加链表结构存储。

    • String.intern() 将字符串对象添加到字符串常量池中,并返回常量池中对象的引用。

2. String 的特性

  • String类是不可继承的(final)。
  • value成员是私有的(private)且不可变的(final)。
  • 字符串字面量存储在字符串常量池中,内容不可修改。
  • JDK 1.8及之前,valuechar[]数组存储;JDK 1.9之后,使用byte[]数组存储。

3. String 的创建方式

  • 直接使用字面量赋值会在常量池中存储引用。
  • 使用new关键字会在堆中创建新的String对象。
  • 使用+运算符连接常量时,编译器会优化,直接在常量池中创建结果字符串。

4. String 不可变的原因

  • 保证线程安全。
  • 缓存HashCode值,保证一致性。
  • 提高安全性,防止字符串被恶意修改。

6.2 类加载器

6.2.1 什么是类加载器,类加载器有哪些?

JVM 只会运行 二进制 文件,类加载器的作用是将 字节码 文件加载到 JVM 中,从而让 Java 程序能够启动起来。

  1. 启动类加载器(BootStrap ClassLoader):加载 JAVA_HOME/jre/lib 目录下的库。
  2. 扩展类加载器(ExtClassLoader):加载 JAVA_HOME/jre/lib/ext 目录中的类。
  3. 应用类加载器(AppClassLoader):加载 classPath下的类。
  4. 自定义加载器(CustomizeClassLoader):自定义类继承 ClassLoader,实现自定义加载规则。
6.2.2 什么是双亲委派模型?
  1. 类加载器 - 层次结构
    • 启动类加载器:用C++实现,是虚拟机自带的类加载器。
    • 扩展类加载器:由Java语言实现,继承自ClassLoader类。
    • 应用程序类加载器:也称为系统类加载器,由Java语言实现。
  2. 双亲委派模型 - 原理
    • 当一个类需要被加载时,JVM不会直接委派给应用程序类加载器。
    • 它会先委派给父类加载器去加载,如果父类加载器没有找到这个类,子类加载器才会尝试自己去加载。
  3. 双亲委派模型 - 优点
    • 避免类的多次加载:确保一个类在JVM中只被加载一次。
    • 安全机制:Java核心库的类只能由启动类加载器加载,防止核心库被随意篡改。
    • 隔离机制:不同层次的类加载器加载不同层次的类,例如,用户自定义的类加载器加载用户自定义的类。
  4. 双亲委派模型 - 破坏
    • 热替换:在运行时替换某个类的定义。
    • OSGi环境:需要加载不同版本的同一个类。
    • 容器环境:如Tomcat,需要隔离不同Web应用的类加载。
6.2.3 JVM 为什么采用双亲委派机制?

JVM采用双亲委派机制是为了确保Java核心库的安全性和一致性。这种机制的工作方式如下:

  1. 安全与一致性:可以避免重复加载同一个类,确保了Java核心库的类在各个类加载器中是唯一的。
  2. 避免核心库被篡改:通过这种机制,可以防止用户自定义的类加载器加载核心库中的类,从而保护了Java核心库不被篡改。
  3. 层次性:类加载器之间存在层次关系,从顶层的启动类加载器到用户自定义的类加载器,有助于维护类加载的顺序和隔离性。
6.2.4 类装载的执行过程?
  1. 加载:查找和导入 class 文件。
  2. 验证:保证加载类的准确性。
  3. 准备:为类变量分配内存并设置类变量初始值。
  4. 解析:把类中的符号引用转换为直接引用。
  5. 初始化:对类的静态变量,静态代码块执行初始化操作。
  6. 使用:JVM 开始从入口方法开始执行用户的程序代码。
  7. 卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象。
6.2.5 对象的深拷贝和浅拷贝?
  1. 浅拷贝:
    • 浅拷贝只复制对象的顶层数据,对于对象中的引用类型,仅复制引用的地址,不复制引用的对象
    • 结果是原对象和拷贝对象共享同一个引用对象,修改其中一个对象的引用类型属性会影响另一个对象。
      • 调用clone()方法可以用于对象的浅拷贝。
  2. 深拷贝:
    • 深拷贝会递归复制对象及其所有嵌套对象,创建完全独立的副本
    • 原对象和拷贝对象不共享任何引用对象,修改一个对象不会影响另一个。
      • 实现Cloneable接口并重写clone()方法用于深拷贝。
  3. Java 中实现深拷贝的方法:
    • 序列化和反序列化:通过实现Serializable接口,将对象序列化为字节流,再从字节流反序列化出新对象。
    • 拷贝构造函数和覆写clone方法:手动编写代码递归复制所有属性,注意处理循环引用问题。
6.2.6 Java 源码从编译到执行,发生了什么?
  1. 编译:经过 语法分析、语义分析、注解处理,最后生成class文件。

  2. 加载:又可以细分步骤为:装载 -> 连接(验证->准备->解析)-> 初始化

    装载则把class文件装载至JVM,连接则校验class信息、分配内存空间及赋默认值,初始化则为变量赋值为正确的初始值。

  3. 解释:则是把字节码转换成操作系统可识别的执行指令,在 JVM 中会有字节码解释器和即时编译器。

    在解释时会对代码进行分析,查看是否为热点代码,如果是则触发 JIT 编译,下次执行时就无需重复进行解释,提高速度。

  4. 执行:调用系统的硬件执行最终的程序指令。

6.3 垃圾回收

6.3.1 对象什么时候可以被垃圾器回收?
  1. 没有引用:对象没有任何引用指向它,即它变得不可达。

  2. 可达性分析:通过一系列的“GC Roots”(如线程栈中的局部变量、静态变量等)可达性分析,如果对象不可达,则可被回收。

  3. finalize() 方法:如果对象被判定为可回收,并且对象的finalize()还没有被调用,那么垃圾回收器可能会调用这个方法。

    如果finalize()方法中对象被重新引用,则该对象可以逃过回收。

  4. 弱引用、软引用、虚引用:即使存在弱引用、软引用或虚引用,如果对象没有其他强引用指向它,也可以被回收。

6.3.2 说一下强引用、软引用、弱引用、虚引用?
  1. 强引用 是最常见的引用类型。如果一个对象有强引用指向它,那么即使系统内存紧张,垃圾回收器也不会回收这个对象。

    • Object strongReference = new Object();
  2. 软引用 用于缓存,只有在内存不足时,垃圾回收器才会回收被软引用指向的对象。使用软引用可以减少内存占用。

    • SoftReference<Object> softRef = new SoftReference<>(new Object());
  3. 弱引用 的对象在垃圾回收时,无论内存是否充足,都会被回收。弱引用常用于实现规范化的缓存。

    • WeakReference<Object> weakRef = new WeakReference<>(new Object());
  4. 虚引用 在对象被回收后,虚引用会被加入到引用队列中,允许程序在对象被回收后执行某些清理操作。

    • ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
    • PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), refQueue);
6.3.3 JVM 垃圾回收算法有哪些?
  1. 标记清除: 这种算法首先标记所有活跃的对象,然后清除那些未被标记的对象。

    这个过程可能会导致内存碎片,因为清除是随机的,不是连续的。

  2. 标记整理: 这种算法首先标记所有活跃的对象,然后它将这些对象移动到内存的一端紧凑地排列,从而消除了内存碎片。

    这种方法比标记-清除更高效,因为它可以回收更连续的内存块,但可能需要更多的计算资源,因为涉及到对象的移动。

  3. 复制: 在这种算法中,内存被分为两个半区,垃圾回收器将活跃对象从当前半区复制到另一个半区。

    当一个半区被填满时,复制过程开始,所有活跃对象被移动到另一个半区,而原半区则被清空。但内存使用效率较低。

6.3.4 说一下 JVM 中的分代回收?

JVM中的分代回收是一种垃圾回收策略,它基于对象的生命周期将堆内存分为不同的区域:

  1. 新生代(Young Generation):新创建的对象首先被分配到这里,新生代又分为Eden区和两个Survivor区(S0和S1)。

    新生代的垃圾回收频繁,因为大部分对象的生命周期都很短。

  2. 老年代(Old Generation):在新生代中经过多次垃圾回收后仍然存活的对象会被移动到老年代。

    老年代的垃圾回收不如新生代频繁,因为对象的生命周期较长。

  3. 元空间(Metaspace):用于存储类的元数据,取代了永久代。

MinorGC、 Mixed GC 、 FullGC 的区别是什么?

  1. Minor GC:只发生在新生代的垃圾回收,回收的是新生代中的对象。

    因为新生代对象的生命周期短,所以Minor GC发生的频率较高,回收速度也较快。

  2. Mixed GC:同时发生在新生代和老年代的垃圾回收,但是不涉及元空间。

    Mixed GC在某些垃圾回收器中被用来回收老年代中的一部分空间,以减少Full GC的发生。

  3. Full GC:涉及整个堆内存(包括新生代、老年代和元空间)的垃圾回收。

    Full GC发生的频率较低,但是回收时间长,因为需要检查整个堆内存中的对象。

6.3.5 JVM 垃圾回收器有哪些?CMS 回收过程?

1. JVM 垃圾回收器:

  • Serial GC:这是最基本的垃圾回收器,使用单线程进行垃圾回收,适合客户端应用。
  • Parallel GC:也称为吞吐量优先收集器,使用多线程进行垃圾回收,适合多处理器机器。
  • CMS (Concurrent Mark-Sweep) GC:通过并发方式进行标记和清除,减少应用程序的停顿时间,主要针对老年代。
  • G1 (Garbage-First) GC:这是服务器端的垃圾收集器,旨在提高吞吐量的同时,尽可能满足垃圾收集暂停时间的要求。

2. CMS 回收过程:

  1. 初始标记:这个阶段会 STW(Stop-The-World),标记所有从GC Roots直接可达的对象。
  2. 并发标记:在这个阶段,GC线程与应用程序线程并发运行,标记所有从GC Roots可达的对象。
  3. 重新标记:由于并发标记阶段应用程序仍在运行,可能会产生新的垃圾,因此短暂的STW来修正这些遗漏的对象。
  4. 并发清除:最后,清除所有标记为可回收的对象,这个阶段也是并发执行的,不会暂停应用程序。
6.3.6 G1 垃圾收集器?它是如何改善性能的?

1. G1 垃圾收集器:

  1. 区域划分:G1将堆内存划分为 Eden区、Survivor区、Old区或大对象区,这样可以更加灵活地管理内存。
  2. 复制算法:G1在新生代回收时使用复制算法,这样可以减少内存碎片,并且提高回收效率。
  3. 性能兼顾:G1试图平衡停顿时间和吞吐量。它通过预测算法来决定哪些区域的回收收益最高,从而优先回收这些区域。
  4. 回收阶段:新生代回收、并发标记、混合回收(新生代 + 部分老年代)。
  5. 并发失败:如果并发标记阶段发现回收速度跟不上新建速度,G1可能触发Full GC来清理整个堆内存,以避免内存耗尽。

2. G1 如何改善性能:

  1. 减少停顿时间:通过优先收集垃圾最多的Region,G1减少了每次垃圾回收的停顿时间。
  2. 提高响应性:G1的并发和增量式收集减少了应用程序的Stop-The-World事件。
  3. 适应性:G1可以根据应用程序的行为动态调整垃圾回收的策略,以适应不同的工作负载。
  4. 减少内存碎片:G1在回收过程中会进行内存压缩,减少了内存碎片,有助于提高内存分配的效率。
  5. 避免Full GC:G1通过有效的Region管理和垃圾回收策略,减少了Full GC的发生,从而避免了长时间的垃圾回收停顿。

6.4 JVM 实践

6.4.1 JVM 调优的参数可以在哪里设置?
  1. 启动参数:通过在JVM启动时传递参数来设置,例如使用-Xms-Xmx来设置堆的初始大小和最大大小。
  2. JVM监控工具:使用JVM监控工具(如JConsole、VisualVM)可以在运行时调整某些参数。
  3. JVM配置文件:如jvm.optionsjvm.cfg文件,这些文件中可以包含JVM启动参数。
  4. 代码中动态设置:在Java代码中可以通过System.setProperty()方法动态设置某些JVM参数。
6.4.2 JVM 调优的参数都有哪些?
  1. 设置堆空间大小:-Xms-Xmx 参数用于设置JVM堆的初始大小和最大大小。
  2. 虚拟机栈的设置:-Xss 参数用于设置每个线程的堆栈大小。
  3. 年轻代的大小比例:-XX:NewRatio 参数用于设置年轻代和老年代的比率。
  4. 晋升老年代的阈值:-XX:MaxTenuringThreshold 参数用于设置对象在年轻代中经过多少次垃圾回收后晋升到老年代。
  5. 设置垃圾回收器:-XX:+UseG1GC 参数用于启用G1垃圾回收器。
6.4.3 JVM 调优的工具?
  1. 命令工具:

    • jps:进程状态信息
    • jstack:查看进程内线程的堆栈信息
    • jmap:生成堆转储快照(heap dump)
  2. 可视化工具:

    • jconsole:监控 jvm 的内存,线程,类的情况
    • VisualVM:监控线程,内存情况
6.4.4 Java 内存溢出、内存泄漏的排查与定位?

1. 栈内存溢出:栈帧过多导致栈内存溢出;栈帧过大导致栈内存溢出。

  1. top 命令:定位是哪一个进程对 CPU 的占用过高
  2. ps 命令:结合 grep命令,定位是哪个线程引起的
  3. jstack 命令获取线程堆栈信息,再根据 线程id 找到有问题的线程,定位是哪一行代码引起的

2. 堆内存溢出:程序分配的内存超出可用堆内存时,导致应用程序崩溃或异常

  1. jps 命令:查看当前系统中有哪些 java 进程
  2. jmap 命令:查看堆内存占用情况 jmap -heap 进程id
  3. jconsole 工具:图形界面的,多功能的监测工具,可以连续监测

3. 内存泄漏:通常是指堆内存,指一些大对象不被回收的情况。

  1. jmap 命令:获取堆内存快照 dump。
  2. VisualVM 工具:加载 dump,查看堆信息的情况,定位是哪行代码出了问题。
  3. 通过阅读代码上下文的情况,进行修复即可。
6.4.5 CPU 飙高排查方案与思路?
  1. 通过 top 命令查看是哪一个进程占用 cpu 较高。
  2. 使用 ps 命令查看进程中的线程信息。
  3. 使用 jstack 命令查看进程中哪些线程出现了问题,最终定位问题。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容