怀念夏天 发表于 2025-4-18 15:09:09

详解JVM的底层原理

目录
1.JVM的内存区域划分
1)程序计数器(Program Counter Register)
2)元数据区(Metaspace)
3)假造机栈(Java Virtual Machine Stacks)
4)堆(Heap)
2.JVM类加载的过程
1. 加载(Loading)
2. 验证(Verification)
3. 准备(Preparation)
4. 解析(Resolution)
5. 初始化(Initialization)
6.类加载过程示例代码
 7.双亲委派模型
1)双亲委派模型的界说
2)双亲委派模型的工作流程
3.JVM的垃圾回收算法
一.找到垃圾的方法

1)找到垃圾的方法:引用计数法(Reference Counting)
原理
示例代码
优缺点
2)找到垃圾的方法(JVM利用):可达性分析算法(Reachability Analysis)
原理
可作为 GC Roots 的对象

优缺点
二.垃圾回收算法
1)标记 - 清除算法(Mark - Sweep)
工作原理
优缺点
2)标记 - 整理算法(Mark - Compact)
工作原理
优缺点
3)复制算法(Copying)
工作原理
优缺点
4)分代收集算法(Generational Collection)
工作原理
优缺点
示例代码及阐明

 
JVM(Java假造机)是Java程序运行的核心情况,它实现了Java“一次编写,随处运行”的跨平台特性。 
1.JVM的内存区域划分

JVM的内存区域主要分为以下几个部分,各司其职,共同支持Java程序的运行:
程序计数器、元数据区、栈、堆。
1)程序计数器(Program Counter Register)



[*] 作用:雷同于盘算机组成原理中所提到的程序计数器,JVM中的程序计数器的作用也是记录当前线程正在实验的字节码指令地点。
[*] 特点:

[*] 线程私有:每个线程独立拥有一个程序计数器。
[*] 唯一无OOM区域:不会抛出OutOfMemoryError。

2)元数据区(Metaspace)



[*] 作用:存储类元数据(如类名、方法信息、字段信息、常量池、静态变量等)。
[*] 演变历史:

[*] JDK 7及之前:称为永久代(PermGen),位于堆内存中,轻易引发OutOfMemoryError。
[*] JDK 8及之后:由元空间(Metaspace)替换,直接利用本地内存(Native Memory),不再受JVM堆大小限定。

[*] 特点:

[*] 线程共享:全部线程共享元数据区。
[*] 动态扩展:默认不限定大小,可通过-XX:MaxMetaspaceSize设置上限。
[*] 异常:OutOfMemoryError(当本地内存不敷时抛出)。

3)假造机栈(Java Virtual Machine Stacks)



[*] 作用:存储线程的方法调用信息,每个方法对应一个栈帧(Stack Frame)。
[*] 栈帧内容:

[*] 局部变量表:保存方法的参数和局部变量。
[*] 操作数栈:实验字节码指令时的临时操作数存储区。
[*] 动态链接:指向方法区中的方法引用。
[*] 方法返回地点:记录方法实验完毕后返回的位置。

[*] 特点:

[*] 线程私有:每个线程独立拥有一个栈。
[*] 异常:StackOverflowError(栈深度超出限定)或OutOfMemoryError(扩展失败)。

4)堆(Heap)



[*] 作用:存放全部对象实例和数组,是垃圾回收(GC)的主要区域。
[*] 特点:

[*] 线程共享:全部线程均可访问堆中的对象。
[*] 细分结构:

[*] 新生代(Young Generation):分为Eden区、Survivor区(From/To),存放新创建的对象。
[*] 老年代(Old Generation):恒久存活的对象(颠末多次GC后存活)提升至此。

[*] 异常:OutOfMemoryError(当堆内存无法分配时抛出)。

2.JVM类加载的过程

1. 加载(Loading)

这是类加载的第一个阶段,在这个阶段,JVM 会完成以下操作:



[*]通过类的全限定名来获取界说此类的二进制字节流。这一步可以从多种泉源获取字节流,好比本地文件系统、网络、ZIP 包等。
[*]将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
[*]在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

2. 验证(Verification)

此阶段的目的是确保被加载的类的字节流符合 JVM 规范,不会危害 JVM 自身的安全。验证主要包含以下几个方面:



[*]文件格式验证:验证字节流是否符合 Class 文件格式的规范,好比是否以 0xCAFEBABE 开头、主次版本号是否在当前 JVM 支持的范围内等。
[*]元数据验证:对字节码形貌的信息进行语义分析,以保证其形貌的信息符合 Java 语言规范的要求,例如这个类是否有父类(除了 java.lang.Object 之外,全部的类都应该有父类)。
[*]字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如,对方法体进行校验,确保跳转指令不会跳转到方法体以外的字节码指令上。
[*]符号引用验证:在解析阶段中,会将符号引用转换为直接引用,符号引用验证是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,好比符号引用所涉及的类是否能被找到等。

