当前位置:   article > 正文

深入理解Java虚拟机:揭秘JVM的奥秘与力量 !

理解java虚拟机

一.前言

在编程世界中,Java以其“一次编写,到处运行”的特性赢得了广大开发者们的青睐。这背后的功臣,便是Java虚拟机(Java Virtual Machine,简称JVM)。JVM作为Java语言的运行环境,不仅实现了跨平台的特性,更通过其强大的内存管理和垃圾回收机制(GC 机制)、字节码执行、动态类加载等核心功能,赋予了Java程序高效、稳定和灵活的运行特性。本文将带领您深入理解Java虚拟机,揭示其背后的奥秘与力量。

二.JDK/JRE/JVM 三者关系

  1. JDK (Java Development Kit): JDK 是 Java 开发工具包,它提供了完整的 Java 开发环境,包括编译器(javac)、Java 运行环境(JRE)、工具和库。安装了 JDK 后,开发者可以开发、编译并运行 Java 应用程序。

  2. JRE (Java Runtime Environment): JRE 是运行 Java 程序所需的环境,包含了 Java 虚拟机(JVM)、Java 类库以及其他组件。它是运行已开发和编译的 Java 程序所必需的。

  3. JVM (Java Virtual Machine): JVM 是 Java 虚拟机,负责执行编译后的 Java 字节码。它为 Java 应用程序提供了一个平台无关的运行环境,确保 Java 程序可以在任何操作系统上运行而无需重新编译。

三.JVM 的运行流程

   JVM 是如何实现一次编译到处运行的呢 ?

1.字节码生成 :

java的执行流程首先要把源代码编译成字节码文件(.class文件)  字节码是一种中间代码,它与平台无关, 使得java程序具有了跨平台的能力.

2 类加载: (Class Loading)

当Java程序运行时,JVM首先通过类加载器(ClassLoader)将这些字节码文件加载到内存中。类加载器执行以下三个主要活动:

  • 加载:查找字节码文件,并从这些文件创建类在JVM内的表示。
  • 链接:执行验证、准备和(可选的)解析步骤,其中验证确保加载的类符合Java语言规范,准备涉及为类变量分配内存并设置默认初始值。
  • 初始化:如果类具有初始值设定项或静态代码块,它们将被执行。

3. 运行时数据区(Runtime Data Area)

加载类文件后,JVM将数据存储在运行时数据区。此内存区域包括以下几个部分:

  • 堆(Heap):所有的对象实例以及数组都在这里分配内存。
  • 方法区(Method Area):存储每个类的结构如运行时常量池、字段和方法数据、构造函数和普通方法的代码。
  • 栈(Stack):Java栈存储局部变量和部分结果,并参与方法的调用和返回。
  • 程序计数器(Program Counter):每个线程都有一个程序计数器,是线程私有的,指示指令地址。

4. 执行引擎(Execution Engine)

执行引擎是JVM的心脏,负责解释字节码文件。当JVM启动时,执行引擎首先获取字节码文件中的指令,这些指令并不是普通的机器指令,而是一种特定于虚拟机的指令集。执行引擎将这些指令转换(或解释)成可由特定CPU执行的指令。为了提高性能,现代JVM提供了即时编译器(JIT),它可以在运行时将热点代码编译为本地机器代码,以减少解释执行的开销。

四.JVM中的内存区域划分(内存布局)

1) 堆(heap)

堆 是Java虚拟机(JVM)管理的最大的内存区域,用作所有Java程序创建的对象实例和数组的存储空间。这一区域是JVM内存管理的核心,因为它不仅存放对象和数组,还是所有线程共享的不论是哪个线程创建的对象,都在堆上分配内存。此外,堆空间还由垃圾回收器自动管理,确保了内存的有效利用和应用程序的性能优化。

2) 栈 (本地方法栈/虚拟机栈)

1. 虚拟机栈:

每个线程拥有一个独立的虚拟机栈,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的调用和返回对应着栈帧的入栈和出栈。

2)本地方法栈 

在JVM内部,通过c++写的代码,调用 (一般不会关注本地方法栈)

3) 程序计数器

记录当前线程所执行的字节码行号,是线程私有的内存区域。

此块区域较小 专门用来存储下一阶段的要执行的 java 指令地址

4) 元数据区

"元数据" 是计算机中的一个常见术语(Meta data) 往往指的是一些辅助性质的, 描述性质的属性

