2023-08-20
JAVA
0

目录

JVM详解
JVM定义
JVM详细介绍
JVM内存模型
类加载器(Class Loader)
执行引擎(Execution Engine)
运行时数据区(Runtime Data Area)
JVM堆内存模型
垃圾回收器和垃圾回收算法
CMS垃圾回收过程

JVM详解

JVM定义

  • JVM(Java Virtual Machine)即Java虚拟机,它是一个抽象的计算机,是运行Java字节码的虚拟执行引擎,JVM为Java的跨平台性提供了runtime环境,屏蔽了底层系统的差异,让Java程序只需要面向JVM编程就可以在不同系统上运行。它为Java程序管理内存,保证安全,提供线程支持等功能。

JVM详细介绍

  1. JVM是一个规范,定义了一台抽象计算机的工作方式,描述了这台计算机如何加载类文件,如何执行字节码,如何进行内存管理等。不同的JVM实现需要遵循这个规范,从而保证Java的“一次编写,到处运行”。
  2. JVM是一个虚拟机,它屏蔽了各种硬件平台和操作系统的差异,Java程序只需要针对JVM进行编译和运行,就可以在不同平台上运行,不需要重新编译。
  3. JVM负责执行Java字节码。Java源代码先被编译成字节码,字节码再在JVM上运行。JVM包含类加载器、运行时数据区、执行引擎、垃圾回收器等组件来支持字节码的运行。
  4. JVM为Java程序提供了内存管理、安全管理和线程管理等功能。它负责自动分配和回收内存,降低内存操作的复杂度;负责程序的字节码验证,确保代码安全;负责创建和调度线程等。
  5. 主流的JVM实现有Oracle HotSpot JVM、OpenJDK JVM、IBM JVM等。不同的JVM实现可以有自己的优化手段,但对外需要符合JVM规范。

JVM内存模型

image.png   JVM包含以下三大部分:类加载器(Class Loader)执行引擎(Execution Engine)运行时数据区(Runtime Data Area)

类加载器(Class Loader)

  类加载器是JVM的一个重要组成部分,负责将Java类加载到内存中并进行初始化。类加载器具有层次结构,分为不同的类加载器,每个加载器负责加载特定的类。以下是对JVM类加载器的详细解释:

Bootstrap Class Loader(启动类加载器):

  • Bootstrap Class Loader是JVM内置的类加载器,负责加载Java核心类库,如java.lang包中的类。
  • 由于Bootstrap Class Loader是用本地代码实现的,因此在Java代码中无法直接获取对其的引用。

Extension Class Loader(扩展类加载器):

  • Extension Class Loader负责加载JRE扩展目录(jre/lib/ext)中的JAR包和类。
  • 它是由sun.misc.Launcher$ExtClassLoader实现的,并继承自java.net.URLClassLoader

Application Class Loader(应用类加载器):

  • Application Class Loader(也称为System Class Loader)负责加载应用程序类路径(CLASSPATH)下的类。
  • 它是由sun.misc.Launcher$AppClassLoader实现的,并继承自java.net.URLClassLoader

自定义类加载器:

  • 除了上述三种标准类加载器,JVM还允许用户自定义类加载器。自定义类加载器可以实现特定的加载需求,如从非标准位置加载类、实现类隔离等。

类加载过程:

  1. 加载(Loading): 类加载器根据类的全限定名找到类文件,将其加载到内存中。这个过程不包括类的实例化,只是将类的元数据加载到JVM中。

  2. 链接(Linking): 将已加载的类与其他类和资源连接起来。链接分为三个阶段:验证(Verification)、准备(Preparation)和解析(Resolution)。

  3. 初始化(Initialization): 对类进行初始化,执行类构造器 <clinit> 方法的过程。在初始化阶段,静态成员变量会被赋初始值,静态代码块会被执行。

双亲委派模型: JVM的类加载器采用了双亲委派模型,即在加载一个类时,会首先尝试由父加载器加载,如果父加载器无法加载,则由子加载器加载。这种模型保证了类的一致性和安全性。

类加载器在Java中具有重要作用,它使得Java应用程序能够动态加载类,实现了类的隔离和版本管理,同时也有助于确保类的安全性和合规性。