3. 准备(Preparation)

该阶段是为类的静态变量分配内存并设置类变量初始值的阶段,这些变量所利用的内存都将在方法区中进行分配。这里的初始值通常是数据类型的零值,例如:
public class PrepareExample {
    public static int value = 123;
}

在准备阶段,value 变量会被初始化为 0,而不是 123。把 value 赋值为 123 的操作是在初始化阶段完成的。

4. 解析(Resolution)

解析阶段是 JVM 将常量池内的符号引用替换为直接引用的过程。
符号引用是以一组符号来形貌所引用的目的,符号可以是任何情势的字面量,只要利用时能无歧义地定位到目的即可。
直接引用是直接指向目的的指针、相对偏移量或是一个能间接定位到目的的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。

5. 初始化(Initialization)

这是类加载的最后一个阶段,在这个阶段,JVM 才真正开始实验类中界说的 Java 程序代码(或者说是字节码)。
初始化阶段是实验类构造器 <clinit>() 方法的过程。<clinit>() 方法是由编译器主动收集类中的全部类变量的赋值动作和静态语句块(static{} 块)中的语句归并产生的。例如:

public class InitializationExample {
    static {
      System.out.println("静态代码块执行");
    }
    public static int value = 123;
}

在初始化阶段,静态代码块会被实验,并且 value 变量会被赋值为 123。

6.类加载过程示例代码


class Parent {
    static {
      System.out.println("Parent static block");
    }
    public static int parentValue = 10;
}

class Child extends Parent {
    static {
      System.out.println("Child static block");
    }
    public static int childValue = 20;
}

public class ClassLoadingExample {
    public static void main(String[] args) {
      System.out.println(Child.childValue);
    }
}
在上述代码中,当实验 main 方法时,会触发 Child 类的加载。
由于 Child 类继续自 Parent 类,所以会先加载 Parent 类,依次颠末加载、验证、准备、解析和初始化阶段。
在初始化阶段,会实验 Parent 类的静态代码块,将 parentValue 赋值为 10。接着加载 Child 类,同样颠末这些阶段,实验 Child 类的静态代码块,将 childValue 赋值为 20。最后打印出 Child.childValue 的值。
 7.双亲委派模型

双亲委派模型与 JVM(Java Virtual Machine)有着精密且多方面的联系,在 JVM 的类加载体系中饰演着至关重要的脚色。
1)双亲委派模型的界说

双亲委派模型规定了类加载器之间的层次关系和类加载的委托机制。在 Java 中,存在多种类加载器,它们形成了一个树形结构,从顶层到底层主要有以下几种:



[*]启动类加载器(Bootstrap ClassLoader):由 C++ 实现,是最顶层的类加载器,负责加载 Java 的核心类库,如 java.lang 包下的类。
[*]扩展类加载器(Extension ClassLoader):由 Java 实现,负责加载 JAVA_HOME/lib/ext 目录下的类库。
[*]应用程序类加载器(Application ClassLoader):也由 Java 实现,负责加载用户类路径(classpath)上的类库,一样平常情况下,我们自己编写的 Java 类就是由该类加载器加载的。
[*]自界说类加载器(User-Defined ClassLoader):用户可以根据需求自界说类加载器,继续自 java.lang.ClassLoader 类。
2)双亲委派模型的工作流程

当一个类加载器收到类加载请求时,它不会立即实验加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。
因此全部的加载请求终极都会传送到顶层的启动类加载器中。
只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会实验自己去加载。
具体流程如下:


[*]应用程序类加载器收到类加载请求。
[*]应用程序类加载器将请求委派给扩展类加载器。
[*]扩展类加载器将请求委派给启动类加载器。
[*]启动类加载器在其搜索范围内查找该类,假如找到则加载该类;假如找不到,则将请求返回给扩展类加载器。
[*]扩展类加载器在其搜索范围内查找该类,假如找到则加载该类;假如找不到,则将请求返回给应用程序类加载器。
[*]应用程序类加载器在其搜索范围内查找该类,假如找到则加载该类;假如找不到,则抛出 ClassNotFoundException 异常。
3.JVM的垃圾回收算法


JVM(Java Virtual Machine)的垃圾回收算法旨在主动回收不再利用的内存,以保证系统的性能和稳定性。那么首先,JVM如何找到垃圾呢?
准确找出垃圾对象(即不再被利用、可回收内存的对象)是垃圾回收的关键条件。
一.找到垃圾的方法



1)找到垃圾的方法:引用计数法(Reference Counting)

原理

给每个对象配备一个引用计数器,每当有一个地方引用该对象时,计数器的值就加 1;当引用失效时,计数器的值就减 1。
要利用一个对象,一定是通过引用来完成的。
当计数器的值为 0 时,就表明这个对象不再被引用,可被视为垃圾对象。
示例代码

# Python 示例模拟引用计数
class MyObject:
    def __init__(self):
      pass

