目录
〇、前言
一、类加载子系统
1.1 内存结构概述
1.2 类加载器及类加载过程概述
1.2.1 类加载器
1.2.2 类加载过程
1.3 类加载过程一:Loading
1.3.1 加载过程
1.3.2 加载类的方式
1.4 类加载过程二:Linking
1.4.1 验证(Verify)
1.4.2 准备(Prepare)
1.4.3 解析(Resolve)
1.5 类加载过程三:Initialization
二、类加载器
2.1 类加载器分类
2.2 启动类加载器(引导类加载器,BootstrapClassLoader)
2.3 扩展类加载器(ExtensionClassLoader)
2.4 应用程序类加载器(系统类加载器,AppclassLoader)
2.5 用户自定义类加载器(UserDefinedClassLoader)
2.5.1 为什么要自定义类加载器
2.5.2 用户自定义类加载器实现步骤
2.6 ClassLoader
2.6.1 常用方法
2.6.2 获取ClassLoader的途径
三、双亲委派机制
3.1 简介
3.2 工作原理
3.3 代码验证
3.4 双亲委派机制的优点
3.5 沙箱安全机制
3.6 两个Class对象是否为同一个类
如果为了面试而来,那么建议看我的这篇文章:Java面试必问:类加载过程与类加载器_玉面大蛟龙的博客-CSDN博客
如果是想深究JVM,那么欢迎你往下看:
本篇文章介绍的就是下图黄色区域的部分。
以一个类HelloLoader.java为例:
Linking链接过程又分为三个子阶段:
经过loading阶段后,已经能够在内存当中生成class实例了。Verify的目的在子确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
例如,所有能被Java虚拟机识别的字节码文件,都必须以CAFEBABE开头(是咖啡宝贝的意思)。
Prepare阶段为类变量分配内存并且设置该类变量的默认初始值,即零值。
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;这里也不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
例如:下面代码中的a变量,先在准备阶段中被赋值为0,再在初始化阶段被赋值为1。
public class HelloApp {
//prepare:a = 0 ---> initial : a = 1
private static int a = 1;
public static void main(String[] args) {
System.out.println(a);
}
}
Resolve阶段是将常量池内的符号引用转换为直接引用的过程。
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机
规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的
CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT Methodref_info等。
还是以上述HelloApp类代码为例,这个类看似代码量很少,但其实在准备阶段需要加载很多的类,将这些类都放在class文件中是不现实的。所以写成符号引用,在准备阶段再转换为直接引用。
咱们可以看一下有哪些引用。反编译一下HelloApp的字节码文件:在 HelloApp.class所在目录下输入命令:
javap -v .\HelloApp.class
可以看到加载了很多的类信息,这些类信息都需要在解析阶段转为直接引用。
初始化阶段就是执行类构造器方法
虚拟机必须保证一个类的
下面我们用一些例子来说明这些:
例如,我们编写一个简单的类来看看是否有
public class ClassInitTest {
private static int num = 1;
public static void main(String[] args) {
System.out.println(ClassInitTest.num);//2
}
}
使用jclasslib Bytecode Viewer 插件查看,发现除了我们手写的main方法之外,还有
加一个静态代码块的初始化:
public class ClassInitTest {
private static int num = 1;
static{
num = 2;
}
public static void main(String[] args) {
System.out.println(ClassInitTest.num);//2
}
}
再次查看,发现是先赋值为1,再变为2
这说明了,
为了更清楚地说明顺序执行的问题,我们再将代码改的复杂一点。我们再定义一个number值,将定义的语句放在静态代码块的下面,看看结果:
public class ClassInitTest {
private static int num = 1;
static{
num = 2;
number = 20;
System.out.println(num);
//System.out.println(number);//报错:非法的前向引用。
}
private static int number = 10; //linking之prepare: number = 0 --> initial: 20 --> 10
public static void main(String[] args) {
System.out.println(ClassInitTest.num);//2
System.out.println(ClassInitTest.number);//10
}
}
看字节码也可以得知,number变量先被赋值为20,然后又赋值为10
需要注意的是,我们虽然可以在定义number的语句之前的静态代码块中给number赋值,但却不能调用它:
同时需要注意的是,如果没有static变量赋值或static代码块赋值,将不会执行
public class ClinitTest {
private int a = 1;
public static void main(String[] args) {
int b = 2;
}
}
没有
再来看看父类和子类的
public class ClinitTest1 {
static class Father{
public static int A = 1;
static{
A = 2;
}
}
static class Son extends Father{
public static int B = A;
}
public static void main(String[] args) {
//加载Father类,其次加载Son类。
System.out.println(Son.B);//2
}
}
查看一下
可以看到是先加载的父类,然后再将父类的值赋给子类。
最后再来看看一个类的
public class DeadThreadTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread dead = new DeadThread();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread t1 = new Thread(r,"线程1");
Thread t2 = new Thread(r,"线程2");
t1.start();
t2.start();
}
}
class DeadThread{
static{
if(true){
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while(true){
}
}
}
}
确实只初始化了一次,说明
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和 自定义类加载器(User-Defined classLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器(包括ExtensionClassLoader 和 AppclassLoader)。
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:
需要注意的是:这里的四者之间的关系是包含关系。不是子父类的继承关系。就好像文件路径/a/b/c/d.txt,我们不能说b继承于a。
我们在代码中实际体会一下:
public class ClassLoaderTest {
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@1540e19d
//获取扩展类加载器的上层:获取不到引导类加载器(其实是引导类加载器BootstrapClassLoader,他是用C/C++语音写的,我们无法获取)
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
我们再通过一段代码加深一下理解:
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader); //null
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
}
}
运行结果:
**********启动类加载器**************
file:/D:/Java/jdk/jdk1.8.0_161/jre/lib/resources.jar
file:/D:/Java/jdk/jdk1.8.0_161/jre/lib/rt.jar
file:/D:/Java/jdk/jdk1.8.0_161/jre/lib/sunrsasign.jar
file:/D:/Java/jdk/jdk1.8.0_161/jre/lib/jsse.jar
file:/D:/Java/jdk/jdk1.8.0_161/jre/lib/jce.jar
file:/D:/Java/jdk/jdk1.8.0_161/jre/lib/charsets.jar
file:/D:/Java/jdk/jdk1.8.0_161/jre/lib/jfr.jar
file:/D:/Java/jdk/jdk1.8.0_161/jre/classes
null
***********扩展类加载器*************
D:\java\jdk\jdk1.8.0_161\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
sun.misc.Launcher$ExtClassLoader@11d50c0
在Java的日常应用程序开发中,类的加载几乎都是由上述3种类加载器相互配合执行的。但在必要时,我们还可以自定义类加载器,来定制类的加载方
式。
通过代码简单体会一下:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if(result == null){
throw new FileNotFoundException();
}else{
return defineClass(name,result,0,result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name){
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
return null;
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class> clazz = Class.forName("One",true,customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
ClassLoader类是一个抽象类,其后所有的类加载器都继承自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类 |
通过代码简单实践一下:
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行结果:
null
sun.misc.Launcher$AppClassLoader@b4aac2
sun.misc.Launcher$ExtClassLoader@15fbaa4
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的
class文件时,Java虚拟机采用的是双亲委滤模式,即把请求交由父类处理,它是一种任务委派模式。
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello,atguigu.com");
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}
然后构建一个java.lang.String类
public class String {
static{
System.out.println("我是自定义的String类的静态代码块");
}
}
如果我们自定义的String类被加载,控制台肯定能打印出上面这句话。但是运行后发现,并不能打印。说明JVM加载的还是自己的String类。
我们在自己的String类中写一个main方法并执行:
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
运行结果:
说明JVM将运行java.lang.String类的需求一步步地递交到引导类加载器,发现他的String类中没有main方法。JVM压根就没有打算要加载我们自己的String方法。
在刚刚的代码示例中,我们自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
换句话说,在JVM中,即使这两个类对象(Class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的classLoader实例对象不同,那么这两个类对象也是不相等的。