深入了解Java虚拟机(JVM)

本文详细介绍了Java虚拟机(JVM)的工作原理,包括内存区域的划分(如Java堆、栈、本地方法栈、程序计数器和元数据区),类加载机制(包括加载、验证、准备、解析和初始化),以及垃圾回收机制(如可达性分析和分代回收策略)。还探讨了JVM的双亲委派模型,确保了类加载的安全性和稳定性。
摘要由CSDN通过智能技术生成

Java虚拟机(JVM)是Java程序运行的核心组件,它负责解释执行Java字节码,并在各种平台上执行。JVM的设计使得Java具有跨平台性,开发人员只需编写一次代码,就可以在任何支持Java的系统上运行。我们刚开始学习Java时就下载了JDK(Java开发工具包),它提供了编译、调试和运行Java应用程序所需的工具和库。JDK包括了JRE(Java运行时环境),JRE包含了Java虚拟机(JVM)。由于不同的CPU的指令集可能不同,所以相同的代码可能不能在不同的系统上都正常运行,而JVM就是为了解决这个问题,Java会先通过javac把  .java 文件编译为 java字节码(相当于Java自己的一套CPU指令)然后再由具体系统平台上的的JVM(不同系统上的JVM可能存在差异),把上述字节码转化为对应的CPU能识别的机器指令。

1. JVM中的内存区域划分 

 JVM其实也是一个进程(任务管理器中看到的java进程),Java程序的执行时申请的内存就是JVM从系统这边申请到的内存,JVM会先申请一块大的内存,这块内存在给Java程序使用时,又会根据实际的用图划分出不同的区域,每个区域都有不同的作用。

  1. Java堆(Java Heap): Java堆是Java虚拟机管理的最大一块内存区域,用于存放对象实例,数组,类的成员变量。Java堆是所有线程共享的内存区域,是垃圾回收的重点区域。

  2. Java虚拟机栈(Java Virtual Machine Stacks): Java虚拟机栈也称为栈内存,用于存储线程的方法调用、局部变量、部分结果等。每个方法在被调用时都会创建一个栈帧,并入栈;方法执行完毕后栈帧出栈。栈帧包括局部变量表、操作数栈、动态链接、方法返回地址等。

  3. 本地方法栈(Native Method Stack): 本地方法栈类似于Java虚拟机栈,但是它为Native方法服务,即JVM内部使用C、C++等编写的本地方法。

  4. 程序计数器(Program Counter Register): 程序计数器是一块较小的内存区域,用于记录当前线程下一条执行的字节码指令地址。在多线程环境下,每个线程都有独立的程序计数器。

  5. 元数据区(Metaspace)/ 方法区(Method Area):元数据指的是一些辅助性质的描述性质的属性,元数据区主要用于存储类的元数据信息,例如类的结构、方法信息、字段信息等。一个程序有哪些类,每个类中有哪些方法,每个方法里要包含哪些指令,都会记录在元数据区中,即元数据区储存了Java代码编译后的Java字节码

 注意:一个JVM进程中,堆和元数据区只有一个,栈和程序计数器可能有多份(每个线程都有一个自己的程序计数器和栈,即每个线程都有自己的执行流)。

public class Test{
    private int n;//堆区
    private static int m;//static修饰的变量为类属性储存在元数据区
    
    public static void main(String[] args) {
        Test t = new Test();//t为局部变量储存在栈区,类的实例储存在堆区
    }
}

2.JVM的类加载机制 

2.1 类加载过程 

JVM的类加载机制是指JVM在运行时将类的字节码加载到内存中并进行验证、准备、解析和初始化的过程。JVM的类加载机制主要包括以下几个步骤:

  1. 加载(Loading):查找并加载类的字节码文件。这个过程可以通过类加载器来完成,类加载器会根据类的全限定名在文件系统、网络或其他地方找到对应的字节码文件,并将其读入内存。

  2. 验证(Verification):确保被加载的类的字节码是合法、符合JVM规范的。包括文件格式验证、元数据验证、字节码验证、符号引用验证等步骤。具体验证依据,在Java虚拟机规范中有明确的格式说明

  3. 准备(Preparation):为类的静态变量分配内存并设置默认初始值,这些变量所使用的内存都将在方法区中进行分配。

  4. 解析(Resolution):将类中的符号引用转换为直接引用,这个过程可以在运行时进行也可以在编译时进行。

    class Test{
        String s = "hello";
    }

    上面代码编译后"hello"会储存在常量池中,s中相当于保存了“hello"的字符串常量的地址,但是代码没有运行时,s和"hello"都在字节码文件中,文件中没有地址这样的概念,所以在代码运行前s中存储的是一个类似于”偏移量"的概念记录了“hello”的相对位置,就是这里的符号引用。

  5. 初始化(Initialization):对类进行初始化,包括执行类构造器<clinit>()方法,静态变量赋值等操作。在初始化阶段,JVM会根据程序中对类的主动使用情况来触发初始化,例如创建类的实例、访问类的静态成员、调用类的静态方法等。

