JVM
JVM 简介
JVM(Java Virtual Machine)
意为 Java虚拟机,是一种在计算机上运行Java程序的虚拟计算机或运行时环境。它充当了Java应用程序和计算机硬件之间的中间层,负责解释和执行Java源代码,并为Java应用程序提供了跨平台的能力,使得相同的Java程序可以在不同操作系统上运行,而无需重新编写或修改源代码。
JVM整体结构
类加载器
:加载class文件。
运行时数据区
:包括程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。
执行引擎
:执行字节码或者本地方法。
JVM 运行时数据区
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域
程序计数器
作用
:用于记录当前线程执行的字节码指令地址。特点
:程序计数器是线程私有的,每个线程都有一个程序计数器。它在多线程环境下用于指导线程执行,例如跳转到下一条指令的位置。垃圾回收
:程序计数器不涉及内存分配,因此不需要垃圾回收。
Java虚拟机栈
作用
:用于存储方法调用的局部变量、操作数栈、方法出口等。特点
:每个线程都有自己的栈,用于存储局部变量和方法调用的状态信息。栈是线程私有的,当一个方法被调用时,一个新的栈帧被压入栈,当方法返回时,栈帧被弹出。垃圾回收
:栈上的数据是线程私有的,不需要垃圾回收。
本地方法栈
作用
:用于执行本地方法(Native Method)。特点
:本地方法栈类似于栈,但它是为本地方法服务的,本地方法是用非Java语言编写的方法,通常由JNI(Java Native Interface)调用。垃圾回收
:和栈一样,本地方法栈上的数据也是线程私有的,不需要垃圾回收。
堆
作用
:用于存储Java对象实例。特点
:堆是一个大的内存池,可以动态分配和释放内存空间。它是Java中最常用的内存区域,用于存储实例对象,包括对象的成员变量和实例方法。垃圾回收
:堆内存中的对象不再被引用时,会被垃圾回收机制自动回收释放内存空间。
方法区
作用
:用于存储类的信息、静态变量、常量池等数据。特点
:方法区包含了类的结构信息,如字段和方法信息,以及常量池,其中存储了类中的字面常量和符号引用。它是各个线程共享的区域。垃圾回收
:JVM规范并没有明确要求方法区必须进行垃圾回收,但一些JVM实现可能会执行方法区的垃圾回收操作。
类加载机制
在Java虚拟机中,类加载过程
分为以下几个阶段,每个阶段负责不同的任务:
1. 加载(Loading)
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.langClass
对象,作为方法区这个类的各种数据的访问入口。
2. 验证(Verification)
- 目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,
文件格式验证
,元数据验证
,字节码验证
,符号引用验证
。
3. 准备(Preparation)
- 为类变量分配内存并且设置该类变量的默认初始值,即零值。
- 这里不包含用
final
修饰的static
,因为final
在编译的时候就会分配了,准备阶段会显式初始化; - 这里不会为
实例变量
分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
4. 解析(Resolution)
- 将常量池内的
符号引用
转换为直接引用
的过程。 - 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等
5. 初始化(Initialization)
- 初始化阶段就是执行类构造器方法
<clinit>()
的过程。 - 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
- ()不同于类的构造器。
- 若该类具有父类,JVM会保证子类的()执行前,父类的 ()已经执行完毕。
- 虚拟机必须保证一个类的 ()方法在多线程下被同步加锁。
类加载器
启动类加载器(Bootstrap ClassLoader)
- 这个类加载器由JVM自身实现,不是Java类,无法直接访问。
- 它用来加载Java的核心库(
JAVA HOME/jre/ lib/rt.jar
、resources.jar
或sun.boot.class.path
路径下的内容),用于提供JVM自身需要的类。 - 不继承
java.lang.ClassLoader
,没有父加载器。 - 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 出于安全考虑,
Bootstrap
启动类加载器只加载包名为java
、javax
、sun
等开头的类。
ClassLoader bootstrapClassLoader = String.class.getClassLoader();
System.out.println(bootstrapClassLoader);// null
扩展类加载器(Extension ClassLoader):
- Java语言编写,由
sun.misc.Launcher$ExtClassLoader
实现。 - 派生于
ClassLoader
类。 - 父类加载器为启动类加载器。
- 从
java.ext.dirs
系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext
子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
ClassLoader extClassLoader = SunEC.class.getClassLoader();
System.out.println(extClassLoader);// sun.misc.Launcher$ExtClassLoader@53d8d10a
应用程序类加载器(Application ClassLoader):
- java语言编写,由
sun.misc.Launcher$AppClassLoader
实现。 - 派生于
ClassLoader
类。 - 父类加载器为扩展类加载器。
- 它负责加载环境变量classpath或系统属性
java.class.path
指定路径下的类库。 - 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。
- 通过
ClassLoader#getsystemclassLoader()
方法可以获取到该类加载器
ClassLoader appClassLoader = App.class.getClassLoader();
System.out.println(appClassLoader);// sun.misc.Launcher$AppClassLoader@18b4aac2
双亲委派机制
类的加载是通过双亲委派模型来完成的,双亲委派模型即为下图所示的类加载器之间的层次关系。
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优势:
- 避免类的重复加载。
- 保护程序安全性,使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改