JVM(Java Virtual Machine)
知识点
1. JVM 内存结构:
JVM 的内存结构大致分为以下几个区域:
- 方法区(Method Area):用于存储类信息、常量、静态变量和即时编译器编译后的代码。JVM 规范要求方法区是一个共享内存区域,各个线程都可以访问。
- 堆区(Heap):用于存储所有的对象实例(类实例)。堆是垃圾回收(GC)的主要区域,GC 会对堆中的对象进行内存回收。
- 栈区(Stack):每个线程都会有一个独立的栈,用于存储局部变量、方法调用、返回地址等信息。每当方法被调用时,栈会分配一个栈帧,栈帧中存储方法的局部变量、操作数栈等。
- 程序计数器(Program Counter Register):指向当前线程执行的字节码指令的地址。每个线程都有独立的程序计数器。
- 本地方法栈(Native Method Stack):为 JNI(Java Native Interface)调用提供服务,存储 Java 调用本地方法时的参数和返回值。
2. JVM 生命周期:
JVM 生命周期大致包括以下几个阶段:
- 启动阶段:启动 JVM 时,加载 Java 类,初始化 JVM 环境。
- 加载阶段:JVM 启动后,首先加载用户指定的主类。
- 执行阶段:JVM 执行主类中的
main方法,并开始解释执行字节码,或者在有 JIT 编译器时进行编译。 - 结束阶段:当
main方法执行完毕或 JVM 发生异常终止时,JVM 结束运行,释放资源。
3. 主流虚拟机:
主流的 JVM 实现包括:
- HotSpot JVM:Oracle 提供的官方 JVM,实现了大部分 JVM 规范,支持 JIT 编译。
- OpenJ9 JVM:由 IBM 开发的 JVM,支持开源和商用,性能优化针对特定场景(如云计算环境)。
- GraalVM:是一个多语言虚拟机,可以执行 Java、JavaScript、Ruby 等多种语言的代码,支持高效的 JIT 编译和 AOT 编译。
- Zulu/OpenJDK:是 Oracle JDK 的开源实现,主要用于在 Linux、Windows 和 macOS 上运行 Java 应用。
4. Java 代码执行流程:
- 编译阶段:Java 源代码(.java 文件)通过
javac编译器编译成字节码(.class 文件)。 - 类加载:字节码通过类加载器加载到内存中。
- 字节码解释:JVM 的解释器将字节码转换为平台相关的机器码进行执行。
- JIT 编译:JVM 通过 JIT 编译将热点代码(经常执行的代码)编译为本地机器码,优化执行效率。
5. 类加载与类加载器:
- 类加载器是 JVM 中用来加载类的组件。它负责将字节码文件加载到 JVM 中,并将它们转化为类对象,供后续使用。
- 类加载过程:类加载过程包括以下几个步骤:
- 加载:通过类加载器将类的字节码加载到内存。
- 验证:验证加载的字节码是否符合 JVM 的规范。
- 准备:为类的静态变量分配内存,并设定初始值。
- 解析:解析类中的符号引用,转化为直接引用。
- 初始化:执行类的初始化代码(如静态代码块)。
- 双亲委派机制:JVM 中的类加载器采用双亲委派模型,即当一个类加载器收到加载请求时,它会先委托给父类加载器去加载,只有在父类加载器无法加载时,子类加载器才会自己加载。这样可以避免重复加载和类冲突。
6. 垃圾回收与垃圾回收策略:
- 垃圾回收器:负责回收 JVM 堆中不再使用的对象,释放内存。常见的垃圾回收器有:
- Serial GC:单线程垃圾回收器,适用于单核处理器。
- Parallel GC:多线程垃圾回收器,适用于多核处理器。
- G1 GC:用于低延迟和大堆内存的场景,适合大规模的应用。
- 垃圾回收策略:JVM 使用不同的垃圾回收策略,通常根据堆的大小、对象的生命周期以及应用的要求来选择。
- 垃圾回收算法:
- 标记-清除算法:标记所有需要回收的对象,然后清除这些对象。
- 复制算法:将堆分为两个区域,每次回收时将存活的对象复制到另一区域。
- 标记-压缩算法:标记需要回收的对象并进行压缩,避免产生内存碎片。
- 分代回收:根据对象的存活时间,将对象分为不同的代,分别进行回收。
- Stop-the-World:是指垃圾回收过程中,所有应用线程被暂停,直到垃圾回收完成。
7. 字节码:
字节码是 Java 源代码编译后的中间代码,它是与平台无关的。JVM 通过解释执行或 JIT 编译将字节码转化为机器码,进而执行。
8. 内存分配与回收:
- 堆内存分配:JVM 会将内存分配给对象实例,堆是内存分配的主要区域。
- 栈内存分配:每个线程都会有一个栈,用于存储方法调用和局部变量。栈内存一般是线程私有的。
9. JVM 性能调优:
- 性能分析方法:通过监控 JVM 的运行状况(如 GC 日志、内存使用情况)来分析性能瓶颈。
- 常用工具:
- jconsole:用于监控和管理 JVM。
- VisualVM:可视化的 JVM 监控工具,适合性能调优。
- GC Logs:查看垃圾回收的日志,分析垃圾回收的效率和停顿时间。
- 参数设置:通过 JVM 启动参数来调节堆大小、GC 策略等参数,优化性能。例如,
-Xmx设置最大堆大小,-XX:+UseG1GC启用 G1 垃圾回收器。 - Java 探针:用于在生产环境中动态监控 JVM 的健康状态。
- 线上故障分析:在生产环境中,通过 JVM 日志和性能数据分析故障,找出性能瓶颈或异常问题。
问题
1. JVM 内存模型(JMM):
JVM 内存模型(Java Memory Model,简称 JMM)是 Java 并发编程的核心概念之一,它定义了不同线程之间如何共享内存,并且规定了内存操作的顺序,以保证多线程并发程序的正确性。JMM 主要涉及以下几个方面:
JMM 的目标
JMM 的目标是提供一个统一的规则,确保多线程环境下,线程之间的数据交换和同步不会引发竞态条件、内存可见性问题和指令重排问题。通过合理的内存模型,Java 能够保证并发程序的可靠性和一致性。
JMM 的主要概念
- 主内存(Main Memory)和工作内存(Working Memory):
- 主内存:每个线程的共享内存区域,存储着所有变量的值。所有的实例字段、静态字段和数组元素都存储在主内存中。
- 工作内存:每个线程有自己的工作内存,工作内存中存放的是从主内存中拷贝过来的变量的副本。线程对共享变量的操作通常是在工作内存中进行的,然后再同步回主内存。
- 内存可见性(Visibility):
- 线程的工作内存中的变量修改对其他线程是否可见是一个重要问题。在 JMM 中,变量的修改是否能被其他线程看到,取决于同步操作。比如,在没有适当同步的情况下,一个线程对变量的修改可能永远不会被其他线程看到。
- 同步关键字(synchronized)、volatile 关键字以及 锁 等都可以确保内存可见性。
- 有序性(Ordering):
- 由于编译器、处理器等优化技术,程序中操作的执行顺序可能会与代码中的顺序不同,这被称为指令重排。JMM 通过保证一定的规则来控制指令的执行顺序,以保证程序的正确性。
- happens-before 规则:JMM 定义了线程间的执行顺序,通过一些同步机制来保证某些操作先于其他操作执行。比如,
synchronized和volatile就提供了 happens-before 保证,确保某些操作的先后顺序。
- 原子性(Atomicity):
- JMM 还保证了对某些操作的原子性。在 JMM 中,简单的读写操作是原子的,但是像复合操作(例如:
i++)这样的操作不是原子性的,可能会导致并发问题。
- JMM 还保证了对某些操作的原子性。在 JMM 中,简单的读写操作是原子的,但是像复合操作(例如:
JMM 中的同步机制
为了保证线程间的内存可见性和操作顺序,Java 提供了几种同步机制:
- synchronized:Java 的
synchronized关键字可以用于方法或代码块,确保同一时刻只有一个线程能执行被同步修饰的代码块,并且同步块中的变量对其他线程是可见的。 - volatile:
volatile关键字确保每次访问变量时都直接从主内存中读取,而不是从线程的工作内存中读取。它提供了轻量级的同步机制,但只保证单个变量的可见性,而不提供原子性或顺序性保证。 - 锁(Lock):Java 通过显式锁(如
ReentrantLock)来提供更多的控制,保证同一时刻只有一个线程能访问某些共享资源。
JMM 的关键问题
-
内存可见性问题:
线程之间的变量修改在不同的工作内存中不会立刻同步到主内存,可能导致其他线程看不到最新的变量值。这是 JMM 需要解决的一个关键问题。
-
指令重排问题:
为了提高程序执行的效率,编译器和 CPU 可能会将代码中的指令顺序重排,导致多线程程序的执行顺序与程序源代码中的顺序不一致,进而可能出现逻辑错误。JMM 使用 happens-before 规则来保证线程操作的有序性,避免指令重排带来的问题。
-
原子性问题:
多线程环境中对共享资源的操作可能出现竞态条件,导致操作不是原子的。为了保证操作的原子性,Java 提供了同步机制。
JMM 的重要性
理解 JMM 对于编写并发程序至关重要。它帮助开发者理解多线程环境中线程之间是如何共享数据的,以及如何通过同步机制保证程序的正确性。掌握 JMM 能够有效避免线程安全问题和并发 bug。
结论:
JVM 内存模型是 Java 并发编程的基础,它通过对线程间内存共享、操作顺序、内存可见性、指令重排等问题进行详细定义和控制,确保多线程程序的正确性。通过 synchronized、volatile、锁等机制,Java
提供了丰富的工具,帮助开发者在复杂的并发环境中编写高效、可靠的代码。
2.JVM 内存为什么要分代:
JVM 堆内存被分为 年轻代、老年代 和 永久代(或元空间),这是为了优化垃圾回收。年轻代主要用于存放新生对象,老年代存放存活时间较长的对象,垃圾回收可以针对年轻代和老年代进行不同的策略,以提高回收效率。
JVM 的堆内存分代(Generation)是为了优化垃圾回收(GC)过程,提升内存管理的效率。在传统的 Java 程序中,程序的对象生命周期有很大的差异,一些对象可能仅存在短暂的时间,而另一些对象则会长时间存活。因此,将堆内存划分为多个区域,可以使垃圾回收器更加高效地处理这些不同生命周期的对象。
JVM 将堆内存分为以下几个主要区域:
- 年轻代(Young Generation):
- 年轻代是存储新生对象的区域。Java 程序通常会创建大量短暂存活的对象,这些对象的生命周期很短,适合放在年轻代中。年轻代的回收通常很频繁,因为大部分对象在创建后不久就会被垃圾回收。
- 年轻代本身又可以分为三个部分:Eden 空间(新对象的创建区域)、Survivor 0 和 Survivor 1(存放经过一次或多次 GC 仍然存活的对象)。
- 老年代(Old Generation):
- 老年代存储那些存活时间较长的对象。一般来说,年轻代中的对象如果经过多次垃圾回收仍然存活,就会被晋升到老年代。老年代的垃圾回收相对较少,因为其中的对象一般较为稳定。
- 老年代的回收成本较高,因此会使用不同的垃圾回收策略(如 Full GC)来进行回收。
- 元空间(Metaspace):
- 从 Java 8 开始,方法区被移到了元空间,用于存储类的元数据(如类信息、常量池、方法信息等)。元空间位于本地内存中,不再占用堆内存。
- 类的元数据通常是长时间存活的,因此不需要频繁回收。
为什么要分代?
- 优化垃圾回收的效率:
- 对象的生命周期:在 Java 程序中,大部分对象是短暂存在的,这些对象在创建后很快就会被销毁。垃圾回收器如果每次都检查整个堆内存,会造成大量的性能开销。通过将对象按照生命周期进行分代,可以使垃圾回收器只针对年轻代进行频繁的回收,而将老年代的回收频率降低,从而提高回收效率。
- 年轻代 GC 的频繁性:年轻代的垃圾回收(Minor GC)通常是快速的,因为它只回收生命周期短、较为简单的对象。与此不同,老年代的垃圾回收(Major GC 或 Full GC)较为复杂且耗时。因此,分代能够使年轻代和老年代的垃圾回收策略和频率有所区别,避免不必要的性能浪费。
- 减少 GC 停顿时间:
- 年轻代的快速回收:因为大部分对象的生命周期较短,年轻代中的对象会很快被回收。通过频繁地对年轻代进行垃圾回收,JVM 能够减少长时间停顿的情况,提升程序的响应性和并发性能。
- 老年代的回收优化:老年代的对象通常需要较长时间才能被回收,因此不需要频繁的垃圾回收。老年代的回收(如 Full GC)虽然耗时较长,但发生频率较低,从而避免频繁的全局回收。
- 内存分配优化:
- 快速分配空间:在年轻代中,新的对象会快速地分配内存。这是因为年轻代使用了 Eden 空间 和两个 Survivor 空间 的设计。每当发生 Minor GC 时,存活的对象会从 Eden 区转移到 Survivor 区,经过多次 GC 后再晋升到老年代。
- 对象晋升和空间回收:通过代际划分,JVM 可以根据对象的年龄来决定它们应当存储在哪个区域,这样可以避免长期存活的对象占用过多的内存空间,同时通过回收短生命周期的对象来减轻内存压力。
- 垃圾回收策略的灵活性:
- 针对不同代的不同回收策略:年轻代和老年代的垃圾回收有不同的策略。例如,年轻代使用复制算法(Copying)来实现高效的垃圾回收,而老年代则通常使用标记-清除(Mark-Sweep)和标记-压缩(Mark-Compact)等算法来减少内存碎片。分代使得 JVM 能够灵活地选择适合每个代的回收算法。
总之,JVM 将堆内存分为年轻代、老年代和元空间是为了提高垃圾回收的效率、减少停顿时间,并根据对象的生命周期选择适当的回收策略。通过这种分代机制,JVM 能够更高效地管理内存,保证多线程并发程序的性能。
3. 介绍一次完整的 GC 流程:
- 标记阶段:首先标记所有需要回收的对象。
- 清除阶段:删除被标记的对象,释放内存。
- 整理阶段:移动对象,避免内存碎片。
- 这个流程在不同的垃圾回收器中可能略有不同,但大体相似。
一次完整的 GC 流程
垃圾回收(GC)是 JVM 中用于自动管理内存的机制。JVM 通过垃圾回收器来清理堆内存中的不再使用的对象,从而释放内存。GC 主要集中在堆内存的回收上,尤其是年轻代和老年代的管理。下面是一次完整的垃圾回收(GC)流程的详细介绍:
1. GC 触发的时机
垃圾回收的触发通常有以下几种情况:
- 年轻代空间不足:当年轻代的 Eden 区空间不足时,JVM 会触发一次 Minor GC(年轻代垃圾回收)。这是最常见的 GC 类型,回收的对象大多是生命周期较短的对象。
- 老年代空间不足:当老年代空间不足时,JVM 会触发一次 Full GC(完全垃圾回收),这通常是一次较长的回收过程。此时不仅回收年轻代,还会回收老年代和永久代(或元空间)。
- JVM 手动触发:通过调用
System.gc()或Runtime.getRuntime().gc()可以手动触发垃圾回收,但这通常不推荐在生产环境中使用。
2. GC 类型
在 JVM 中,垃圾回收主要分为以下几种类型:
- Minor GC(年轻代 GC):
- 触发条件:年轻代的 Eden 区空间不足时会触发 Minor GC。
- 处理区域:只回收年轻代的对象(Eden 区和 Survivor 区)。
- 执行速度:由于年轻代的对象通常生命周期较短,回收时大多数对象都会被清除,所以 Minor GC 执行较为高效,暂停时间较短。
- Major GC 或 Full GC(老年代 GC):
- 触发条件:当老年代空间不足时触发 Full GC。
- 处理区域:不仅回收年轻代,还会回收老年代和永久代(或元空间)。
- 执行速度:由于老年代中存活的对象较多,回收时需要更长的时间,暂停时间较长。
3. 完整 GC 流程
(1)Minor GC 过程(年轻代)
Minor GC 是在年轻代内存空间不足时触发的回收过程,主要涉及以下步骤:
- 标记阶段(Mark):
- 在这个阶段,JVM 会遍历年轻代中的对象,标记所有可达(存活)对象。可达的对象是那些从根对象(如栈中的局部变量、静态字段等)能够直接或间接访问到的对象。
- 标记阶段的目的是为了确定哪些对象是可以回收的。
- 清除阶段(Sweep):
- 在标记阶段完成后,JVM 会清除掉所有未标记的对象,即那些不可达的对象。这些对象被认为是垃圾,可以从堆中回收。
- 压缩阶段(Compact):
- 为了避免内存碎片,JVM 会将存活的对象压缩到一起。这样不仅清理了不再使用的对象,还保证了堆内存的连续性,从而提高内存的使用效率。
- 对象晋升:
- 在 Minor GC 结束时,存活的对象会从年轻代的 Survivor 区转移到老年代(如果这些对象已经存活了多次 GC)。这意味着一些“长期存活”的对象会被晋升到老年代。
(2)Full GC 过程(老年代)
Full GC 是在老年代内存不足时触发的垃圾回收,过程更加复杂,主要包括:
- 标记阶段(Mark):
- 类似于 Minor GC,JVM 会标记出所有存活的对象。标记的对象会被认为是可达的,这些对象不需要被回收。
- 清除阶段(Sweep):
- 清除所有不可达的对象,将其从内存中回收。对于老年代来说,通常这一步操作会比较慢,因为老年代存活的对象比较多。
- 压缩阶段(Compact):
- 经过清理的内存区域会被压缩,回收的空间被整合起来,避免碎片问题。此步骤的目的是减少内存碎片,优化老年代的内存分配。
- 类的卸载(如果有永久代/元空间):
- 如果垃圾回收器发现永久代或元空间中有不再使用的类,它也会卸载这些类,释放相关的内存。
(3)JVM 对象的晋升
在垃圾回收过程中,JVM 会根据对象的存活时间和回收情况,将年轻代中存活的对象晋升到老年代。晋升的条件通常是:
- 对象在年轻代经过一定次数的 GC 仍然存活。
- 对象的年龄达到了老年代的晋升阈值。
这个机制有助于减少老年代的频繁回收,从而提高 GC 的效率。
(4)Stop-The-World
在垃圾回收过程中,JVM 会发生 Stop-the-World 事件,意味着所有应用程序的线程都会暂停,直到 GC 完成。具体的暂停时间会根据垃圾回收的种类和堆的大小而有所不同。Minor GC 的暂停时间通常较短,而 Full GC 的暂停时间则较长。
(5)GC 日志与分析
在进行垃圾回收时,JVM 会生成 GC 日志,记录每次 GC 的详细信息,包括垃圾回收的类型(Minor/Full)、回收前后的内存情况、回收时间等。通过分析这些日志,开发者可以监控 JVM 的内存使用情况,判断是否需要调整堆大小、垃圾回收策略等。
4. GC 策略与优化
JVM 提供了多种垃圾回收策略,可以根据不同应用的需求进行调整:
- Serial GC:单线程垃圾回收,适用于内存小、单核 CPU 环境。
- Parallel GC:多线程垃圾回收,适用于多核 CPU 环境,可以提高垃圾回收效率。
- G1 GC:适用于大堆内存和低延迟场景,能够将回收过程分成多个小步骤,减少停顿时间。
- ZGC 和 Shenandoah GC:低延迟垃圾回收器,适用于对响应时间要求极高的应用。
总结
一次完整的垃圾回收流程包括标记、清除、压缩等步骤。首先进行 Minor GC 回收年轻代对象,当年轻代空间不足时触发;如果老年代内存不足,则会触发 Full GC,进行全堆的回收。垃圾回收器通过这些过程确保 JVM 堆内存中的对象能够被高效管理,并最大化地利用内存。
4. 介绍双亲委派模型,为什么需要它?
双亲委派机制是类加载器的一种结构,它确保了类的加载顺序,避免了类加载的冲突和重复加载。通过委派机制,子类加载器优先将加载请求传递给父类加载器,从而保证了核心类库的安全性和一致性。
双亲委派模型(Parent Delegation Model)
双亲委派模型是 Java 类加载机制的一种设计模式。该模型规定了类加载器在加载类时的委派关系,即每个类加载器都有一个父类加载器,类加载器在接收到类加载请求时,首先将请求委派给父类加载器处理,只有父类加载器无法处理时,子类加载器才会自己处理。这种模型确保了类加载的一致性和安全性,避免了类的重复加载和冲突。
双亲委派模型的工作原理
-
类加载器的层次结构:
JVM 中有多个类加载器,每个类加载器都有自己的职责。Java 的类加载器层次结构通常包括:
- Bootstrap ClassLoader:顶级类加载器,负责加载 JDK 中的核心类库(如
rt.jar中的类)。这个加载器通常是由 C++ 编写,直接与 JVM 相关联。 - Extension ClassLoader:负责加载 JDK 中扩展库(如
lib/ext目录下的类)。它的父类加载器是 Bootstrap ClassLoader。 - System ClassLoader:也称为应用程序类加载器,负责加载应用程序的类路径(通常是
classpath下的类)。它的父类加载器是 Extension ClassLoader。 - 自定义类加载器:用户可以通过继承
ClassLoader来创建自己的类加载器,用于加载应用程序特定的类。
- Bootstrap ClassLoader:顶级类加载器,负责加载 JDK 中的核心类库(如
-
委派过程:
- 当一个类加载器接收到一个类加载请求时,它首先检查是否已经加载过该类。如果已经加载,它就直接返回。
- 如果该类还没有被加载,类加载器会将加载请求委派给父类加载器。父类加载器会采用同样的方式处理请求。
- 只有当父类加载器无法加载时,子类加载器才会自行加载该类。
- 这种委派机制保证了类的加载顺序和一致性。例如,应用程序的类不能覆盖 JDK 中的核心类,避免了类的冲突。
为什么需要双亲委派模型?
-
保证核心类的安全性和一致性:
双亲委派模型的最大优势是保证了 Java 核心类(如
java.lang.*、java.util.*等)不被用户的自定义类覆盖。由于所有的类加载请求都首先委派给 Bootstrap ClassLoader 处理,因此 JDK 核心库的类始终由顶层加载器加载,防止了应用程序类意外覆盖这些核心类。例如,如果没有双亲委派机制,应用程序就可能会加载一个与 JDK 核心库同名的类,这样就可能导致系统崩溃或错误的行为。 -
防止类加载的重复和冲突:
双亲委派模型避免了类的重复加载。通过让父加载器先尝试加载类,确保了类的加载不会被多个加载器重复执行。这样,类加载器可以通过委派关系来保证每个类只加载一次。
-
提高类加载的规范性和可维护性:
通过这种模型,类加载器的层次结构更加清晰,易于理解和维护。每个类加载器都有其固定的职责和加载范围,避免了类加载混乱和重复的情况。这种清晰的设计也使得 Java 在多层次、多模块的环境中表现得更加稳定和高效。
-
支持自定义类加载器:
双亲委派模型不仅确保了系统核心类的加载安全性,还为开发者提供了灵活的扩展性。开发者可以创建自定义的类加载器,通过实现自己的加载逻辑来加载应用程序特定的类,而不必担心覆盖 Java 核心库类或引发类加载冲突。
双亲委派模型的实际应用
-
应用程序的类加载:
在实际应用中,应用程序的类通常由系统类加载器(即应用程序类加载器)来加载。但是如果用户的类有冲突或依赖于其他库,系统类加载器会将请求委派给 Extension ClassLoader 或 Bootstrap ClassLoader 进行处理。
-
自定义类加载器:
自定义类加载器是通过继承
ClassLoader类实现的。在开发 Java EE 应用或集成某些第三方框架时,经常需要自定义类加载器来加载特定的类。自定义加载器通常会使用双亲委派模型,从而确保核心类不会被覆盖。 -
动态加载和隔离:
双亲委派模型可以帮助管理不同版本的类库。通过使用不同的类加载器加载不同版本的类,可以实现类库的隔离和版本管理。例如,某些复杂的 Java Web 容器(如 Tomcat)通过不同的类加载器加载 Web 应用和容器自己的类,以避免版本冲突和类覆盖。
总结
双亲委派模型是一种保障 Java 类加载器安全性、规范性和高效性的设计模式。通过将类加载请求委派给父类加载器,Java 保证了核心类不会被应用程序的类覆盖,避免了类的冲突和重复加载问题。这个模型不仅为 Java 系统提供了一个稳定的类加载机制,也支持了用户自定义类加载器,从而提供了灵活的扩展能力。在实际开发中,了解并遵循双亲委派模型,能够帮助开发者高效地管理类加载过程,并确保 Java 应用程序的稳定运行。
💬 评论