@TOC[文章目录]
类加载器是一个很抽象的概念,要是不理解的话,不妨看这个例子:
字节码文件存储在硬盘上,是实际存在的东西,我们称之为“阳间”体;
Class对象存在在内存中,是一个抽象的虚的概念,我们称之为“阴间”体;
而类加载器就好像是牛头马面这样的引渡人,将字节码文件引渡到阴间。此时的Class对象为了不变成孤魂野鬼,会牢记他的引渡人ClassLoader,因此你可以通过任何一个class对象很轻松获取他的ClassLoader。
文章目录
再看一遍程序执行的总的流程图:
ClassFile的部分由前端编译器javac
完成,我们咱不讨论。
因此,开启JVM的学习,从类加载子系统开始!
类加载后的信息放在方法区
,方法区在不同版本的jvm有不同的实现。
一、类加载子系统
- 类加载器从网络或者内存中加载字节码文件
- ClassLoader只负责加载类, 而程序能否得到有效执行主要看ExecutionEngine的发挥。
加载流程:
硬盘上的.class文件 -> ClassLoader通过二进制流的形式装载到jvm -> 进入方法区(.class首先到达的内存区域,与Class共同驻足这片区域的还有字符串和数字常量池) -> 按照.class文件的模板生成具体的Class对象【class文件相当于创建Class对象的图纸,这个模板成为Class DNA元数据模板】
1. 加载类的过程
加载过程:
ps:不要把第一步的加载与整个加载弄混,这些步骤加起来才是综合的加载。
1. 1加载:
类加载按照全限定类名找到硬盘中对应的字节码文件; -> 将字节码文件转为二进制流 -> 将静态数据结构转化为实际数据结构 ->生成Class对象
1.2 链接
- 检查:
对.class进行检查,若不符合字节码规范,直接抛出异常。
【目的是保护系统安全,防止恶意篡改、顶替字节码文件】
验证头部信息:CA FE BA BE是java字节码的头部固定字符; - 准备 :
在这个阶段中,需要对类中定义的静态变量和常量进行处理,引用类型或者实例暂且不实例化。
若为static变量:赋值为默认值;【后面的初始化会为其赋真实值】
java默认值:
数字整形类型为:0;
字符类型为:’\u0000’
引用类型:null
浮点型:0.0
boolean:false
若为static常量,赋值为定义的那个值。
3.解析:
在这个阶段,需要将字符引用转换为直接引用。
字符引用:
按照java命名规范命名的引用名称;
直接引用:
直接指向内存中对象的指针。
1.3 初始化
- <clinit>方法: 只要类中声明了静态的部分【代码块或者变量】,就会自动生成clinit方法
静态变量的赋值流程:先通过静态成员变量声名为该变量赋值默认值;之后按照从前往后的顺序进行覆盖。
【final常量会直接赋值,因为final默认了它不会再改变,可以直接赋初始值】
这就是为什么下面的代码为什么能通过的原因:
即使number的声明在后面,因为JVM会首先根据声明来创建这个变量,之后才考虑初始化过程。
即:
number = 0[默认] -》 number - 》 number = 1 -》 number = 2【常量赋值】
但是,即使能够赋值,也不能在声名之前通过方法调用。
创建变量赋值默认值是jvm的行为,程序不能使用该初始值,因此此时的变量被视为“不存在的”
public class Demo2 {
private static int num = 1;
static {
num = 1;
number = 1; //可以,但是会被后面的覆盖。
// System.out.println(number); //不可以,非法的前向引用
}
private static int number = 2;
public static void main(String[] args) {
System.out.println(num);
System.out.println(number);
}
}
- 父类<clinit>先执行才能轮到子类的;
public class Demo3 {
static class A{
public static int a = 1;
static {
a = 2;
}
}
static class B extends A {
public static int b = a;
}
}
字节码的覆盖过程:
可以看到:
首先该静态变量赋值为1(static代码块),再被覆盖为2(成员变量赋值语句);
- 多线程情况下,若有多个去加载同一个类,虚拟机会为加载过程加锁,使得别的进程的加载过程进不来,因此任何类的加载只会被执行一次。
下面的代码在静态代码块中加入死循环,使得类加载的第三步初始化过程一直被卡住,因此一直无法正常加载,此时两个不同线程去加载他也只会被加载一次。
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName());
// Class<DeadThread> deadThreadClass = DeadThread.class;
DeadThread deadThread = new DeadThread();
};
new Thread(runnable, "线程1").start();
new Thread(runnable, "线程2").start();
}
static class DeadThread{
static {
//static代码块不用if不被放while(true)
// 另一种方法是将true作为变量拿出去,这样编译器也会检查通过
if (true) {
System.out.println("我被加载了");
while (true) {
}
}
}
}
}
按道理说,使用.class也会加载类,但是我使用.class没有加载DeadThread这个类,只有创建实例才可以,为什么呢?
本篇最后,这个应该属于被动加载,故static代码块不会被初始化。
java编译器具有检查机制,若在static代码块中出现while(true)的结构就无法编译通过,这是为了预防加载类出现意外。通过一层if或者单独定义变量的形式可以越过这层检查。
二、类加载器的分类
类加载分为两类:
引导类加载器与自定义加载器。
区分的依据是:是否直接或者间接继承了ClassLoader抽象类
.
下面的代码演示了各种类加载器的获取方式:
public static void main(String[] args) {
ClassLoader userClassLoader = Demo1.class.getClassLoader();
System.out.println(userClassLoader);
ClassLoader extClassLoader = userClassLoader.getParent();
System.out.println(extClassLoader);
//注意获取系统类加载器是通过classLoader类的内置方法实现的
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
System.out.println(systemClassLoader == userClassLoader);//true
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); // null
//系统类库都是通过引导类加载器加载。
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println(stringClassLoader);//null
System.out.println(stringClassLoader == bootstrapClassLoader);
}
- getParent() 获取上级而是父类;
几个类加载器之间没有派生关系,只有上下级的关系。真要说,就好像目录结构,若/a/b/
目录结构, 你不能说a是b的父类,它们其实都是File类的实例,在类的关系上是等同的。
但是,它们切实存在着上下级关系,这会则后面的双亲委派机制
中重点体现。 - BootStrap类加载器的获取总会获得null,仿佛他就高贵得不想鸟你,实际上是因为他根本就不是java类,它是由c和c++编写的,内嵌到JVM中,不能生成java对象自然不能打印,因此一直都是null;
- 系统类加载器直观地获取必须通过ClassLoader的静态方法getSystemClassLoader();
但其实只要是任何用户自定义的类,或者第三方jar包的加载都通过SystemClassLoader,他的类名并不是这个,而是一个Launch类的内部类,叫做AppClassLoader;
2.1 引导类加载器
内嵌到JVM中,使用c与c++编写;
复杂加载以java, javax, sun等
开头的类。【若是用户自定义的类却使用这些作为包名,不会得到得到加载,这也是为了保护引导加载器。】
2.2 扩展类加载器
派生自ClassLoader,是自定义类加载器的最高级。
负责加载jre/lib/ext
以及java.ext.dirs
目录下内容.
可以通过getProperty()来获取他能够加载的目录。
2. 3 系统类加载器
AppClassLoader类.
用户自定义的类以及第三方类库都是由他加载的【除了有些有自定义加载器】。
他家在的是classpath;
下的内容。
3.4 自定义类加载器
有一些原因,需要我们去自定义类加载器。
他的好处是可以自定义类的编码解码格式、处理重名包的偏重问题等。
三、双亲委派机制【重点】
jvm对class文件采取“按需加载”地方式。
在具体加载过程中,采取“双亲委派机制”。
试想一种情况:
在src下自定义一个包结构:/java/lang
,并在此之下定义一个String类,那么我们其他程序使用的java.lang.String使用的是哪个String类呢?
如果不对这种情况加以处理,会造成严重的系统安全问题【恶意用户通过这种方式去破坏整个项目结构或者对引导类加载器造成危害】。
双亲委派机制就是解决这个问题的。
当系统类加载器接到加载类的任务时,他不会加载,而是委托上级扩展类加载器,扩展同样委托上级引导。
若确实是引导的业务范围,引导就会加载;否则会返还给扩展加载;再不然就继续返还给系统加载。
回到刚刚的例子:
java开头正好是引导类加载器的业务,因此他会帮类加载String,而用户自定义的java.lang.String不会加载。
若是在自定义String下定义main方法,将得不到执行,报如下错误:
同样,若是定义一个java.lang包下不同名的文件,引导类加载器找不到的情况下就会报一个安全异常,提醒我们不要使用java.lang去命名包名。
这样的找不到main()方法的处理方式就是沙箱机制
- Class对象相同的条件:
- 包名,类名相同;
- 类加载器相同
- 类的主动加载与被动加载。
被动加载不会被初始化【即加载的第三步】.