执行引擎(Execution Engine)

  执行引擎是Java虚拟机(JVM)的一个重要组成部分,负责将编译后的字节码文件转化为机器代码并执行。它是实现Java程序运行的核心引擎,控制着程序的执行流程。以下是对执行引擎的详细解释:

  1. 字节码解释器(Bytecode Interpreter):

    • 字节码解释器是执行引擎的一种常见实现方式,它逐条解释字节码指令,将其转化为底层机器代码并执行。
    • 解释器的优势是可以实现跨平台的特性,但由于逐条解释的性能较低,可能会导致程序的执行效率相对较低。
  2. 即时编译器(Just-In-Time Compiler,JIT Compiler):

    • 即时编译器是执行引擎的另一种实现方式,它将字节码转化为本地机器代码,并在运行时进行编译,以提高程序的执行效率。
    • JIT编译器将热点代码(经常执行的代码块)编译成本地代码,从而减少解释执行的开销。
    • JIT编译器有两种模式:客户端模式和服务器模式,分别针对不同的应用场景进行优化。
  3. 执行流程:

    • 执行引擎从方法区中获取字节码指令,解释器或JIT编译器将其转化为机器代码。
    • 解释器逐条执行字节码指令,将其翻译为机器指令并执行。
    • JIT编译器会将热点代码编译为机器代码,并将其存储在本地代码缓存区(Native Method Stack)中,以供重复执行。
    • 执行引擎根据解释器或编译器生成的代码执行程序逻辑,实现方法调用、数据操作等功能。

执行引擎在Java虚拟机中扮演了至关重要的角色,它是将高级Java程序转化为底层机器代码的关键环节。字节码解释器和即时编译器在执行引擎中的共同作用,可以在保证跨平台特性的同时提供高效的执行性能。

运行时数据区(Runtime Data Area)

  运行时数据区(Runtime Data Area)是Java虚拟机(JVM)在运行Java程序时用来存储数据的区域,它包含了多个不同的内存区域,每个区域负责不同的任务和数据存储。以下是对运行时数据区各个区域的详细解释:

  1. 方法区(Method Area):

    • 方法区用于存储已加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • 类信息包括类的结构、方法、字段、父类、接口等。它是所有线程共享的内存区域。
    • 在HotSpot虚拟机中,方法区又称为永久代(Permanent Generation),但在较新的版本中已经被元空间(Metaspace)取代。
  2. 堆(Heap):

    • 堆是用于存储对象实例和数组的区域,它是Java程序中最主要的内存区域。
    • 堆在JVM启动时创建,用于存放所有通过new关键字创建的对象。
    • 堆可以分为新生代(Young Generation)和老年代(Old Generation),以及可选的永久代(在较早的JVM版本中存在)或元空间。
  3. 虚拟机栈(Java Virtual Machine Stacks):

    • 每个线程都有自己的虚拟机栈,用于存储方法调用和局部变量。
    • 每个方法在调用时都会创建一个栈帧,栈帧包含局部变量表、操作数栈、方法返回地址等。
    • 虚拟机栈可能会出现栈溢出异常,当递归调用过深或者栈帧过多时会发生。
  4. 本地方法栈(Native Method Stacks):

    • 本地方法栈用于存储调用本地方法(由本地代码实现的方法)时的栈帧。
    • 类似于虚拟机栈,但用于执行本地方法调用。
  5. 程序计数器(Program Counter):

    • 程序计数器是一个较小的内存区域,用于存储当前线程正在执行的字节码指令地址。
    • 每个线程都有一个程序计数器,用于记录线程的执行位置,以支持线程切换和恢复执行。

运行时数据区是Java虚拟机运行程序时的重要组成部分,各个区域的功能和作用紧密相连,它们一起支持了Java程序的执行和管理。不同区域的管理和分配在不同的JVM实现中可能有所不同,但总体上符合Java虚拟机规范。

JVM堆内存模型

image.png   Java虚拟机(JVM)的堆内存是存储对象实例和数组的主要区域,它在运行时动态分配和管理内存。堆内存模型由新生代、老年代和持久代(或元空间)组成,每个区域负责不同类型的对象和垃圾回收。以下是对JVM堆内存模型各个区域的详细解释:

  1. 新生代(Young Generation):

    • 新生代是堆内存中的一个较小的区域,用于存放新创建的对象。
    • 新生代又分为Eden空间和两个Survivor空间(通常称为S0和S1)。
    • 大部分对象在Eden空间分配,经过一次垃圾回收后,仍然存活的对象会被移动到Survivor空间。
    • 年轻代使用的垃圾回收算法主要是复制算法,通过将存活的对象从一个Survivor空间复制到另一个Survivor空间,然后清理掉不再存活的对象。
  2. 老年代(Old Generation):

    • 老年代用于存储经过多次垃圾回收仍然存活的对象,它的内存较大。
    • 老年代中的对象寿命较长,可能会在程序的整个生命周期中存在。
    • 老年代使用的垃圾回收算法主要是标记-清除算法和标记-整理算法,它们会在堆内存中进行大规模的垃圾回收。
  3. 持久代(Permanent Generation)或元空间(Metaspace):

    • 持久代在较早的JVM版本中存在,用于存储类的元数据、方法信息等。
    • 元空间是取代持久代的概念,在较新的JVM版本中出现。元空间将类的元数据存储在本地内存中,无需手动设置大小,可以根据需要自动调整。