2.2 双亲委派模型

在类加载过程中,JVM采用了双亲委派模型,即由多个不同层次的类加载器组成一个层次结构,每个类加载器都有自己的责任范围,当一个类需要加载时,先由最顶层的类加载器尝试加载,如果无法加载再交由下一层的类加载器,依次类推,直到最底层的类加载器。

JVM中进行类加载是由一个专门的模块“类加载器(ClassLoader)”完成的,类加载器的作用是通过“全限定类名”(带有包名的类名,例如java.land.String,可以类比为文件路径中的绝对路径)查找 .class文件,把 .class文件的数据转化为运行时需要的类对象并加载到JVM中。

JVM中默认提供了三种类加载器,分别是:

  1. 启动类加载器(Bootstrap ClassLoader):负责加载Java的核心类库,如java.lang包下的类。它是JVM自身的一部分,通常由C++编写,并不继承自java.lang.ClassLoader类。

  2. 扩展类加载器(Extension ClassLoader):负责加载Java的扩展类库,位于jre/lib/ext目录下的类库。

  3. 应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载当前项目的代码目录,以及第三方库的目录。

除了这三个默认的类加载器,开发者也可以自定义类加载器来实现特定的类加载需求,比如从网络中动态加载类、加密类加载等。自定义类加载器需要继承自java.lang.ClassLoader类,并重写其中的findClass()方法来实现类的加载逻辑。 

注意:这三个类加载器之间存在父子关系,上面的为父加载器,下面的为子加载器,即1是2的父亲,2是3的父亲。

双亲委派流程:当一个类加载器收到类加载请求时,它首先将这个请求委托给它的父类加载器处理。如果父类加载器无法完成此加载请求,子加载器才会尝试自己去加载。这个过程会一直递归下去直到启动类加载器。这样做的目的是保证Java核心API的稳定性,防止用户自定义的类替换掉核心类库中的类。

1. 从Application ClassLoader作为入口

2. Application ClassLoader不会立刻搜索自己负责的目录,会把任务交给父类加载器Extension ClassLoader。

3. Extension ClassLoader也不会立刻搜索自己负责的目录,也会把任务交给父类加载器Bootstrap ClassLoader。

4.Bootstrap ClassLoader没有父类加载器,就会搜索自己负责的目录查找需要的 .class文件,如果找到了就直接进入打开文件/读文件等流程中,如果没找到,则把任务交给下一级类加载器Extension ClassLoader继续尝试寻找。

5.Extension ClassLoader 接受到任务此时就会在自己负责的目录中开始寻找,如果找到了就直接进入打开文件/读文件等流程中,如果没找到,则同样把任务交给下一级类加载器Application ClassLoader继续尝试寻找。

6.Application ClassLoader 也会在自己负责的目录中开始寻找,如果找到了就直接进入打开文件/读文件等流程中,如果没找到,也会尝试把任务交给下一级,但是默认情况下Application ClassLoader没有下一级类加载器了,于是就会类加载失败抛出ClassNotFoundException异常

上述流程就保证了类加载的顺序,防止用户自定义的类替换掉核心类库中的类。例如用户自己定义了一个java.lang.String如果这个类先被加载了,java核心库中的String类就不会被加载。

3. 垃圾回收机制(GC) 

垃圾回收(GC)是自动内存管理的关键技术之一。它负责清理不再使用的对象,释放内存空间。垃圾回收,回收的是堆的内存 ,所以也可以说是回收对象。

3.1 识别垃圾 

