JVM整体结构和JMM内存模型
1、什么是JVM
JVM(Java Virtual Machine,Java虚拟机)是执行Java字节码的虚拟环境,它将Java源代码编译成字节码,使得程序可以在各种操作系统和硬件上跨平台运行,而无需重新编译。JVM负责加载字节码、解释或编译字节码为机器码,并提供自动内存管理、垃圾回收等功能。
JVM的主要功能: 跨平台支持、自动内存管理、性能优化、多线程支持、安全性以及诊断和监控。
2、JVM整体结构
JVM (Java Virtual Machine)的整体结构可以分为五个主要部分:类加载子系统、运行时数据区、执行引擎、本地方法接口和运行时类库。分别负责不同的功能模块,为 Java 程序的执行提供支持。
2.1、类加载子系统
类加载子系统负责将Java字节文件 (.class文件)加载到JVM中,将其转换JVM能够执行的类对象。类加载过程按需加载,解决类的依赖关系。
类加载过程
(1) 加载: 通过类的全限定名获取类的字节码,并将其加载到内存中,生成 Class 对象。
(2) 验证: 校验字节码文件的正确性。
(3) 准备: 给类的静态变量分配内存,并赋予默认值。
(4) 解析: 将常量池的符号引用替换直接引用。
(5) 初始化: 执行类中的静态初始化块,给静态变量赋值。
四种类加载器
(1) 引导类加载器(Bootstrap ClassLoader): 负责加载支撑JVM运行的核心类库,位于JRE的lib目录,比如rt.jar、charsets.jar等。
(2) 扩展类加载器(Extension ClassLoader): 负责加载支撑JVM运行的扩展目录中的JAR类包,位于JRE的lib目录下的ext目录。
(3) 应用类加载器(Application ClassLoader): 负责加载ClassPath路径下的类包,主要加载自己写的类。
(4) 自定义类加载器(Custom ClassLoader): 负责加载用户自定义路径下的类包。
类加载的双亲委派机制
工作原理: 加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载。如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载器路径中查找并载入目标类。
目的: 避免类的重复加载,保证被加载类的唯一性。
优势: 沙箱安全机制,防止核心API库被随意篡改,比如自己写的java.lang.String.Class
类不会被加载。
2.2、运行时数据区
运行时数据区包含五部分:线程共享的堆和方法区,线程私有的虚拟机栈、本地方法栈和程序计数器。
(1) 堆(Heap): 存储所有对象实例和数组。管理不同生命周期的对象,分年轻代(Eden、Survivor)和老年代。
(2) 方法区(Method Area): 存储类的元数据:类结构、方法定义、静态变量和运行时常量池等。
(3) 虚拟机栈(JVM Stack): 存储方法调用时的局部变量、操作数栈和方法返回地址等数据。数据以栈帧形式存在,每个栈帧对应一个方法调用。
(4) 本地方法栈(Native Method Stack): 支持本地方法的调用,存储调用本地方法时的局部变量、操作数栈等数据。执行非Java的本地方法。
(5) 程序计数器(Program Counter, PC Register): 记录当前线程执行的字节码指令的地址,随线程切换改变。
2.3、执行引擎
执行引擎执行Java字节码,将字节码转换为机器指令。由解释器、即时编译器和垃圾收集器组成。
解释器: 将字节码逐条解释为机器指令并执行,执行未经过JIT编译的字节码。
即时编译器: 将高频调用的字节码块编译为机器码,将热点代码直接转换为本地机器代码。
垃圾收集器: 执行垃圾回收,释放不再使用的对象占用的内存。
2.4、本地方法接口
本地方法接口提供与本地代码(如C、C++)进行交互的接口,允许Java调用非Java代码。
使用场景: 访问底层系统资源可已有的本地库(如操作系统API),进行高性能操作。
2.5、运行时类库
提供支持Java应用程序运行的核心类库,比如Java核心API、I/O网络和数据结构等。通常以.jar文件形式提供。
3、JMM内存模型
3.1、JMM简介
JMM内存模型 (Java Memory Model,简称JMM) 是Java语言中用于定义线程间通信与共享变量可见性的一种规范,目的是保证在多线程环境下数据的一致性和内存访问的有序性。JMM对变量的存储和读写进行了明确规定,并引入了“主内存”和“工作内存”的概念。
主内存(Main Memory): 所有线程共享的存储区域,所有变量(实例变量、静态变量等)都存储在主内存中。线程在主内存中的变量副本上进行操作,通过主内存实现不同线程间的通信。
工作内存(Working Memory): 每个线程都有自己的工作内存(类似于CPU缓存),其中存放了主内存中变量的副本。线程对变量的所有操作(读取、写入)都必须在工作内存中进行,而不是直接操作主内存。每个线程只能在自己的工作内存中看到这些变量的修改。
3.2、JMM关键问题
(1) 可见性: 保证当一个线程修改了变量后,其他线程能够立即看到变化。在JMM中通过volatile
关键字、锁(如synchronized
、ReentrantLock
)实现可见性。
(2) 有序性: 保证代码的执行顺序与预期一致,避免重排序带来的执行不一致。JMM在没有依赖时允许指令重排序优化,但是通过“happens-before”规则确保在并发下操作的顺序。volatile
和锁机制也可以保证有序性。
(3) 原子性: 保证操作不可分割,要么完全执行要么完全不执行。简单变量的读取和写入是原子性的,但复合操作如自增等不是。锁和CAS操作可以确保复合操作的原子性。
3.3、内存操作规则
线程间通信规则
- 线程对共享变量的读写需要经过主内存,线程的工作内存存储着共享变量的副本。
- 所有的修改必须先同步回主内存,其他线程才能看到修改后的值。
happens-before 规则
JMM通过“Happens-Before”规则定义操作的顺序,来保证多线程环境下内存的可见性和一致性:
(1) 程序次序规则:单线程内,按代码的顺序执行。
(2) 锁定规则:一个锁的解锁操作,happens-before 后续对这个锁的加锁操作。
(3) volatile变量规则:对一个volatile
变量的写操作,happens-before 后续对该变量的读操作。
(4) 传递性:如果A happens-before B,B happens-before C,那么A happens-before C。
(5) 线程启动规则:Thread对象的start()方法happens-before线程的每一个操作。
(6) 线程中断规则:线程的interrupt()调用,happens-before 检查中断的代码。
4、我的公众号
敬请关注我的公众号:大象只为你,持续更新技术知识…