堆内存模型中的各个区域之间有对象的流动,对象从新生代的Eden空间开始分配,经过多次垃圾回收后,如果仍然存活,会晋升到老年代。垃圾回收的主要目标是清理无用的对象,以便为新对象的分配腾出空间,同时也是为了避免堆内存溢出。

不同的JVM实现可能在堆内存管理方面有所不同,但总体上遵循类似的内存模型,以支持Java程序的运行和内存管理。

垃圾回收器和垃圾回收算法

垃圾回收(Garbage Collection)是Java虚拟机(JVM)自动管理内存的重要机制,它可以识别和清理不再被使用的对象,以回收内存并防止内存泄漏。垃圾回收器(Garbage Collector)负责执行垃圾回收的操作,而垃圾回收算法决定了具体如何标识和回收垃圾对象。以下是对垃圾回收器和垃圾回收算法的详细解释:

垃圾回收器:
  垃圾回收器是执行垃圾回收操作的模块,负责找到不再使用的对象并将其释放以回收内存。不同的JVM实现可能有不同类型的垃圾回收器,以适应不同的应用场景。以下是常见的垃圾回收器:

  1. Serial Garbage Collector:

    • 单线程垃圾回收器,适用于简单的单线程应用。
    • 使用“新生代-老年代”的内存布局,新生代使用复制算法,老年代使用标记-整理算法。
    • 主要用于客户端应用和开发/测试环境。
  2. Parallel Garbage Collector:

    • 多线程垃圾回收器,用于多核CPU的应用。
    • 也使用“新生代-老年代”的内存布局,新生代和老年代都使用复制算法和标记-整理算法。
    • 常用于需要更好垃圾回收性能的服务器应用。
  3. Concurrent Mark-Sweep (CMS) Garbage Collector:

    • 并发垃圾回收器,主要用于减少垃圾回收对应用线程的影响。
    • 使用“新生代-老年代”布局,老年代使用标记-清除算法。
    • 进行标记和清除的阶段尽量与应用线程并发执行,以减少停顿时间。
  4. G1 Garbage Collector:

    • 一款面向服务端应用的垃圾回收器,旨在提供高吞吐量和低停顿时间。
    • 使用“多区域”内存布局,将堆内存划分为多个小区域,进行混合垃圾回收。
    • G1通过智能地选择需要回收的区域,以达到更好的性能和可预测的停顿时间。
  5. ZGC收集器:
      ZGC(Z Garbage Collector)收集器是oracle出版的一种低延迟的垃圾回收器,它的目标是将垃圾回收的停顿时间控制在10毫秒以内。ZGC收集器适用于对低延迟有较高要求的应用程序。

    • 低停顿时间: ZGC的主要目标是实现低停顿时间。它通过在垃圾回收过程中与应用线程并发执行,减少了长时间的STW(Stop-The-World)暂停。大部分的垃圾回收操作都是并发执行的,最大的STW暂停时间通常只在几毫秒到十几毫秒之间。

    • 分代内存布局: ZGC使用分代内存布局,分为新生代和老年代。新生代使用可并发复制算法,而老年代使用标记-整理算法。

    • 压缩: 为了减少堆空间的碎片,ZGC回收器会在标记-整理阶段对存活对象进行压缩,使得内存空间更加连续。

    • 可预测性: ZGC回收器设计时强调可预测性,尽量保证回收的时间是可控的,不会导致应用出现长时间的停顿。

    • 适应大堆: ZGC适用于大内存堆环境,可以有效管理几个字节到数TB的内存,ZGC在JDK 11中引入,并在后续版本中逐渐得到改进和优化。

  6. Shenandoah收集器:
      Shenandoah收集器是Red Hat公司出版的一种并发垃圾回收器,它通过将垃圾回收的工作分散到多个线程中,实现了低停顿时间。Shenandoah垃圾回收器具有以下特点:

    • 低停顿时间: Shenandoah的主要目标是实现非常低的停顿时间。它通过在大部分垃圾回收阶段与应用线程并发执行,最大程度地减少了长时间的STW(Stop-The-World)暂停。

    • 并发性: Shenandoah回收器在许多阶段中与应用线程并发执行,这意味着应用线程可以在回收过程中继续工作,减少了应用的停顿。

    • 分代内存布局: Shenandoah使用了分代内存布局,分为新生代和老年代。它使用并发复制算法来处理新生代,使用标记-整理算法来处理老年代。

    • 堆压缩: Shenandoah回收器采用了堆压缩技术,以减少内存碎片,提高内存使用效率。

    • 适用大堆: Shenandoah适用于大型的Java应用,可以有效管理数十GB甚至数百GB的堆内存,Shenandoah最早在JDK 12中以实验特性引入,在后续版本中继续得到改进和优化。