# 创建对象
obj1 = MyObject()
obj2 = obj1# 引用计数加 1
obj1 = None# 引用计数减 1
obj2 = None# 引用计数减 1,此时对象可被回收

优缺点



[*]长处:实现简单,判定服从高,能及时发现垃圾对象。
[*]缺点:难以处置惩罚循环引用的情况。好比两个对象相互引用,即使它们在程序中已不再被其他地方利用,由于引用计数器的值不为 0,它们也不会被当作垃圾回收。
2)找到垃圾的方法(JVM利用):可达性分析算法(Reachability Analysis)

原理

以一系列被称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链。
当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 到这个对象不可达),则证实此对象是不可用的,可被判定为垃圾对象。
   可作为 GC Roots 的对象



[*]假造机栈(栈帧中的本地变量表)中引用的对象:例如,在方法中创建的局部变量所引用的对象。
[*]方法区中类静态属性引用的对象:像类的静态变量引用的对象。
[*]方法区中常量引用的对象:例如,利用 final 关键字界说的常量所引用的对象。
[*]本地方法栈中 JNI(即一样平常说的 Native 方法)引用的对象:也就是在本地方法中引用的对象。


public class ReachabilityAnalysisExample {
    public static void main(String[] args) {
      Object obj1 = new Object(); // obj1 是 GC Roots 可达的
      Object obj2 = new Object();
      obj1 = null; // obj1 不再是 GC Roots 可达的,可能被回收
      obj2 = null; // obj2 不再是 GC Roots 可达的,可能被回收
    }
}

优缺点



[*]长处:可以或许有效解决引用计数法中循环引用的题目,是目前主流 JVM 采用的判定垃圾对象的方法。
[*]缺点:实现相对复杂,必要进行递归遍历,并且在进行可达性分析时,必要停息全部的用户线程(即 “Stop The World”),这可能会对应用程序的性能产生一定影响。
二.垃圾回收算法

JVM(Java Virtual Machine)的垃圾回收算法旨在主动回收不再利用的内存,以保证系统的性能和稳定性。
下面详细介绍几种常见的垃圾回收算法。
1)标记 - 清除算法(Mark - Sweep)

工作原理

该算法分为两个阶段。
首先是标记阶段,垃圾回收器会从根对象(如栈中的引用、静态变量等)开始遍历,标记出全部存活的对象。
接着是清除阶段,对未被标记的对象(即垃圾对象)进行清除,开释其所占用的内存空间。
   优缺点



[*]长处:实现简单,不必要额外的数据结构来记录内存信息。
[*]缺点:轻易产生内存碎片。随着多次回收,内存中会出现大量不连续的小内存块,当必要分配较大对象时,可能会因找不到足够大的连续内存空间而提前触发新的垃圾回收。
2)标记 - 整理算法(Mark - Compact)

工作原理

同样先进行标记阶段,找出全部存活的对象。
之后进入整理阶段,将存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。
   优缺点



[*]长处:解决了标记 - 清除算法产生内存碎片的题目,使内存空间连续,有利于大对象的分配。
[*]缺点:整理过程必要移动对象,会带来一定的性能开销,且在移动对象时必要停息应用程序的实验。
3)复制算法(Copying)

工作原理

将可用内存按容量划分为大小相称的两块,每次只利用其中一块。
当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后把已利用过的内存空间一次清理掉。
   优缺点



[*]长处:实现简单,回收服从高,且不会产生内存碎片。
[*]缺点:可用内存空间淘汰为原来的一半,内存利用率较低。通常实用于对象存活率较低的场景,如新生代。
4)分代收集算法(Generational Collection)

工作原理

根据对象的存活周期将内存划分为不同的区域,一样平常分为新生代和老年代。
新生代中对象的存活时间较短,大部分对象很快就会变成垃圾,因此适合利用复制算法。
老年代中对象的存活时间较长,对象存活率较高,采用标记 - 清除或标记 - 整理算法更为符合。
   优缺点



[*]长处:联合了不同算法的优势,根据对象的特点选择符合的算法,进步了垃圾回收的服从。
[*]缺点:必要对内存进行分代管理,增长了系统的复杂性。

 
示例代码及阐明

以下是一个简单的 Java 代码示例,展示了对象创建和垃圾回收的情况:

public class GarbageCollectionExample {
    public static void main(String[] args) {
      // 创建大量对象
      for (int i = 0; i < 100000; i++) {
            new Object();
      }
      // 手动触发垃圾回收
      System.gc();
    }
}

在上述代码中,通过循环创建了大量的 Object 对象,这些对象大部分会很快成为垃圾。System.gc() 方法用于手动触发垃圾回收,现实应用中,JVM 会根据内存利用情况主动触发垃圾回收。
不同的垃圾回收算法会在这个过程中发挥作用,以确保内存的有效利用。

 
 
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 详解JVM的底层原理