这篇文章/笔记的话,打算从类加载器,双亲委派到代码块的加载顺序这样来讲。最后才是加载字节码。
说类加载器有些师傅可能没听过,但是说 Java ClassLoader,相信大家耳熟能详。
加载 Class 文件
以这段简单代码为例
Student student = new Student();
我们知道,Student 本身其实是一个抽象类,是通过 new 这个操作,将其实例化的,类加载器做的便是这个工作。
ClassLoader 的工作如图所示
加载器也分多种加载器,每个加载器负责不同的功能。
主要分为这四种加载器
虚拟机自带的加载器
启动类(根)加载器
扩展类加载器
应用程序加载器
引导类加载器(BootstrapClassLoader),底层原生代码是 C++ 语言编写,属于 JVM 一部分。
不继承java.lang.ClassLoader
类,也没有父加载器,主要负责加载核心 java 库(即 JVM 本身),存储在/jre/lib/rt.jar
目录当中。(同时处于安全考虑,BootstrapClassLoader
只加载包名为java
、javax
、sun
等开头的类)。
扩展类加载器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader
类实现,用来在/jre/lib/ext
或者java.ext.dirs
中指明的目录加载 java 的扩展库。Java 虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载 java 类。
App类加载器/系统类加载器(AppClassLoader),由sun.misc.Launcher$AppClassLoader
实现,一般通过通过(java.class.path
或者Classpath
环境变量)来加载 Java 类,也就是我们常说的 classpath 路径。通常我们是使用这个加载类来加载 Java 应用类,可以使用ClassLoader.getSystemClassLoader()
来获取它。
在 Java 开发当中,双亲委派机制是从安全角度出发的。
我们这里以代码先来感受一下,双亲委派机制确实牛逼。
尽量别尝试,看看就好了。要不然整个文件夹挺乱的,如果想上手尝试一下的话,我建议是新建一个项目,不要把其他的文件放一起。
新建一个 java.lang的文件夹,在其中新建 String.java的文件。
String.java
package java.lang;
// 双亲委派的错误代码
public class String {
public String toString(){
return "hello";
}
public static void main(String[] args) {
String s = new String();
s.toString();
}
}
看着是不是没有问题,没有错误吧?
我们自己定义了一个java.lang
的文件夹,并在文件夹中定义了 String.class,还定义了 String 这个类的 toString 方法。我们跑一下程序。(这里如果把 Stirng 类放到其他文件夹会直接报错,原因也是和下面一样的)
结果居然报错了!而且非常离谱
我这不是已经定义了 main 方法吗??为什么还会报错,这里就提到双亲委派机制了,双亲委派机制是从安全角度出发的。
首先,我们要知道 Java 的类加载器是分很多层的,如图。
我们的类加载器在被调用时,也就是在 new class 的时候,它是以这么一个顺序去找的 BOOT ---> EXC ----> APP
如果 BOOT 当中没有,就去 EXC 里面找,如果 EXC 里面没有,就去 APP 里面找。
所以我们之前报错的程序当中,定义的java.lang.String
在 BOOT 当中是有的,所以我们自定义 String 时,会报错,如果要修改的话,是需要去 rt.jar 里面修改的,这里就不展开了。
前文提到我们新建的java.lang.String
报错了,是因为我们定义的 String 和 BOOT 包下面的 String 冲突了,所以才会报错,我们这里定义一个 BOOT 和 EXC 都没有的对象试一试。
在其他的文件夹下,新建 Student.java
Student.java
package src.DynamicClassLoader;
// 双亲委派的正确代码
public class Student {
public String toString(){
return "Hello";
}
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.getClass().getClassLoader());
System.out.println(student.toString());
}
}
并把加载器打印出来
我们定义的 Student 类在 APP 加载器中找到了。
这里的代码块主要指的是这四种
静态代码块:static{}
构造代码块:{}
无参构造器:ClassName()
有参构造器:ClassName(String name)
这里有两个文件,分别介绍一下用途:
Person.java
:一个普普通通的类,里面有静态代码块、构造代码块、无参构造器、有参构造器、静态成员变量、普通成员变量、静态方法。
Main.java
:启动类
Person.java
package src.DynamicClassLoader;
// 存放代码块
public class Person {
public static int staticVar;
public int instanceVar;
static {
System.out.println("静态代码块");
}
{
System.out.println("构造代码块");
}
Person(){
System.out.println("无参构造器");
}
Person(int instanceVar){
System.out.println("有参构造器");
}
public static void staticAction(){
System.out.println("静态方法");
}
}
Main.java
package src.DynamicClassLoader;
// 代码块的启动器
public class Main {
public static void main(String[] args) {
Person person = new Person();
}
}
运行结果如图
结论:
通过new
关键字实例化的对象,先调用静态代码块,然后调用构造代码块,最后根据实例化方式不同,调用不同的构造器。
直接调用类的静态方法
Person.java 不变,修改 Main.java 启动器即可。
Main.java
package src.DynamicClassLoader;
// 代码块的启动器
public class Main {
public static void main(String[] args) {
Person.staticAction();
}
}
结论:
不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法
Main.java
package src.DynamicClassLoader;
// 代码块的启动器
public class Main {
public static void main(String[] args) {
Person.staticVar = 1;
}
}
结论:
在对静态成员变量赋值前,会调用静态代码块
package src.DynamicClassLoader;
// 代码块的启动器
public class Main {
public static void main(String[] args) {
Class c = Person.class;
}
}
// 空屁
结论:
利用class
关键字获取类,并不会加载类,也就是什么也不会输出。
这里要抛出异常一下。
我们写三种forName
的方法调用。
修改 Main.java
package src.DynamicClassLoader;
// 代码块的启动器
public class Main {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("src.DynamicClassLoader.Person");
}
}
// 静态代码块
package src.DynamicClassLoader;
// 代码块的启动器
public class Main {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("src.DynamicClassLoader.Person", true, ClassLoader.getSystemClassLoader());
}
}
// 静态代码块
package src.DynamicClassLoader;
// 代码块的启动器
public class Main {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("src.DynamicClassLoader.Person", false, ClassLoader.getSystemClassLoader());
}
}
//没有输出
结论:
Class.forName(className)
和Class.forName(className, true, ClassLoader.getSystemClassLoader())
等价,这两个方法都会调用类中的静态代码块,如果将第二个参数设置为false
,那么就不会调用静态代码块
Main.java
package com.xiinnn.i.test;
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("com.xiinnn.i.test.Person", false, ClassLoader.getSystemClassLoader());
}
}
//没有输出
结论:
ClassLoader.loadClass()
方法不会进行类的初始化,当然,如果后面再使用newInstance()
进行初始化,那么会和场景一、实例化对象
一样的顺序加载对应的代码块。