垃圾回收算法:
  垃圾回收算法决定了如何标识和回收不再使用的对象,不同的算法有不同的策略和特点。

  1. 引用计数算法(Reference Counting):

    • 通过维护对象的引用计数,当引用数为0时,说明对象不再被引用,可以回收。
    • 但无法解决循环引用问题,对于循环引用的对象,引用计数不会为0,导致内存泄漏。
  2. 可达性分析算法(Reachability Analysis):

    • 通过从一组根对象出发,沿着引用链追踪,将可以被访问到的对象视为“存活”,不可访问的对象即为“垃圾”。
    • 常见的可达性分析算法有标记-清除算法、标记-整理算法、复制算法等。
  3. 分代收集算法(Generational Collection):

    • 基于“年轻代-老年代”内存布局,假设大部分对象在短时间内会变为垃圾,只有一小部分对象会存活较长时间。
    • 使用不同的垃圾回收策略和算法,如新生代使用复制算法,老年代使用标记-整理算法。

垃圾回收器和垃圾回收算法的选择取决于应用的性能需求、停顿时间要求、内存规模等因素。不同的应用可能需要不同的垃圾回收策略,以平衡性能和资源消耗。

CMS垃圾回收过程

  CMS(Concurrent Mark-Sweep)垃圾回收器是Java虚拟机(JVM)中的一种并发垃圾回收器,它的主要目标是减少垃圾回收对应用线程的影响,以降低应用的停顿时间。CMS回收器的工作过程可以分为以下阶段:

  1. 初始标记(Initial Mark):

    • 初始标记是一次短暂的STW(Stop-The-World)暂停,用于标记老年代中的所有直接可达对象。
    • 在这个阶段,JVM会从根对象出发,标记所有在根对象直接可达的对象,并将这些对象的标记位设置为已标记。
  2. 并发标记(Concurrent Mark):

    • 在初始标记之后,CMS回收器会与应用线程并发执行,进行标记阶段。应用线程可以继续执行,而垃圾回收线程则在后台标记不可达的对象。
    • 在这个阶段,CMS会通过Tracing算法追踪并标记可达对象。由于并发执行,可能会存在一些对象在标记时发生变化,因此标记阶段结束时会有一些漏标的对象。
  3. 重新标记(Remark):

    • 重新标记是一次短暂的STW暂停,用于修正在并发标记阶段中发生的漏标问题。
    • 在这个阶段,CMS回收器会重新标记那些在并发标记阶段发生变化的对象,以确保所有的可达对象都被正确标记。
  4. 并发清除(Concurrent Sweep):

    • 在重新标记之后,CMS回收器再次与应用线程并发执行,进行清除阶段。这个阶段主要是清理被标记为垃圾的对象。
    • 被清除的内存空间不会被立即回收,而是被放入空闲列表中,以备将来使用。
  5. 并发重置(Concurrent Reset):

    • 并发重置是最后一个阶段,用于清除CMS回收器的一些内部数据结构,使其准备好下一次垃圾回收。

总体来说,CMS垃圾回收器通过并发执行的方式,在标记和清除阶段尽量减少应用的停顿时间。但是,CMS回收器也存在一些缺点,如在标记和清除阶段会产生一些附加的内存开销,可能会导致碎片问题。为了解决这些问题,一些新的垃圾回收器,如G1(Garbage-First)回收器,已经在较新的JVM版本中出现。