判定对象后续是否会继续使用,不会继续使用的就会被视为垃圾,如果一个对象没有任何引用指向它,那么这个对象就无法被继续使用了,也就会被视为垃圾。

3.1.1 引用计数

引用计数方法并没有在JVM中使用,但是广泛运用在其他主流语言的垃圾回收机制中,如Python,PHP。

引用计数是通过给每个对象安排一个额外的空间,记录当前有几个引用指向该对象。每有一个引用指向该对象时,就把值加一,反之则减一,当这个值为0时则视为垃圾,当负责垃圾回收的扫描线程获取到这个对象的引用计数情况时,发现为0就会释放这个对象的空间。

引用计数存在两个关键的问题:

  1. 问题一:要给每个对象安排计数器,就会消耗额外的空间,如果对象数量很多,总的空间浪费也就很多。
  2. 问题二:可能产生循环引用问题,例如两个对象互相引用,但没有一个外部的引用指向它们,此时这两个对象是无法被获取到的,但他们的引用计数又不为0,也就不会被释放。
3.1.2 可达性分析 

JVM采用的就是可达性分析来识别对象是否是垃圾。

可达性分析采用的方法是 遍历所有变量,JVM会遍历所有能够被直接或者间接访问到的对象,能访问到的自然不是垃圾,遍历一圈后不能访问到的就视为垃圾。

3.2 释放垃圾 

找到垃圾以后就需要把垃圾对象所占的内存空间进行释放。

3.2.1 标记-清除算法

把标记为垃圾的对象直接释放。这种释放方式会导致内存碎片问题。

如图所示,释放后,会导致出现很多大大小小的内存碎片,而内存申请都是一次申请一段连续的内存空间,这就导致了部分内存碎片可能无法使用到,也就导致了空间浪费。

3.2.2 复制算法

将内存分为两个相等的部分,每次只使用其中一半。当这一半的内存用完后,就将还在使用的对象复制到另一半,然后再清除掉已经使用过的那一半内存中的所有对象。

这种方法避免了内存碎片,但是总的可用空间变少了,同时复制对象也会消耗时间。

3.2.3 标记-整理算法

将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

 这样也可以避免内存碎片问题,但是移动对象也要消耗时间。

3.2.4 分代回收算法

JVM中采用的是分代回收算法。给每个对象引入年龄的概念,JVM中存在专门的线程负责周期性的扫描/释放对象,如果一个对象被线程扫描了一次,并且不是垃圾,该对象的年龄就会+1(初始年龄为0)。

