Java 八股文:JVM 篇

6 - JVM 篇

6.1 JVM 组成

6.1.1 JVM 组成 / Java 架构的核心组件?
  • 类加载器: 负责加载类的字节码,分为启动类加载器、扩展类加载器和应用类加载器。
  • 内存模型: 管理 JVM 内存的分配和回收。 栈: 每个线程有独立的栈,存储局部变量和方法调用信息。(非静态变量)堆: 存储所有的对象实例,GC 主要在此区域进行。(非静态变量)方法区: 存储类信息、常量、静态变量等数据。(静态变量)程序计数器: PC 是线程私有的,用于记录正在执行的字节码指令的地址。本地方法栈: 用于存储本地方法的信息。
  • 执行引擎: 负责执行字节码,包括解释执行和即时编译 (JIT)。 垃圾回收器: 自动管理内存,回收不再使用的对象。

6.1.2 介绍一下堆?
  • 堆主要用来保存对象实例,数组等,内存不够则抛出 OutOfMemoryError 异常。
  • 组成:年轻代 + 老年代 年轻代:Eden区、两个Survivor区。如果对象在Survivor区中经历了多次GC仍然存活,会被晋升到老年代。老年代:用于存放长期存活的对象。这些对象在年轻代中经历了多次垃圾收集后仍然存在,被认为生命周期较长。
  • jdk1.7 和 1.8 的区别: 1.7 中有一个永久代,存储的是类信息、静态变量、常量、编译后的代码1.8 移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出
6.1.3 什么是虚拟机栈?
  • 虚拟机栈是JVM运行时数据区的一部分,它描述的是JVM栈的内存模型。
  • 每个线程 在创建时都会创建一个虚拟机栈,其内部包含了多个 栈帧(Stack Frame),每个栈帧对应一个方法的调用。
  • 栈帧中存储了局部变量表、操作数栈、动态链接信息和方法返回地址等。 局部变量表:存储方法的参数和局部变量。操作数栈:用于存储操作数,支持方法中字节码指令的操作。动态链接:用于确定方法调用时的动态连接,即方法的具体实现。方法返回地址:存储方法执行完毕后的返回地址。
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 命令查看进程中哪些线程出现了问题,最终定位问题。
#java##八股文##JVM##笔记#
全部评论

相关推荐

01-04 16:28
厦门大学 法务
点赞 评论 收藏
分享
评论
4
2
分享
牛客网
牛客企业服务