一个程序有哪些类,每个类都有哪些方法,每个方法里面都要包含哪些指令 这些都记录在元数据区中

 在上述的 JVM 运行时数据区域 堆与元数据区只能有一份,栈跟程序计数器可以有多份(每个线程都有自己的程序计数器)

  1. class Test{
  2. private int n;
  3. private static int m;
  4. }
  5. main(){
  6. Test t = new Test();
  7. }

  1. n - 实例变量:
    • 存储位置:堆(Heap)
    • 解释:n 是 Test 类的非静态成员变量,也被称为实例变量。每次使用 new 关键字创建 Test 类的实例时,都会在堆内存中为 n 分配空间。实例变量随对象一起存在,因此 n 是存储在创建的对象内部的,位于堆内。
  1. m - 静态变量:
    • 存储位置:方法区(Method Area)或元数据区(Metaspace,从 Java 8 开始)
    • 解释:m 是 Test 类的静态变量。静态变量不属于任何单个实例,而是类级别的,因此在所有实例中共享。它们被存储在方法区(在 Java 8 之前称为永久代,PermGen)或从 Java 8 开始,可能存储在元数据区(Metaspace)。这个区域用于存储类信息、常量以及静态变量。
  1. t - 局部变量:
    • 存储位置:虚拟机栈(JVM Stack)中的局部变量表
    • 解释:t 是在 main 方法中声明的局部变量,用于存储对 Test 类实例的引用。局部变量存储在调用该方法的线程的虚拟机栈上,具体地说,在该方法的局部变量表中。

 五. 类加载机制

 1) 加载

  1. 通过一个类的全限定名去找到其对应的.class文件
  2. 将这个.class文件内的二进制数据读取出来,转化成方法区的运行时数据结构
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

 2) 验证

验证是连接阶段的第⼀步,这⼀阶段的⽬的是确保Class⽂件的字节 流中包含的信息符合《Java虚拟机 规范》的全部约束要求,保证这些信 息被当作代码运⾏后不会危害虚拟机⾃⾝的安全。

          

                    下图为官方java虚拟机规范

 3) 准备 (给类对象 申请空间)

准备阶段是正式为类中定义的变量(即为static修饰的变量 静态变量) 分配内存并设置类变量初始值的阶段

例如 public static int n = 123;

此时他的初始化的值为0 而非123.

4) 解析

解析阶段是Java虚拟机将常量池内的符合引用替换成为直接引用的过程,也就是初始化常量的过程.

5) 初始化

初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应⽤程序。初始 化阶段就是执行类构造器⽅法的过程。

 

 六. 双亲委派模型(如何查找.class文件的策略)

JVM中进行类加载的操作,是有一个专门的模块,称为"类加载器"(ClassLoader)

类加载器的主要作用是根据提供的全限定类名(例如 java.lang.String),找到并加载相应的 .class 文件到 Java 虚拟机中。

JVM中的类加载器默认有三个的(可以自定义)

BootstrapClassLoader : 负责查找标准库的目录

ExtensionClassLoader : 负责查找拓展库的目录

ApplicationClassLoader : 负责查找当前项目的代码目录以及第三方库的目录

类加载器的父子关系

这种层次结构和委托关系确保了 Java 环境中类加载的安全性和有序性。

当一个类加载器收到类加载请求时,它首先会委托给其父加载器来尝试加载该类。

只有当父加载器无法加载该类时,当前加载器才会尝试自行加载。这种机制也帮助避免了类的重复加载。

双亲委派模型工作过程:

  1. 类加载请求的开始
    • 当应用需要加载一个类时,ApplicationClassLoader 通常是处理这个请求的第一个类加载器。
  1. 委派给父类加载器
    • ApplicationClassLoader 并不立即尝试加载这个类,而是首先将这个请求委托给它的父加载器,即 ExtensionClassLoader
  1. 逐级委派
    • ExtensionClassLoader 也采取同样的策略:它继续将加载任务委托给它的父加载器,即 BootstrapClassLoader
  1. 顶级加载器尝试加载
    • BootstrapClassLoader 是顶级加载器,在它发现没有更高级的父加载器后,开始在它负责的目录(JRE 的 lib 目录中的核心 Java 库)中查找和尝试加载指定的类。
  1. 向下回溯尝试加载
    • 如果 BootstrapClassLoader 不能加载该类,控制权会回溯到 ExtensionClassLoaderExtensionClassLoader 接着尝试在它负责的目录(JRE 的 lib/ext 目录或者通过 java.ext.dirs 系统属性指定的目录)中加载类。
    • 如果 ExtensionClassLoader 也加载失败,最后会回溯到 ApplicationClassLoader,它将尝试在应用的类路径(classpath)中加载类,包括所有的项目目录和第三方库目录。
  1. 处理加载失败
    • 如果所有的类加载器都不能加载所需的类,最终 ApplicationClassLoader 会抛出 ClassNotFoundException,表示类加载过程失败。

这个模型确保了类加载的安全性,因为核心 Java 类库的类不能被覆盖。同时,这种机制也保证了 Java 类的唯一性,因为每个类首先由 BootstrapClassLoader 尝试加载,确保了 Java 运行时环境的一致性和稳定性。此外,双亲委派模型可以防止恶意代码替换核心库中的类,增强了运行时的安全性。

 

   

                     怀着期待出发,等待下一次再见 ~~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/837199
推荐阅读
相关标签
  

闽ICP备14008679号