JVM中会根据对象年龄的差异,把整个内存分成两个大的部分,新生代(年龄较小的对象)/ 老年代(年龄较大的对象),新生代又被划分为三个区域,其中大的一部分区域为 伊甸区 ,剩下两块大小相同的区域叫做  生存区  或者  幸存区 。

 

 新的对象都是从伊甸区中被创建的,第一轮GC扫描后,没有被清除的对象就会被通过复制算法移动到生存区(即生存区相当于未被使用的那块内存),生存区中的对象下次被GC扫描后,存活的对象又会被通过复制算法移动到另一个生存区(注意两个生存区完全是对等的),每经历一次GC,对象的年龄就会+1,如果某个对象在生存区中经过了若干轮GC任然没有被清除,JVM就会认为这个对象的生命周期很长,就会把这个对象移动到老年代,老年代的对象也会被GC扫描,只不过扫描的频率较低。老年代的对象被视为垃圾时会按照标记-整理算法释放内存。

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
深入java虚拟机第二版 第1章 Java体系结构介绍 1.1 为什么使用Java 1.2 网络带来的挑战和机遇 1.3 体系结构 1.3.1 Java虚拟机 1.3.2 类装载器的体系结构 1.3.3 Java class文件 1.3.4 Java API 1.3.5 Java程序设计语言 1.4 Java体系结构的代价 1.5 结论 1.6 资源页 第2章 平台无关 2.1 为什么要平台无关 2.2 Java的体系结构对平台无关的支持 2.2.1 Java平台 2.2.2 Java语言 2.3.3 Java class文件 . 2.2.4 可伸缩性 2.3 影响平台无关性的因素 2.3.1 Java平台的部署 2.3.2 Java平台的版本 2.3.3 本地方法 2.3.4 非标准运行时库 2.3.5 对虚拟机的依赖 2.3.6 对用户界面的依赖 2.3.7 Java平台实现中的bug 2.3.8 测试 2.4 平台无关的七个步骤 2.5 平台无关性的策略 2.6 平台无关性和网络移动对象 2.7 资源页 第3章 安全 3.1 为什么需要安全性 3.2 基本沙箱 3.3 类装载器体系结构 3.4 class文件检验器 3.4.1 第一趟:class文件的结构检查 3.4.2 第二趟:类型数据的语义检查 3.4.3 第三趟:字节码验证 3.4.4 第四趟:符号引用的验证 3.4.5 二进制兼容 3.5 Java虚拟机中内置的安全特性 3.6 安全管理器和Java API 3.7 代码签名和认证 3.8 一个代码签名示例 3.9 策略 3.10 保护域 3.11 访问控制器 3.11.1 implies()方法 3.11.2 栈检查示例 3.11.3 一个回答“是”的栈检查 3.11.4 一个回答“不”的栈检查 3.11.5 doPrivileged()方法 3.11.6 doPrivileged()的一个无效使用 3.12 Java安全模型的不足和今后的发展 方向 3.13 和体系结构无关的安全性 3.14 资源页 第4章 网络移动性 4.1 为什么需要网络移动性 4.2 一种新的软件模式 4.3 Java体系结构对网络移动性的支持 4.4 applet:网络移动性代码的示例 4.5 Jini服务对象:网络移动对象的示例 4.5.1 Jini是什么 4.5.2 Jini如何工作 4.5.3 服务对象的优点 4.6 网络移动性:Java设计的中心 4.7 资源页 第5章 Java虚拟机 5.1 Java虚拟机是什么 5.2 Java虚拟机的生命周期 5.3 Java虚拟机的体系结构 5.3.1 数据类型 5.3.2 字长的考量 5.3.3 类装载器子系统 5.3.4 方法区 5.3.5 堆 5.3.6 程序计数器 5.3.7 Java栈 5.3.8 栈帧 5.3.9 本地方法栈 5.3.10 执行引擎 5.3.11 本地方法接口 5.4 真实机器 5.5 一个模拟:“Eternal Math” 5.6 随书光盘 5.7 资源页 第6章 Java class文件 6.1 Java class文件是什么 6.2 class文件的内容 6.3 特殊字符串 6.3.1 全限定名 6.3.2 简单名称 6.3.3 描述符 6.4 常量池 6.4.1 CONSTANT_Utf8_info表 6.4.2 CONSTANT_Integer_info表 6.4.3 CONSTANT_Float_info表 6.4.4 CONSTANT_Long_info表 6.4.5 CONSTANT_Double_info表 6.4.6 CONSTANT_Class_info表 6.4.7 CONSTANT_String_info表 6.4.8 CONSTANT_Fieldref_info表 6.4.9 CONSTANT_Methodref_info表 6.4.10 CONSTANT_InterfaceMethodref_ info表 6.4.11 CONSTANT_NameAndType_info 表 6.5 字段 6.6 方法 6.7 属性 6.7.1 属性格式 6.7.2 Code属性 6.7.3 ConstantValue属性 6.7.4 Deprecated属性 6.7.5 Exceptions属性 6.7.6 InnerClasses属性 6.7.7 LineNumberTable属性 6.7.8 LocalVariableTable属性 6.7.9 SourceFile属性 6.7.10 Synthetic属性 6.8 一个模拟:“Getting Loaded” 6.9 随书光盘 6.10 资源页 第7章 类型的生命周期 7.1 类型装载、连接与初始化 7.1.1 装载 7.1.2 验证 7.1.3 准备 7.1.4 解析 7.1.5 初始化 7.2 对象的生命周期 7.2.1 类实例化 7.2.2 垃圾收集和对象的终结 7.3 卸载类型 7.4 随书光盘 7.5 资源页 第8章 连接模型 8.1 动态连接和解析 8.1.1 解析和动态扩展 8.1.2 类装载器与双亲委派模型 8.1.3 常量池解析 8.1.4 解析CONSTANT_Class_info入口 8.1.5 解析CONSTANT_Fieldref_info 入口 S.1.6 解析CONSTANT_Methodref_info 入口 8.1.7 解析CONSTANT_Interface- Methodref_info入口 8.1.8 解析CONSTANT_String_info入口 8.1.9 解析其他类型的入口 8.1.10 装载约束 8.1.11 编译时常量解析 8.1.12 直接引用 8.1.13 _quick指令 8.1.14 示例:Salutation程序的连接 8.1.15 示例:Greet程序的动态扩展 8.1.16 使用1.1版本的用户自定义类装 载器 8.1.17 使用1.2版本的用户自定义类装 载器 8.1.18 示例:使用forName()的动态扩展 8.1.19 示例:卸载无法触及的greeter类 8.1.20 示例:类型安全性与装载约束 8.2 随书光盘 8.3 资源页 第9章 垃圾收集 9.1 为什么要使用垃圾收集 9.2 垃圾收集算法 9.3 引用计数收集器 9.4 跟踪收集器 9.5 压缩收集器 9.6 拷贝收集器 9.7 按代收集的收集器 9.8 自适应收集器 9.9 火车算法 9.9.1 车厢、火车和火车站 9.9.2 车厢收集 9.9.3 记忆集合和流行对象 9.10 终结 9.11 对象可触及性的生命周期 9.11.1 引用对象 9.11.2 可触及性状态的变化 9.11.3 缓存、规范映射和临终清理 9.12 一个模拟:“Heap of Fish” 9.12.1 分配鱼 9.12.2 设置引用 9.12.3 垃圾收集 9.12.4 压缩堆 9.13 随书光盘 9.14 资源页 第10章 栈和局部变量操作 10.1 常量入栈操作 10.2 通用栈操作 10.3 把局部变量压入栈 10.4 弹出栈顶部元素,将其赋给局部变量 10.5 wide指令 10.6 一个模拟:“Fibonacci Forever” 10.7 随书光盘 10.8 资源页 第11章 类型转换 11.1 转换操作码 11.2 一个模拟:“Conversion Diversion” 11.3 随书光盘 11.4 资源页 第12章 整数运算 12.1 二进制补码运算 12.2 Inner Int:揭示Java int类型内部性质 的applet 12.3 运算操作码 12.4 一个模拟:“Prime Time” 12.5 随书光盘 12.6 资源页 第13章 逻辑运算 13.1 逻辑操作码 13.2 一个模拟:“Logical Results” 13.3 随书光盘 13.4 资源页 第14章 浮点运算 14.1 浮点数 14.2 Inner Float:揭示Java float类型内部 性质的applet 14.3 浮点模式 14.3.1 浮点值集合 14.3.2 浮点值集的转换 14.3.3 相关规则的本质 14.4 浮点操作码 14.5 一个模拟:“Circle of Squares” 14.6 随书光盘 14.7 资源页 第15章 对象和数组 15.1 关于对象和数组的回顾 15.2 针对对象的操作码 15.3 针对数组的操作码 15.4 一个模拟:“Three—Dimensional Array” 15.5 随书光盘 15.6 资源页 第16章 控制流 16.1 条件分支 16.2 五条件分支 16.3 使用表的条件分支 16.4 一个模拟:“Saying Tomato” 16.5 随书光盘 16.6 资源页 第17章 异常 17.1 异常的抛出与捕获 17.2 异常表 17.3 一个模拟:“Play Ball!” 17.4 随书光盘 17.5 资源页 第18章 finally子句 18.1 微型子例程 18.2 不对称的调用和返回 18.3 一个模拟:“Hop Around” 18.4 随书光盘 18.5 资源页 第19章 方法的调用与返回 19.1 方法调用 19.1.1 Java方法的调用 19.1.2 本地方法的调用 19.2 方法调用的其他形式 19.3 指令invokespecial 19.3.1 指令invokespecial和[init]()方法 19.3.2 指令invokespecial和私有方法 19.3.3 指令invokespecial和super关键字 19.4 指令invokeinterface 19.5 指令的调用和速度 19.6 方法调用的实例 19.7 从方法中返回 19.8 随书光盘 19.9 资源页 第20章 线程同步 20.1 监视器 20.2 对象锁 20.3 指令集中对同步的支持 20.3.1 同步语句 20.3.2 同步方法 20.4 Object类中的协调支持 20.5 随书光盘 20.6 资源页 附录A 按操作码助记符排列的指令集 附录B 按功能排列的操作码助记符 附录C 按操作码字节值排列的操作码助
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ting-yu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值