文章目录
JVM的主要组件及其作用
Java虚拟机(JVM)是运行所有Java应用程序的抽象计算机。它是一个可以执行Java字节码的虚拟机进程。JVM的主要组件包括:
- 类加载器(Class Loader):
- 引导类加载器(Bootstrap Class Loader):加载Java标准库中的核心类,这些类位于
<JRE_HOME>/lib
目录中的rt.jar
文件中。 - 扩展类加载器(Extension Class Loader):加载Java标准库的扩展目录(
<JRE_HOME>/lib/ext
)中的类。 - 应用程序类加载器(Application Class Loader):加载当前应用程序classpath中的类。
- 引导类加载器(Bootstrap Class Loader):加载Java标准库中的核心类,这些类位于
- 运行时数据区(Runtime Data Area):
- 方法区(Method Area):存储类结构信息,如字段、方法、构造函数等。
- 堆(Heap):所有对象都在这里分配内存,这是垃圾收集器管理的主要区域。
- 栈(Stack):对于每个线程,JVM都会创建一个栈,用于存储局部变量和部分结果,以及控制方法调用和返回。
- 程序计数器(Program Counter Register):每个线程都有一个程序计数器,用于记录当前线程正在执行的虚拟机字节码指令地址。
- 本地方法栈(Native Method Stack):为使用到的本地方法(如C或C++编写的代码)提供内存空间。
- 执行引擎(Execution Engine):
- 执行引擎负责执行字节码。它可以是即时编译器(JIT),也可以是解释器。即时编译器将字节码转换成本地代码,而解释器则逐条解释执行字节码。
- 本地库接口(Native Interface):
- 本地库接口(JNI)允许Java代码调用其他语言编写的库。
- 垃圾收集器(Garbage Collector):
- 垃圾收集器负责回收不再使用的对象所占用的内存资源,以减少内存泄露并优化程序性能。
JVM是如何执行Java程序的?
JVM执行Java程序的过程可以分为以下几个步骤:
- 编译Java代码:
- 首先,Java源代码(
.java
文件)需要被编译成字节码(.class
文件)。这个过程由Java编译器(javac
)完成。编译器会检查源代码的语法和类型,并将其转换成JVM可以理解的中间代码——字节码。
- 首先,Java源代码(
- 类加载:
- 当运行Java程序时,JVM会使用类加载器(Class Loader)来加载
.class
文件。类加载器会读取字节码文件,并将其转换成运行时数据结构,然后将这些数据存储在方法区(Method Area)中。
- 当运行Java程序时,JVM会使用类加载器(Class Loader)来加载
- 验证字节码:
- 在类加载后,字节码验证器会检查字节码以确保它符合JVM规范,没有安全风险,并且是有效的。这是JVM安全机制的一个重要部分。
- 运行时数据区的创建:
- 对于每个线程,JVM会在堆(Heap)中为对象分配内存,在栈(Stack)中为方法调用和局部变量分配内存,并为每个线程分配程序计数器(Program Counter Register)和本地方法栈(Native Method Stack)。
- 执行字节码:
- 执行引擎(Execution Engine)负责执行字节码。它可以使用解释器逐条解释执行字节码,也可以使用即时编译器(JIT,Just-In-Time Compiler)将字节码编译成本地机器码执行,以提高性能。现代JVM通常结合使用解释器和即时编译器,这种方法称为混合模式(Mixed Mode)。
- 垃圾收集:
- 在执行过程中,JVM会自动管理内存,垃圾收集器(Garbage Collector)会回收不再使用的对象所占用的内存资源。这个过程是自动的,开发者无需手动进行内存管理。
- 本地方法调用:
- 如果Java程序调用本地方法(如C或C++编写的代码),JVM会通过本地方法接口(JNI,Java Native Interface)来执行这些方法。
- 程序结束:
- 当Java程序执行完成后,JVM会执行清理工作,包括卸载加载的类、结束所有线程,并释放占用的系统资源。
Java类是如何被加载的?类加载器有哪些类型?
Java类被加载的过程是由JVM的类加载器(Class Loader)执行的。类加载器负责读取字节码文件(.class
文件),并将其转换成运行时数据结构,存储在JVM的方法区中。类加载过程可以分为三个主要步骤:加载、链接和初始化。
- 加载:
- 加载是类加载过程的第一个阶段,在这个阶段中,类加载器会找到字节码文件,并生成一个对应的
Class
对象,这个对象包含了类的所有信息,如类名、父类、接口、字段、方法等。 - 加载阶段完成后,
Class
对象会被存储在方法区中。
- 加载是类加载过程的第一个阶段,在这个阶段中,类加载器会找到字节码文件,并生成一个对应的
- 链接:
- 验证:验证阶段会验证字节码文件的正确性,确保它符合JVM规范,没有安全风险。
- 准备:准备阶段为类变量分配内存,并设置默认初始值。
- 解析:解析阶段是将符号引用替换为直接引用的过程。符号引用是在类文件中使用的,而直接引用是指向方法区的指针、偏移量或者是指向对象的引用。
- 初始化:
- 初始化阶段是执行类构造器
<clinit>()
方法的过程,这个方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。在这个阶段,类变量会被赋予正确的初始值。
类加载器主要分为以下几种类型:
- 初始化阶段是执行类构造器
- 引导类加载器(Bootstrap Class Loader):
- 它是JVM自带的类加载器,用于加载Java标准库中的核心类,如
java.lang
、java.util
等。这些类通常位于<JRE_HOME>/lib
目录中的rt.jar
文件中。
- 它是JVM自带的类加载器,用于加载Java标准库中的核心类,如
- 扩展类加载器(Extension Class Loader):
- 它负责加载Java标准库的扩展目录(
<JRE_HOME>/lib/ext
)中的类。
- 它负责加载Java标准库的扩展目录(
- 应用程序类加载器(Application Class Loader):
- 它加载当前应用程序classpath中的类。
除了以上三种内置的类加载器,开发者还可以自定义类加载器来加载特定路径或特定方式的类文件。自定义类加载器可以通过继承java.lang.ClassLoader
类来实现。
类加载器之间存在一种层级关系,称为类加载器的委派模型。当一个类需要被加载时,JVM会首先请求引导类加载器尝试加载,如果失败,则请求扩展类加载器,最后才请求应用程序类加载器。这种模型确保了核心API的安全性,防止了自定义的类替代核心类库中的类。
- 它加载当前应用程序classpath中的类。
什么是双亲委派模型?为什么要有这个模型?
双亲委派模型(Parent-First Delegation Model)是Java类加载器(Class Loader)的一种层级委托模型,用于确保Java应用程序的安全性和稳定性。在这个模型中,当一个类需要被加载时,JVM会首先请求父类加载器(即上级加载器)尝试加载该类,只有当父类加载器无法加载该类时,才由当前类加载器加载。
类加载器的层级结构通常如下:
- 引导类加载器(Bootstrap Class Loader):最顶层的加载器,用于加载Java标准库中的核心类。
- 扩展类加载器(Extension Class Loader):位于引导类加载器之下,用于加载Java扩展库中的类。
- 应用程序类加载器(Application Class Loader):位于扩展类加载器之下,用于加载用户应用程序classpath中的类。
双亲委派模型的工作流程如下: - 当一个类需要被加载时,JVM会首先请求应用程序类加载器尝试加载。
- 应用程序类加载器会请求扩展类加载器加载。
- 扩展类加载器会请求引导类加载器加载。
- 如果引导类加载器能够加载该类,那么加载过程结束;如果不能,扩展类加载器会尝试自己加载;如果扩展类加载器也不能加载,应用程序类加载器会尝试自己加载。
为什么要使用双亲委派模型呢?原因有以下几点: - 避免类的重复加载:如果一个类已经被父类加载器加载过,那么子类加载器无需再次加载,这保证了类的一致性。
- 安全性:防止核心API被随意篡改。例如,如果用户自定义了一个
java.lang.String
类,并尝试加载,双亲委派模型会确保这个类不会被加载,因为java.lang.String
类是由引导类加载器加载的,而用户自定义的类是由应用程序类加载器加载的。 - 稳定性:确保Java应用程序的稳定运行。通过委派给更可靠的父类加载器先加载,可以减少因加载不正确的类而导致的运行时错误。
解释JVM的内存结构
JVM的内存结构是指JVM在运行Java程序时使用的内存划分。这些内存区域各有不同的用途和生命周期,主要包括以下几个部分:
- 堆(Heap):
- 堆是JVM管理的主要内存区域,用于存储所有创建的对象和数组。堆是线程共享的,也就是说,所有线程都共享堆空间。堆的大小可以动态调整,其生命周期与JVM相同。垃圾收集器在堆中回收不再使用的对象,以释放内存。
- 栈(Stack):
- 每个线程在JVM中都有自己的栈,用于存储局部变量、方法调用的参数、返回值以及控制方法调用和返回的信息。栈是线程私有的,其生命周期与线程相同。栈内存不需要垃圾回收,因为它随着线程的结束而被完全释放。
- 方法区(Method Area):
- 方法区是所有线程共享的内存区域,用于存储类结构信息,如字段、方法、构造函数等。它还包括静态变量和常量池。方法区的大小也可以动态调整,其生命周期与JVM相同。在Java 8之前,方法区是永久代(Permanent Generation)的一部分,而从Java 8开始,方法区被元空间(Metaspace)取代。
- 程序计数器(Program Counter Register):
- 程序计数器是每个线程私有的,它用于记录当前线程正在执行的虚拟机字节码指令地址。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,因此为了线程切换后能够恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器。
- 本地方法栈(Native Method Stack):
- 本地方法栈用于存储本地方法(用C、C++等语言编写的方法)的调用状态。它是线程私有的,其生命周期与线程相同。
- 元空间(Metaspace):
- 从Java 8开始,元空间取代了永久代作为方法区的实现。元空间是JVM用于存储类的元数据的 native 内存区域,它的大小只受本地内存限制,不再占用JVM堆内存。元空间的主要目的是减少JVM对堆内存的依赖,从而减少JVM发生内存溢出的风险。
Java堆和栈的区别是什么?
Java堆(Heap)和栈(Stack)是JVM内存结构中的两个重要组成部分,它们在存储内容、生命周期、管理方式等方面有着显著的区别:
- 存储内容:
- 堆:堆是Java对象和数组的存储区域。每当new关键字创建对象时,该对象都会被分配到堆内存中。堆内存是所有线程共享的,因此多个线程可以访问同一个对象。
- 栈:栈是线程私有的内存区域,用于存储局部变量、方法调用的参数、返回值以及控制方法调用和返回的信息。每个线程都有自己的栈,栈中的数据只能由所属线程访问。
- 生命周期:
- 堆:堆内存的生命周期与JVM相同,它随着JVM的启动而创建,随着JVM的关闭而销毁。堆内存的大小可以动态调整。
- 栈:栈内存的生命周期与线程相同。每个线程在创建时都会分配一个栈,线程结束时,其对应的栈也会被销毁。栈的大小通常在JVM启动时固定,但某些JVM允许动态调整栈的大小。
- 管理方式:
- 堆:堆内存的管理由垃圾收集器负责。垃圾收集器会自动回收不再使用的对象,以释放内存。开发者可以通过设置JVM参数来调整垃圾收集器的行为。
- 栈:栈内存的管理是自动的。每个方法调用都会在栈上创建一个新的栈帧(Frame),用于存储该方法的状态信息。当方法调用结束时,对应的栈帧会被自动移除,释放内存。开发者通常不需要干预栈内存的管理。
- 内存溢出:
- 堆:如果应用程序不断创建对象且不释放,堆内存可能会耗尽,导致OutOfMemoryError异常。
- 栈:如果线程请求的栈深度超过了JVM允许的最大深度,或者栈内存分配不足,会导致StackOverflowError或OutOfMemoryError异常。
总的来说,堆和栈是JVM中两种不同用途的内存区域。堆用于存储对象和数组,是所有线程共享的,需要通过垃圾收集器来管理;而栈用于存储线程私有的局部变量和方法调用的状态信息,其管理是自动的,且每个线程都有自己的栈。
JVM的垃圾回收过程
JVM的垃圾回收(Garbage Collection,GC)过程是自动管理内存的一种机制,它旨在回收不再使用的对象所占用的内存资源,以减少内存泄露并优化程序性能。垃圾回收过程主要包括以下几个步骤:
- 标记(Marking):
- 在这一步,垃圾回收器会遍历所有的活跃对象,并标记它们为存活状态。通常,垃圾回收器会从根集合(Root Set)开始,根集合包括当前活跃的线程栈中的局部变量、静态变量、常量等。任何可以被根集合直接或间接访问到的对象都会被标记为存活。
- 可达性分析(Reachability Analysis):
- 在标记过程中,垃圾回收器会执行可达性分析,即从根集合出发,遍历对象图,标记所有可到达的对象。一个对象被认为是可达的,如果它可以通过任何活跃的引用链追溯到根集合中的一个对象。
- 清除(Sweeping):
- 清除阶段,垃圾回收器会遍历堆中的所有对象,并回收那些没有被标记为存活的对象所占用的内存空间。这些未标记的对象被认为是不可达的,即不再被任何活跃的部分引用,因此可以安全地回收。
- 整理(Compacting):
- 在清除之后,堆内存可能会变得碎片化。整理阶段(如果使用整理算法)会将所有存活的对象移动到堆的一端,使得剩余的堆空间集中在另一端,形成一个连续的空闲内存块。这样可以减少内存碎片,提高内存分配的效率。
- 重置(Resetting):
- 最后,垃圾回收器会重置内部数据结构,为下一次垃圾回收循环做准备。这可能包括更新对象的标记状态、维护内存分配指针等。
根据不同的垃圾回收器(如Serial GC、Parallel GC、CMS GC、G1 GC、ZGC等),垃圾回收的具体过程和算法可能会有所不同。一些垃圾回收器使用的是标记-清除(Mark-Sweep)算法,一些则使用标记-整理(Mark-Compact)算法。此外,现代垃圾回收器通常会采用分代收集的策略,将堆内存划分为不同的区域(如新生代、老年代),并根据对象的存活周期采用不同的回收策略。
- 最后,垃圾回收器会重置内部数据结构,为下一次垃圾回收循环做准备。这可能包括更新对象的标记状态、维护内存分配指针等。
常见的垃圾回收算法有哪些?
垃圾回收算法是JVM垃圾回收器中用于识别和回收不再使用的对象的技术。以下是一些常见的垃圾回收算法:
- 标记-清除(Mark-Sweep)算法:
- 这种算法分为两个阶段:标记和清除。
- 在标记阶段,垃圾回收器会遍历所有的活跃对象,并标记它们为存活状态。
- 在清除阶段,垃圾回收器会遍历堆中的所有对象,并回收那些没有被标记为存活的对象所占用的内存空间。
- 这种算法的主要缺点是会产生内存碎片,可能导致后续的内存分配效率降低。
- 标记-整理(Mark-Compact)算法:
- 这种算法也分为标记和整理两个阶段。
- 在标记阶段,与标记-清除算法相同,垃圾回收器会标记所有存活的对象。
- 在整理阶段,垃圾回收器会将所有存活的对象移动到堆的一端,然后清理掉边界以外的内存。
- 这种算法可以解决内存碎片问题,但可能需要更多的CPU资源来移动对象。
- 复制(Copying)算法:
- 复制算法将可用内存划分为两个相等的部分,每次只使用其中一个。
- 当进行垃圾回收时,存活的对象会被复制到未使用的内存区域,然后清理掉旧的内存区域。
- 这种算法的优点是运行速度快,不会产生内存碎片,但缺点是内存利用率低,因为任何时候只有一半的内存是可用的。
- 分代收集(Generational Collection)算法:
- 这种算法基于这样一个观察:大多数对象要么在创建后很快死亡,要么存活很长时间。
- 分代收集算法将堆内存划分为几个不同的区域,通常是新生代和老年代。
- 新生代使用复制算法,因为新生代中的对象死亡率高。
- 老年代使用标记-清除或标记-整理算法,因为老年代中的对象死亡率低。
- 这种算法可以优化垃圾回收的性能,因为新生代的回收频率高,但每次回收的速度快。
- 增量收集(Incremental Collection)算法:
- 增量收集算法将垃圾回收的工作分成多个小部分,每次执行一小部分,然后暂停,让应用程序运行。
- 这种算法的目的是减少应用程序的停顿时间,提高响应性,但可能会增加总的垃圾回收时间。
- 并发标记清除(Concurrent Mark Sweep,CMS)算法:
- CMS算法允许垃圾回收线程与应用程序线程同时运行。
- 它在标记和清除阶段都会与应用程序并发执行,尽量减少应用程序的停顿时间。
- CMS算法适用于对响应时间要求较高的应用程序,但可能会因为并发执行而降低垃圾回收的效率。
- Garbage-First(G1)算法:
- G1算法是一种面向服务器的垃圾回收器,旨在满足具有大内存需求的应用程序,并提供更可预测的垃圾回收暂停时间。
- G1将堆划分为多个区域(Region),并根据每个区域的垃圾回收价值来优先回收。
- G1的目标是达到指定的垃圾回收暂停时间目标,同时保持高吞吐量。
什么是新生代和老年代?它们在GC中的作用是什么?
在Java虚拟机(JVM)的内存管理中,堆内存被划分为不同的区域,以优化垃圾回收过程。其中,新生代(Young Generation)和老年代(Old Generation,也称为Tenured Generation)是两个主要的区域,它们各自扮演着不同的角色。
新生代:
新生代是Java堆内存中的一个区域,用于存放新创建的对象。大多数对象最初都在新生代中分配。由于大多数对象的生命周期都很短,新生代经常发生垃圾回收,这种回收称为Minor GC(小型垃圾回收)。
新生代的特点和作用:
- 对象的快速分配和回收:新生代通常使用复制算法(Copying Algorithm),这种算法在垃圾回收时能够快速地移动存活对象,并且不会产生内存碎片。
- 高频的垃圾回收:由于新生代中的对象死亡率高,JVM会频繁地进行新生代垃圾回收,以回收不再使用的对象。
- 分为Eden和Survivor区:新生代通常被分为一个Eden区和两个Survivor区(通常称为S0和S1)。新创建的对象首先在Eden区分配,当Eden区满时,进行Minor GC,存活的对象会被复制到一个Survivor区(如S0),而非存活对象则被清除。
老年代:
老年代是用于存放长时间存活的对象的内存区域。对象在新生代中经历多次垃圾回收后,如果仍然存活,它们就会被移动到老年代。老年代的空间通常比新生代大,因为老年代中的对象生命周期更长,垃圾回收的频率也相对较低,这种回收称为Major GC(或Full GC,大型垃圾回收)。
老年代的特点和作用: - 存储长期存活的对象:老年代用于存储那些在多次垃圾回收后仍然存活的对象,这些对象被认为是有价值的,可能会在整个应用程序的生命周期中持续存在。
- 低频的垃圾回收:老年代垃圾回收发生的频率低于新生代,因为老年代中的对象死亡率较低。
- 使用不同的垃圾回收算法:老年代通常使用标记-清除(Mark-Sweep)算法或标记-整理(Mark-Compact)算法,这些算法能够处理大量的存活对象和减少内存碎片。