1. JVM组成
Java虚拟机可任意运行任何符合规则的字节码文件(.class),它是用户应用程序和操作系统之间的桥梁,负责将程序编译得到的字节码文件转换为计算机可以运行的机器码并进行运行。
总体来说,JVM可以分为三部分:
- 类加载子系统(ClassLoader)
- 运行时数据区(Runtime Data Areas)
- 执行引擎(Execution Engine)
详细的组成如下所示:
2. 类装载子系统
类装载子系统负责从文件系统或网络中加载编译生成的字节码文件,加载的.class
文件在文件的开头有特定的标识。但ClassLoader只负责.class
文件的加载,文件是否可以成功运行由执行引擎决定。加载的类型信息会存放于方法区(或元空间),方法区中不仅存放类信息,还会存放常量池信息,包含数字常量和字符串常量(JDK8后字符串常量池移到了堆空间中)。
ClassLoader加载.class
文件的步骤:
- 通过类的全限定类名获取此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据
- 在内存中生成一个代表该类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
那么如何通过ClassLoader来加载字节码文件使用呢?下面通过一个例子来看一下:
当我们定义了一个Car类,并通过编译器生成Car.class
文件后,首先通过ClassLoader加载并初始化得到Car的类对象,通过反射机制调用Car类中的构造器就可以实例化类对象,类对象将保存到堆中供程序使用。我们可以使用Class对象的getClassLoader()
获取到使用的ClassLoader,以及类对象可以通过getClass()
获取对应的类对象。
public class Car {
String name;
double price;
public static void main(String[] args) {
ClassLoader classLoader = new Car().getClass().getClassLoader();
System.out.println(classLoader); // jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
Car car = new Car();
final Class<? extends Car> aClass = car.getClass();
System.out.println(aClass); // class ClassLoader.Car
}
}
ClassLoader整体的过程又可以分为加载(Loading)、链接(Linking)和初始化(Initialization)三个阶段。其中Linking又可以分为验证、准备和解析三部分。
2.1 验证
要想使用JVM运行生成的字节码文件,那么通过ClassLoader加载得到的字节流中的信息应满足JVM的规范,从而保证类的正确性,不会危害虚拟机本身。同时为了防范恶意攻击,也需要验证的存在。验证部分主要包括如下四种类型:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
2.2 准备
准备阶段主要完成的内容是为类的静态变量分配内存并设置该变量的默认初始值,但如果变量使用final static关键字进行修饰,默认初始值的分配在编译阶段完成;而且不会为实例变量设置初始值,因为实例变量会随对象一起分配到堆中。
2.3 解析
解析操作是将常量池中的符号引用转换为直接引用的过程,其中符号引用是用来描述引用目标的一组符号,而直接引用是直接指向方法的指针、相对偏移量或一个间接定位到目标的句柄。解析主要针对于类或接口、字段、类方法、接口方法、方法类型等。此外,解析可能会在初始化阶段后执行,称之为动态绑定或是晚期绑定。
2.4 初始化
初始化阶段执行类构造器方法clinit()
,该方法由JVM定义,主要由javac编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并而来。cinit()
不同于类本身的构造器,它是JVM视角下的一种init()
。
如上图所示,ClassINitDemo
中包含两个变量,一个是由Static修饰的a,另一个是位于静态代码块中的b。
初始化的时机通常有:
- 使用new关键字实例化对象
- 调用
java.lang.reflect
包中的方法 - 对类的静态变量进行访问和赋值
- 调用类的静态方法
- 初始化类的子类,父类本身也会被初始化
- 作为程序的启动入口,包含
main()
3. 分类
JVM主要支持两种类型的ClassLoader:
- 引导类加载器(BootStrapClassLoader)
- 自定义类加载器(ExtensionClassLoader、User-DefinedClassLoader、SystemClassLoader)
其中所有派生于ClassLoader的类加载器都统称为自定义类加载器。程序中常见的有BootStrapClassLoader、ExtensionClassLoader和SystemClassLoader。
自定义类使用系统类加载器AppClassLoader进行加载,Java核心类库中的类使用引导类加载器BoostStrapClassLoader进行加载。
public class ClassLoadDemo {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层 扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6
//获取其上层 获取不到引导类加载器
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println(bootStrapClassLoader);//null
//对于用户自定义类来说:使用系统类加载器进行加载
ClassLoader classLoader = ClassLoadDemo.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String 类使用引导类加载器进行加载的 -->java核心类库都是使用引导类加载器加载的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
3.1 BoostStrapClassLoader
BoostStrapClassLoader使用C/C++语言实现的,它用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar/resources.jar
或sun.boot.class.path
路径下的内容),用于提供JVM自身需要的类。出于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类。
3.2 ExtClassLoader
ExtClassLoader由Java语言编写,它通过由sun.misc.Launcher$ExtClassLoader
实现,派生于ClassLoader类,父类加载器为BoostStrapClassLoader。它用于从java.ext.dirs
系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext
子目录(扩展目录)下加载类库。
3.3 AppClassLoader
AppClassLoader由Java语言编写,同样由sun.misc.Launcher$AppClassLoader
实现,派生于ClassLoader类,父类加载器为ExtClassLoader。它负责加载环境变量classpath或系统属性java.class.path
指定路径下的类库。它是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载,通过ClassLoader.getSystemClassLoader()
可以获取到该类加载器。
3.4 特点
- 层级结构:Java中的类加载器之间具有层级结构,BoostStrapClassLoader是所有类加载器的父亲
- 代理模式:当一个类加载器加载某个类时,首先会检查它的父加载器中是否已经对该类进行了加载。如果父加载器已经加载过了,那么该类将会被直接使用;否则类加载器会请求加载该类
- 可见性限制:一个子加载器可以查找父加载器中的类,但是父加载器不能查找子le加载器中的类
- 不允许卸载:类加载器可以加载一个类但是不能执行卸载操作,不过可以删除当前的类加载器,然后创建一个新的类加载器进行加载
4.ClassLoader的常用方法
除了BoostStrapClassLoader外,所有的类加载器都派生于ClassLoader。ClassLoader常用的方法如下:
方法名称 | 描述 |
---|---|
getParent() | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例 |
findClass(String name) | 查找名称为name的类,返回结果为java.lang.Class类的实例 |
findLoadedClass(String name) | 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例 |
defineClass(String name,byte[] b,int off,int len) | 把字节数组b中的内容转换为一个Java类 ,返回结果为java.lang.Class类的实例 |
resolveClass(Class<?> c) | 连接指定的一个java类 |
5. 获取ClassLoader的方式
类对象.getClassLoader()
:获取当前类的ClassLoaderThread.currentThread().getContextClassLoader()
:获取当前线程的ContextClassLoaderClassloader.getSystemClassLoader()
:获取系统默认的ClassLoaderDriverManager.getClasserClassLoader()
:获取调用者的ClassLoader
public class Car {
String name;
double price;
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = new Car().getClass().getClassLoader();
System.out.println(classLoader); // jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println(contextClassLoader);
// jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
System.out.println(ClassLoader.getSystemClassLoader());
// jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
}
}
6. 双亲委派机制
JVM对.Class
文件采用的是按需加载的方式,当使用该类时才将其字节码文件加载到内存中生成对应的Class类对象。当加载某个类的字节码文件时,JVM采用的是双亲委派机制,它的核心思想为:
- 自底向上检查类是否已经加载
- 自顶向下尝试加载类
6.1 工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行
- 如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的BoostStrapClassLoader
- 如果父类加载器可以完成类加载 任务,则成功返回;如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载
- 如果所有的类加载器都没法进行加载时,JVM会抛出ClassNotFoundException异常
通过双亲委派机制可以避免类的重复加载,同时保护程序的安全,防止核心API被随意篡改。