JVM体系
此文对JVM的体系进行介绍。
# 1. 引言
Java虚拟机(Java Virtual Machine,JVM)是一个虚拟的计算机,负责运行Java程序。了解JVM对Java开发人员至关重要,因为它有助于提高代码的性能、稳定性和可维护性。本文将详细介绍JVM的各个组件和特性,包括类加载器、运行时数据区、执行引擎、垃圾回收、Java内存模型和Java Native Interface等。
# 2. JVM体系结构概述
JVM主要包括以下部分:
- 类加载器(Class Loaders)
- 运行时数据区(Runtime Data Areas)
- 垃圾回收(Garbage Collection)
- 执行引擎(Execution Engine)
- 内存模型(Memory Model)
- 本地方法接口(Java Native Interface,JNI)
- 字节码(Bytecode)
下面一一对他们进行介绍。
# 3. 类加载器
类加载器负责将字节码文件加载到JVM中。类加载器按照双亲委派模型进行组织,主要分为以下三种类型:
# 3.1 引导类加载器(Bootstrap Class Loader)
引导类加载器负责加载JVM核心类库,如 java.lang.*
。它是最顶层的类加载器,没有父类加载器。
# 3.2 扩展类加载器(Extension Class Loader)
扩展类加载器负责加载Java的扩展类库,如 javax.*
。它的父类加载器是引导类加载器。
# 3.3 应用类加载器(Application Class Loader)
应用类加载器负责加载应用程序代码。它的父类加载器是扩展类加载器。当我们使用 Class.forName()
或 ClassLoader.loadClass()
方法加载类时,默认使用的是应用类加载器。
示例请参考:Java运行期动态能力
# 4. 运行时数据区
运行时数据区负责存储Java程序运行过程中产生的数据。它主要包括以下几个部分:
# 4.1 方法区(Method Area)
方法区存储已加载类的元数据信息,如类名、父类名、方法和字段信息等。JVM规范对方法区的具体实现并没有强制要求,HotSpot虚拟机将方法区实现为永久代(PermGen)或元空间(Metaspace)。
JVM(Java虚拟机)规范并没有对方法区的具体实现方式做出强制性要求,这意味着虚拟机可以自由选择如何实现方法区,只要能够满足规范中所定义的语义和行为即可。
在早期的Java版本中,方法区被实现为一个称为永久代(PermGen)的内存区域。永久代主要用于存储静态类信息、常量池、方法的字节码和符号引用等元数据信息。然而,永久代有一个非常明显的问题,就是内存不足的时候只能通过调整JVM参数或者重启虚拟机来解决,而且永久代还容易发生内存泄漏,导致内存占用越来越高。
为了解决这些问题,从Java 8开始,HotSpot虚拟机引入了元空间(Metaspace)来代替永久代。元空间将方法区的元数据信息存储在本地内存中,而不是Java堆中,因此可以避免永久代中出现的内存问题。此外,元空间还支持动态调整大小,并且可以通过命令行参数来限制元数据信息的大小和增长速度,从而更加灵活地管理内存。
需要注意的是,虽然元空间已经代替了永久代成为了主流的方法区实现方式,但是永久代仍然可以在一些老的JVM版本或者特殊的应用场景中使用。同时,由于元空间中存储的是本地内存而非Java堆内存,因此需要注意元空间的大小控制,避免因为元数据信息过大而导致内存溢出的问题。
# 4.2 堆(Heap)
堆是Java程序运行时的主要内存区域,用于存储对象实例和数组。
堆是线程共享的资源,所有线程的对象实例都存储在堆中。
堆分为年轻代(Young Generation)和老年代(Old Generation),年轻代包括Eden区和两个Survivor区(S0和S1),用于存放新创建的对象。
年轻代是 Java 堆内存中被划分出来的一个区域,用于存放新创建的对象。当一个 Java 对象被创建时,它首先会被分配到年轻代内存中。年轻代被分为三个区域:Eden 区域和两个 Survivor 区域。新创建的对象会被分配到 Eden 区域中。当 Eden 区域满时,会触发一次垃圾回收,将 Eden 区域中不再被引用的对象清理掉,并将还存活的对象移动到 Survivor 区域中。
年轻代的垃圾回收采用的是复制算法。当一个 Survivor 区域被占满时,会将其中还存活的对象移动到另一个 Survivor 区域中,并清空原来的 Survivor 区域。这个过程被称为 Minor GC,因为只对年轻代进行了垃圾回收。
老年代是 Java 堆内存中的另一个区域,用于存放存活时间较长的对象。当一个对象在年轻代中经历了多次垃圾回收后仍然存活下来,它就会被移到老年代中。老年代的垃圾回收采用的是标记-清除算法或标记-整理算法。由于老年代中存放的对象寿命较长,垃圾回收的频率比年轻代低得多,因此垃圾回收时的停顿时间也会更长。
# 4.3 栈(Stack)
每个线程在创建时都会创建一个栈,用于存放栈帧。
栈帧包含了方法的局部变量、操作数栈、动态链接和方法返回地址等信息。
每次方法调用时,JVM会为该方法创建一个栈帧并压入栈中,方法返回时,对应的栈帧会被弹出。
# 4.4 本地方法栈(Native Method Stack)
本地方法栈用于存放本地方法(native method)调用时的栈帧。
本地方法栈与Java栈的区别在于,本地方法栈处理的是本地方法调用,而Java栈处理的是Java方法调用。
# 4.5 程序计数器(Program Counter Register)
程序计数器是线程私有的资源,用于存储当前线程正在执行的字节码指令的地址。
当线程执行一个新的字节码指令时,程序计数器的值会更新为下一条指令的地址。如果线程正在执行的是一个本地方法,那么程序计数器的值为undefined。
# 4.6 直接内存(Direct Memory)
直接内存是一种直接向操作系统申请内存的方式,而不是通过 Java 堆来分配内存。
直接内存与 Java 堆内存不同的是,它不受 Java 堆的大小限制,可以直接在操作系统的内存中分配一块连续的内存区域。这样可以有效地提高程序的内存访问速度,因为直接内存使用的是与操作系统直接交互的方式,避免了数据在 Java 堆和操作系统之间的复制。
直接内存的申请和释放需要使用 NIO(New I/O)库中的 ByteBuffer 类的 allocateDirect() 和 deallocate() 方法来进行。在使用直接内存时需要注意,因为直接内存不是由 Java 堆管理的,因此需要手动释放,否则可能会出现内存泄漏等问题。
直接内存的使用场景包括网络编程和文件操作等需要频繁读写数据的场合。例如,在进行文件传输时,可以使用直接内存来减少数据在 Java 堆和操作系统之间的复制,提高程序的传输速度。另外,在进行网络编程时,也可以使用直接内存来加速数据的读写。
# 5. 垃圾回收
在本章节中,我们将讨论Java虚拟机(JVM)中的垃圾回收(Garbage Collection,GC)机制。垃圾回收是JVM自动管理内存的一种方式,以确保在不需要时及时回收对象所占用的内存。我们将介绍垃圾回收的基本概念、常用算法、不同类型的垃圾回收器以及如何进行调优和性能监控。
# 5.1 垃圾回收的基本概念
在Java中,程序员不需要手动管理内存分配和释放。JVM通过垃圾回收器负责这些任务。垃圾回收器的主要目标是自动查找不再使用的对象并释放它们所占用的内存。
垃圾回收主要关注堆和方法区的内存。
# 5.2 垃圾回收的时机
堆中放着Java世界几乎所有的对象实例。垃圾收集器对堆进行回收前,第一件事要确定这些对象之中哪些还“存活”着,哪些已“死去”。
为了确定对象的死亡,JVM采用了如下的算法:
- 引用计数算法(Reference Counting):这是一种简单的算法,它为每个对象维护一个引用计数器。当对象被引用时,计数器加1;当引用失效时,计数器减1。当计数器为0时,对象不再被使用,可以被回收。但是,引用计数算法无法解决对象之间的循环引用问题。
- 可达性分析算法(Reachability Analysis):这是一种基于图论的算法,它的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。在Java语言中,可以作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
# 再谈引用
Java 1.2后,对于对象引用进行了扩展,分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference),它们在垃圾回收机制中起着不同的作用。
强引用(Strong Reference):是最常见的引用类型,也是默认的引用类型。如果一个对象具有强引用,即使在内存不足的情况下,垃圾回收器也不会回收该对象。只有当没有任何强引用指向一个对象时,对象才会被标记为可回收。
软引用(Soft Reference):是一种相对强引用弱化的引用类型。当内存不足时,垃圾回收器可能会回收被软引用引用的对象。这使得软引用非常适合缓存数据,可以在内存不足时释放一些缓存对象,从而避免OutOfMemoryError的发生。
弱引用(Weak Reference):是一种比软引用更弱化的引用类型。当垃圾回收器进行回收时,无论内存是否足够,都会回收被弱引用引用的对象。弱引用通常用于实现非必要对象的缓存,典型的例子是WeakHashMap。
虚引用(Phantom Reference):是最弱化的引用类型。虚引用的存在主要用于跟踪对象被垃圾回收的状态。虚引用在任何时候都可能被垃圾回收器回收,无法通过虚引用访问对象的任何属性或方法。通常与引用队列(ReferenceQueue)一起使用,用于在对象被回收之前执行一些清理操作。
需要注意的是,软引用、弱引用和虚引用是通过SoftReference
、WeakReference
和PhantomReference
类来实现的。可以通过调用相应的get()
方法来获取引用的对象,如果对象已经被回收,则返回null
。
# 不可达对象
不可达的对象,也并非“非死不可”。宣告对象的死亡,至少经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
- 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(这种情况下,finalize()方法将永远无法结束),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
# 方法区的回收
堆中,新生代,一次垃圾收集可以回收70%-90%的空间,而永久带的垃圾收集效率远低于此。
永久带的垃圾收集主要两部分:废弃常量和无用的类。
回收废弃常量与回收Java堆中的对象非常相似。
判断无用的类则相对苛刻许多:
- 该类所有的实例都已被回收
- 加载该类的ClassLoader已被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
是否对类进行回收,HotSpot提供了-Xnoclassgc
参数,还可以使用-verbose:class
以及-XX:+TraceClassLoading
、-XX:+TraceClassUnLoading
查看类加载和卸载信息。
在旧版的Java虚拟机(JVM)中,方法区使用的是传统的垃圾回收器,例如串行回收器(Serial Collector)或并行回收器(Parallel Collector)。
然而,从Java 8开始,随着永久代的移除,方法区被替换为元数据区(Metaspace),并且不再使用传统的垃圾回收器。元数据区的内存管理是基于垃圾回收器的动态分配和释放机制,而不是通过特定的垃圾回收算法来回收内存。
具体来说,元数据区使用了一种称为"条目列表"(Chunk List)的数据结构来管理元数据的分配和释放。当元数据不再被使用时,它们会被自动释放,无需进行垃圾回收。
# 5.3 垃圾回收算法
以下是几种常见的垃圾回收算法:
- 标记-清除(Mark-Sweep):这种算法首先标记所有不再使用的对象,然后清除它们。该算法可能会导致内存碎片。
- 标记-整理(Mark-Compact):这种算法在标记阶段与标记-清除算法类似,但在清除阶段,它会将活动对象压缩到内存的一侧,从而消除内存碎片。
- 复制(Copying):这种算法将内存分为两部分,每次只使用其中一部分。在回收时,它会把活动对象从一部分复制到另一部分,并清空原来的部分。这种算法不会产生内存碎片,但会降低可用内存的一半。
- 分代收集(Generational Collection):这种算法基于对象的生命周期将内存划分为新生代和老年代。新创建的对象在新生代分配空间,经过一定次数的垃圾回收后仍然存活的对象会晋升到老年代。新生代和老年代可以使用不同的垃圾回收算法。
# 5.4 垃圾回收器
在JVM中,有多种垃圾回收器可用。以下是一些HotSpot虚拟机中常见的垃圾回收器:
- Serial:这是一个单线程的垃圾回收器,适用于单核处理器的环境。在进行垃圾回收时,它会暂停所有的应用线程(称为“Stop-the-World”事件)。由于其简单性,Serial回收器对于小型应用和低内存环境可能是一个合适的选择。
- Parallel:又称为吞吐量优先(Throughput First)回收器。它使用多个线程并行执行垃圾回收任务,以减少垃圾回收所需的时间。Parallel回收器适用于多核处理器环境,并主要关注应用的吞吐量。
- Concurrent Mark Sweep (CMS):这是一个并发执行的垃圾回收器。与其他回收器不同,CMS在执行垃圾回收时,并不会暂停应用线程太长时间。因此,它特别适用于对低延迟有较高要求的应用。然而,CMS可能会导致内存碎片,并且在回收过程中会消耗更多的CPU资源。
- Garbage First (G1):这是一种新型的垃圾回收器,旨在替代CMS。G1将堆划分为多个区域,并优先回收那些垃圾对象最多的区域。G1回收器在降低延迟的同时,还具有较好的吞吐量。与CMS相比,G1回收器更易于预测停顿时间,并且不容易产生内存碎片。
Java 11引入了ZGC,Java 12引入了Shenandoah,对他们的简单介绍:
- ZGC(Z Garbage Collector):由Oracle公司开发,自JDK 11起作为实验性特性引入。ZGC采用了并发的整理算法,可以在不影响应用线程的情况下实现超大堆内存的快速收集,避免了传统垃圾回收器在大内存环境下的长时间停顿。ZGC还支持固定时间的并发垃圾收集,以及无限制的分配空间,具有更高的灵活性和可用性。
- Shenandoah:由Red Hat公司开发,自JDK 12起作为实验性特性引入。Shenandoah采用了并发的标记-压缩算法,可以在不影响应用线程的情况下实现内存的动态压缩和释放,避免了传统垃圾回收器的内存碎片问题。Shenandoah还支持可预测的低延迟垃圾收集,可以在高并发环境下提供更好的响应能力和性能表现。
# 5.5 调优和性能监控
在实际应用中,垃圾回收器的选择和配置对程序性能有很大影响。因此,了解如何调优垃圾回收器以满足特定应用需求是非常重要的。以下是一些建议:
- 选择合适的垃圾回收器:根据应用的特点和需求,选择最合适的垃圾回收器。例如,对于对延迟敏感的应用,可以选择CMS或G1;对于吞吐量优先的应用,可以选择Parallel回收器。
- 调整堆大小和内存分代:根据应用的内存使用情况,合理调整堆的大小、新生代和老年代的比例。这可以降低垃圾回收的频率和暂停时间,提高程序性能。
- 监控和诊断:使用JVM提供的工具(如JConsole、VisualVM等)监控程序的内存使用和垃圾回收情况,根据监控结果调整垃圾回收器的配置。
# 5.6 诊断垃圾回收问题
在某些情况下,应用程序可能会遇到由垃圾回收引起的性能问题。以下是一些建议和技巧,以帮助您诊断和解决垃圾回收相关的问题:
- 分析垃圾回收日志:通过启用垃圾回收日志记录,您可以收集有关垃圾回收事件的详细信息。这些日志通常包含每次垃圾回收的时间、持续时间、回收的对象数量等信息。通过分析这些日志,您可以确定垃圾回收的频率、暂停时间以及可能的性能瓶颈。
- 使用性能分析工具:利用性能分析工具(例如:VisualVM、Java Flight Recorder等)可以帮助您分析应用程序的内存使用情况、垃圾回收统计信息以及其他性能指标。这些工具通常提供可视化界面,使您更容易发现和诊断性能问题。
- 识别内存泄漏:内存泄漏是指应用程序无法释放不再需要的对象,从而导致内存耗尽。内存泄漏可能会导致频繁的垃圾回收和最终导致应用程序崩溃。使用堆分析工具(如:Eclipse Memory Analyzer等)可以帮助您识别和修复内存泄漏问题。
- 优化对象分配和内存使用:通过优化应用程序的内存使用和对象分配策略,可以减少垃圾回收的负担。例如,使用对象池来重用对象、避免创建大量短暂的临时对象、使用更小的数据结构等。
# 6. JVM的执行引擎
执行引擎(Execution Engine)是 Java 虚拟机(JVM)的一个核心组件,负责执行 Java 字节码。执行引擎将 Java 字节码转换为本地机器指令,从而实现 Java 程序的运行。
# 6.1 解释器
当 JVM 加载字节码后,解释器将逐条解释并执行字节码指令。解释器是执行引擎的基本组成部分,但解释执行的效率较低,因为每条字节码都需要在运行时解释为机器码。
# 6.2 Just-In-Time编译器(JIT)
Just-In-Time编译器是Java虚拟机中用于实现即时编译的组件,主要负责将字节码指令编译成本地机器码,并优化代码执行过程中的性能瓶颈。
JIT编译器的优点是可以针对具体的硬件平台和程序运行环境进行优化,提升程序的执行速度和性能,缺点是在编译过程中会消耗一定的系统资源和时间。
# 6.3 HotSpot虚拟机的即时编译
HotSpot虚拟机是Java平台上应用最广泛的虚拟机之一,是Java的默认虚拟机实现,也是一款高性能、可扩展的虚拟机。在HotSpot虚拟机中,即时编译器采用了C1和C2两个阶段的编译器流水线,分别负责对冷代码和热代码进行编译和优化。
C1 编译器(Client 编译器):C1 编译器主要负责编译冷代码,即执行次数较少的代码。C1 编译器会在编译时进行一些简单的优化,但不会花费太多时间。这样做的目的是在启动速度和执行性能之间找到一个平衡点。编译后的代码会被执行,同时 HotSpot 虚拟机会收集运行时信息,以便进一步优化。
C2 编译器(Server 编译器):C2 编译器主要负责编译热代码,即经常执行的代码。它会使用更复杂、更耗时的优化方法来提高代码的执行效率。这些优化方法可能包括内联、循环展开、常量折叠等。C2 编译器会根据 HotSpot 虚拟机收集到的运行时信息对代码进行优化,以达到最佳的性能。
# 6.4 优化技术
Java虚拟机提供了一些优化技术,用于提高代码执行的效率和性能。以下是一些常见的优化技术:
# 6.4.1 方法内联
方法内联是指在编译时将方法调用直接替换为方法体的代码。这样可以减少方法调用的开销和栈帧的创建,提高代码的执行效率。
# 6.4.2 逃逸分析
逃逸分析是指在编译时分析对象的作用域和生命周期,确定对象是否会被外部引用,以便进行优化。如果对象不会逃逸出方法的作用域,可以将其分配在栈上,而不是在堆上,避免了垃圾回收的开销。
# 6.4.3 循环展开
循环展开是指在编译时将循环体中的语句复制多次,以减少循环的迭代次数和分支判断,提高代码的执行效率。但循环展开也会增加代码的体积,可能会影响缓存的命中率和分支预测的准确性。
# 6.4.4 其他优化技术
Java虚拟机还提供了其他一些优化技术,如栈上分配、标量替换、去除冗余操作、类加载优化等。这些技术都是为了提高代码执行的效率和性能,减少垃圾回收的开销和内存的占用。
# 补充:JIT、AOT与JSR 269和Java编译器
JIT和AOT都是属于虚拟机的一部分,而JSR 269属于Java编译器(javac)的一部分。
Java编译器(Java Compiler)不是JVM的一部分,它是独立于JVM的一个工具,用于将Java源代码编译成Java字节码文件,以供JVM执行。
在Java开发中,Java编译器是将Java源代码转化为Java字节码文件的必备工具,它将Java源代码编译成平台无关的字节码指令,以便在任何支持Java虚拟机的平台上运行。Java编译器的主要作用是检查Java源代码的语法和语义,并将其转化为字节码指令,同时还可以对Java源代码进行优化和压缩,以提高程序的执行效率和性能。
Java编译器通常包括两个主要的工具:javac和javadoc。其中,javac用于编译Java源代码,生成对应的字节码文件;javadoc用于生成Java源代码的文档注释。
在JVM中,编译后的Java字节码文件是由JVM解释执行或者即时编译执行的。因此,Java编译器和JVM是紧密相关的两个组件,但并不是JVM的一部分。
# 7. JMM内存模型
JMM定义了一套规范,描述了在Java程序中线程如何访问内存。JMM规范中涉及到的概念有:
- 主内存(Main Memory):Java虚拟机所管理的内存,是所有线程共享的内存区域。
- 工作内存(Working Memory):每个线程独立的内存区域,存储线程运行过程中需要读写的变量拷贝。
- 内存间交互操作:Java程序中使用volatile、synchronized、final、Lock等关键字进行内存间的交互操作。
# 7.1 原子性、可见性和有序性
JMM规范中定义了原子性、可见性和有序性三个概念。它们分别对应了在Java程序中线程对内存访问时可能出现的问题。
- 原子性:指一个操作是不可中断的整体,要么全部执行成功,要么全部执行失败。
- 可见性:指当一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。
- 有序性:指程序执行的顺序,JMM规定了一些约束条件,保证程序执行的顺序是正确的。
# 7.2 volatile关键字
volatile关键字可以保证共享变量的可见性和一定程度上的有序性,但并不能保证原子性。当一个变量被volatile修饰时,所有线程都会从主内存中读取这个变量的值,而不是从自己的工作内存中读取。当一个线程修改了共享变量的值后,这个值会立即写回到主内存,而不是在某个时刻才写回。
# 7.3 Happens-Before规则
Happens-Before规则是JMM中一个重要的概念,用于描述在多线程并发访问内存时的时序关系。它规定了一些原则,确保程序执行的结果是正确的。Happens-Before规则可以帮助我们理解Java程序中内存访问的行为和规则,从而更好地编写高效、正确和线程安全的Java程序。
Happens-Before 规则包括以下几个方面:
- 程序顺序规则:在一个线程内,一个操作(如变量赋值、方法调用等)按程序顺序发生在另一个操作之前,那么这两个操作满足 Happens-Before 关系。换句话说,在同一个线程中,代码的执行顺序遵循书写顺序。
- 监视器锁规则:对一个锁的解锁操作 Happens-Before 该锁的后续加锁操作。当线程 A 释放一个锁,然后线程 B 获取该锁时,线程 A 在释放锁之前所进行的所有操作对线程 B 都是可见的。
- volatile 变量规则:对一个 volatile 变量的写操作 Happens-Before 该变量的后续读操作。当线程 A 写入一个 volatile 变量,然后线程 B 读取该变量时,线程 A 在写入该变量之前所进行的所有操作对线程 B 都是可见的。
- 线程启动规则:线程 A 启动线程 B 时,线程 A 对线程 B 的启动操作 Happens-Before 线程 B 中的任何操作。换句话说,当线程 A 启动线程 B 时,线程 A 在启动线程 B 之前所进行的所有操作对线程 B 都是可见的。
- 线程终止规则:线程 A 的终止操作 Happens-Before 任何线程 B 检测到线程 A 已经终止(通过 Thread.join() 方法或其他方式)。当线程 A 结束时,线程 A 在结束之前所进行的所有操作对线程 B 都是可见的,前提是线程 B 检测到了线程 A 的终止。
- 线程中断规则:线程 A 对线程 B 的中断操作 Happens-Before 线程 B 发现中断事件(通过调用 Thread.interrupted() 方法或其他方式)。
- 对象构造函数规则:对象构造函数的执行 Happens-Before 该对象的 finalizer 方法。在构造对象时,构造函数中所进行的所有操作对 finalizer 方法都是可见的。
# 7.4 Java中的锁和同步原语
Java中提供了多种锁和同步原语,用于保证线程安全。其中最常用的有synchronized关键字和ReentrantLock类。synchronized关键字是Java中最基本的同步机制,它采用的是互斥锁机制,确保同一时刻只有一个线程可以进入同步代码块。而ReentrantLock是一个可重入的互斥锁,相比synchronized具有更高的灵活性和可定制性。
除了基本的锁和同步机制外,Java中还提供了一些高级的同步工具,如Semaphore、CountDownLatch、CyclicBarrier等,它们可以帮助我们更好地管理线程和协调线程之间的交互行为。
# 8. Java Native Interface (JNI)
Java Native Interface (JNI) 是Java平台提供的一种机制,用于在Java代码和本地代码(如C/C++代码)之间进行交互。通过JNI,Java应用程序可以调用本地代码提供的功能,也可以将Java对象传递给本地代码进行处理。
# 8.1 JNI的基本概念
JNI提供了一些标准的API和约定,用于描述Java和本地代码之间的交互。JNI的基本概念如下:
Native Method:在Java类中声明的本地方法,使用native关键字修饰。
JNIEnv:JNI环境接口,用于调用JNI提供的各种函数。
jclass、jobject、jarray等:JNI定义的一系列类型,用于表示Java中的类、对象和数组等数据结构。
jfieldID、jmethodID等:用于标识Java中的字段和方法等成员的唯一ID。
# 8.2 使用JNI调用本地代码
在Java中调用本地代码,需要经过以下步骤:
编写本地代码,并生成本地库文件(如动态链接库.so文件)。
在Java类中声明native方法,以便在Java中调用本地代码。
在Java代码中使用System.loadLibrary()函数加载本地库文件。
在Java代码中调用native方法,触发JNI机制将调用传递到本地代码中。
# 8.3 在本地代码中调用Java方法
在本地代码中调用Java方法,需要经过以下步骤:
获取JNIEnv接口指针,以便在本地代码中调用JNI提供的函数。
使用FindClass()函数查找Java类,使用GetMethodID()函数获取Java方法的ID。
使用CallXXXMethod()函数调用Java方法,其中XXX可以是Void、Boolean、Byte、Char、Short、Int、Long、Float或Double等基本类型。
# 8.4 Java和本地代码之间的数据传递
在Java和本地代码之间传递数据,需要经过以下步骤:
在Java中创建jobject或jarray对象,用于表示数据结构。
在本地代码中使用GetXXXArrayElements()函数获取数组元素或GetXXXField()函数获取对象字段的值。
在本地代码中使用SetXXXArrayElements()函数设置数组元素或SetXXXField()函数设置对象字段的值。
在本地代码中使用NewXXXArray()函数创建数组对象或NewObject()函数创建Java对象。
综上所述,JNI提供了Java和本地代码之间进行交互的一种机制,可以帮助Java应用程序获得更高的灵活性和性能。但是,由于涉及到跨语言交互,JNI的使用也需要非常小心谨慎,避免出现内存泄漏、数据类型不匹配等问题。
# 9. 字节码
Java 之所以如此受欢迎,一方面是因为它具有跨平台性,另一方面则是因为它使用了一种称为 Java 字节码的中间语言。
# 9.1 什么是 Java 字节码?
Java 字节码是一种中间语言,是 Java 源代码编译后得到的二进制代码,也被称为 JVM 代码。Java 虚拟机可以解释并执行这些字节码。由于字节码是中间语言,所以它具有跨平台性,可以在不同的操作系统和硬件平台上运行。
Java 字节码可以通过使用 javac 命令将 Java 源代码编译成 .class 文件得到。.class 文件包含了 Java 字节码以及一些其他的元数据信息,例如类名、方法名等。Java 虚拟机可以读取 .class 文件,并将其中的字节码解释成机器码来执行程序。
# 9.2 Java 字节码的结构
Java 字节码是由一系列指令组成的,每个指令都对应着一些操作。
指令是 Java 虚拟机(JVM)执行 Java 字节码时所遵循的指令集。这些指令控制 JVM 如何处理数据、执行操作以及调用方法等。按用途分类,字节码操作指令大致分为9类。以下是这9类指令的详细介绍:
- 加载指令(Load):这类指令用于将数据从内存(如局部变量表或类字段)加载到操作数栈中。例如,
iload
指令从局部变量表加载 int 类型数据到操作数栈,aload
指令从局部变量表加载引用类型数据到操作数栈。 - 存储指令(Store):这类指令用于将数据从操作数栈存储到内存(如局部变量表或类字段)。例如,
istore
指令将 int 类型数据从操作数栈存储到局部变量表,astore
指令将引用类型数据从操作数栈存储到局部变量表。 - 栈操作指令(Stack):这类指令用于操作数栈的管理,如压栈、弹栈、交换栈顶元素等。例如,
pop
指令用于弹出操作数栈顶的元素,dup
指令用于复制操作数栈顶的元素。 - 算术指令(Arithmetic):这类指令用于执行基本的算术运算,如加法、减法、乘法、除法等。例如,
iadd
指令用于执行 int 类型的加法运算,ddiv
指令用于执行 double 类型的除法运算。 - 类型转换指令(Type Conversion):这类指令用于在不同数据类型之间进行转换。例如,
i2f
指令用于将 int 类型数据转换为 float 类型数据,l2d
指令用于将 long 类型数据转换为 double 类型数据。 - 对象操作指令(Object Manipulation):这类指令用于操作对象,如创建对象、访问字段、调用方法等。例如,
new
指令用于创建一个新对象,getfield
指令用于访问对象的实例字段,invokevirtual
指令用于调用对象的虚方法。 - 控制转移指令(Control Transfer):这类指令用于控制程序的执行流程,如条件跳转、无条件跳转、循环等。例如,
ifeq
指令用于在条件为真时跳转到指定位置,goto
指令用于无条件跳转到指定位置。 - 异常处理指令(Exception Handling):这类指令用于异常的处理和抛出。例如,athrow 指令用于抛出异常,jsr 和 ret 指令用于跳转到异常处理程序并返回。
- 同步指令(Synchronization):这类指令用于实现线程同步,如进入或退出监视器(monitor)。例如,monitorenter 指令用于获取对象的监视器,monitorexit 指令用于释放对象的监视器。
# 9.3 Java 字节码的使用
Java 字节码可以通过使用反编译工具来查看,例如 javap 命令。使用 javap 命令可以查看 .class 文件中包含的字节码信息,例如方法名、方法参数、返回类型以及字节码指令等。
Java 字节码也可以通过 ASM(Java 字节码操作框架)这样的字节码操作框架来生成和修改。ASM 提供了一组 API,用于操作字节码,并提供了一些工具类,例如 ClassWriter、MethodVisitor 等,用于生成和修改字节码。通过使用 ASM,开发人员可以在不改变 Java 语言语法的情况下,生成高效的字节码。
Java 字节码还可以用于进行代码分析和优化。在进行代码分析时,可以使用字节码工具来分析代码中的指令、变量和方法等信息,以便进行代码调试和性能优化。在进行代码优化时,可以通过改进字节码的生成方式来提高程序的性能。
# 9.4 Java 字节码的优缺点
Java 字节码的主要优点是跨平台性。由于字节码是一种中间语言,可以在不同的操作系统和硬件平台上运行。这样可以使开发人员只需要编写一次代码,就可以在不同的平台上运行。
Java 字节码的另一个优点是安全性。Java 字节码可以通过 Java 虚拟机的安全机制来执行,这样可以有效地防止一些安全漏洞,例如缓冲区溢出等。
Java 字节码的主要缺点是性能。由于 Java 字节码需要被解释成机器码来执行,所以它的执行速度相对较慢。这个问题可以通过 JIT(Just-In-Time)编译器来解决,JIT 编译器可以将字节码编译成本地机器码来执行,这样可以大大提高程序的执行速度。
另外,由于 Java 字节码是一种中间语言,所以它的可读性相对较差。对于开发人员来说,需要掌握一定的字节码知识才能够有效地分析和优化字节码。
# 10. JVM参数
JVM的参数有很多,以下是一些常用的JVM参数:
-Xms<size>
:设置初始堆大小。例如:-Xms256m
。-Xmx<size>
:设置最大堆大小。例如:-Xmx1024m
。-Xss<size>
:设置每个线程的栈大小。例如:-Xss1m
。-XX:MetaspaceSize=<size>
:设置 Metaspace 的初始大小。例如:-XX:MetaspaceSize=128m
。-XX:MaxMetaspaceSize=<size>
:设置 Metaspace 的最大大小。例如:-XX:MaxMetaspaceSize=512m
。-XX:NewSize=<size>
:设置新生代的初始大小。例如:-XX:NewSize=64m
。-XX:MaxNewSize=<size>
:设置新生代的最大大小。例如:-XX:MaxNewSize=256m
。-XX:SurvivorRatio=<ratio>
:设置新生代 Eden 区与 Survivor 区的大小比例。例如:-XX:SurvivorRatio=8
。-XX:+UseG1GC
:启用 G1 垃圾收集器。-XX:+UseParallelGC
:启用并行垃圾收集器。-XX:+UseConcMarkSweepGC
:启用 CMS 垃圾收集器(在 Java 11 中已被弃用)。-XX:+UseSerialGC
:启用串行垃圾收集器。-XX:MaxGCPauseMillis=<millis>
:设置垃圾收集的最大暂停时间(毫秒)。例如:-XX:MaxGCPauseMillis=200
。-XX:ParallelGCThreads=<num>
:设置并行垃圾收集器的线程数。例如:-XX:ParallelGCThreads=4
。-XX:+PrintGCDetails
:打印详细的垃圾收集信息。-XX:+PrintGCDateStamps
:在 GC 日志中输出带日期时间戳的详细信息。-Xlog:gc*
:打印垃圾收集日志。-Xlog:gc*:file=<file>
:将垃圾收集日志输出到指定文件。例如:-Xlog:gc*:file=gc.log
。-XX:+HeapDumpOnOutOfMemoryError
:在发生内存溢出错误时生成堆转储文件。-XX:HeapDumpPath=<path>
:设置堆转储文件的路径。例如:-XX:HeapDumpPath=/tmp/dump.hprof
。
可以看到JVM参数是与他的内存结构、垃圾回收息息相关的,需要熟悉有关知识,在实际中根据应用的特点和实际情况进行选择和优化。
需要特别注意的是-Xss和-Xmx参数,分别定义了栈大小和堆大小。栈内存不够会抛出StackOverflowError,堆内存不够会抛出OutOfMemoryError,这都是开发者经常遇到的错误。
# 11. JVM相关工具
- 内置命令行工具:jps、jstat、jinof、jhat、jstack
- 可视化工具:VisualVM、Java Flight Recorder
- Arthas (opens new window) 一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
- gc日志分析工具 (opens new window)
# 12. 总结
在Java开发中,JVM扮演着非常重要的角色。了解JVM的基本概念和运行机制,以及掌握JVM的性能优化技术,可以帮助我们更好地理解和优化Java应用程序的性能,提高应用的可靠性和可维护性。同时,JVM的优化也是一个不断发展的过程,随着硬件和软件技术的不断进步,JVM的性能优化技术也会不断地更新和发展。因此,对JVM的深入理解和持续学习,对于Java开发者来说是非常必要的。
除了性能优化之外,JVM在Java开发中还有很多重要的作用。例如,它可以通过类加载器实现动态加载和卸载类文件,提高应用程序的灵活性和扩展性;它也可以通过Java虚拟机调试接口(JVMTI)实现应用程序的动态分析和调试,方便开发人员进行调试和排错。
总之,JVM作为Java平台的核心组件,在Java开发中扮演着至关重要的角色。了解JVM的基本概念、运行机制和性能优化技术,以及掌握JVM的应用和调试技术,是Java开发者必须具备的基本能力。
文中对JVM体系进来了介绍,因为JVM体系庞大,这里肯定是挂一漏万,希望大家多提意见!
祝你变得更强!