类的动态加载
类加载
类加载的时机
类加载,即虚拟机加载.class文件。什么时候虚拟机需要开始加载一个类呢?虚拟机对此没有规范约束,交给虚拟机把握。
Javac原理
javac是用于将源码文件.java编译成对应的字节码文件.class。
其步骤是:源码——>词法分析器组件(生成token流)——>语法分析器组件(语法树)——>语义分析器组件(注解语法树)——>代码生成器组件(字节码)。
类加载过程
先在方法区找class信息,有的话直接调用,没有的话则使用类加载器加载到方法区(静态成员放在静态区,非静态成功放在非静态区),静态代码块在类加载时自动执行代码,非静态的不执行;先父类后子类,先静态后非静态;静态方法和非静态方法都是被动调用,即不调用就不执行。
类加载的流程如下图:
- 加载
- 通过类的全名限定获取定义此类的二进制字节流。
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存(方法区)生成一个代表这个类的class对象,作为方法区这个类的各种数据访问入口。
加载和连接是交叉进行的,加载未完成可能连接已经开始了。
-
验证
检查class是否符合要求,非必须阶段,对程序的运行期没有影响,-Xverif:none 关闭(可以提高启动速度)
- 文件格式验证(魔数、常量类型);
-
元数据验证(语义分析);
- 字节码验证(语义合法);
- 符号引用验证;
-
准备
正式为类变量(static成员变量)分配内存并设置类变量初始值(零值)的阶段,这个变量所使用的内存都将在方法区进行分配,这时候的内存分配仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起在堆中进行分配。
-
解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
-
初始化
初始化阶段是执行类构造器\<clinit>()方法的过程,虚拟机会保证一个类的类构造器\<clinit>()在多线程环境中被正确的加锁,同步;如果多个线程同时初始化一个类,那么只会有一个线程区执行这个类的类构造器,其他线程阻塞等待,直到\<clinit>()方法完毕,同一个类加载器,一个类只会被初始化一次。
代码分析
-
类加载的时候会执行代码
-
初始化:静态代码块
- 实例化:构造代码块、无参构造函数
构建一个Person类
package loadclassTest;
import java.io.Serializable;
public class Person implements Serializable {
public String name;
private int age;
public static int id;
static {
System.out.println("静态代码块");
}
public static void staticMethod(){
System.out.println("静态方法");
}
{
System.out.println("构造代码块");
}
public Person() {
System.out.println("无参构造");
}
public Person(String name, int age) {
System.out.println("有参构造");
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试
new Person(); //输出 静态代码块 构造代码块 无参构造
new Person("a", 18); //输出 静态代码块 构造代码块 有参构造
Person.staticMethod(); //输出 静态代码块 静态方法
Person.id = 1; //输出 静态代码块
可以看到无论是无参构造、有参构造、调用静态方法还是给静态变量赋值都会调用静态代码块
尝试直接调用class加载类Class c = Person.class;
,发现没有输出,没有进行初始化只进行了加载
动态加载
可以使用Class.forName来动态加载类
Class<?> c = Class.forName("Person"); //输出 静态代码块
可以看到Class.forName进行了初始化,我们看下底层代码,进来看到调用了forName0函数
跟进看下发现是由native修饰的,参数里有个initialize,应该是判断是否初始化
native:一个native方法就是一个Java调用非Java代码的接口。一个native方法是指该方法的实现由非Java语言实现,比如用C或C++实现。
往上找能找到传3个参数的forName函数
传入类名,是否初始化布尔值以及加载类,当initialize传入false时,没有输出
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> c = Class.forName("Person", false, cl);
调用newInstance()函数可以进行初始化
c.newInstance(); //输出 静态代码块 构造代码块 无参构造
这里需要了解什么是ClassLoader
参考连接:https://www.cnblogs.com/makai/p/11081879.html
类加载分析
ClassLoader cl = ClassLoader.getSystemClassLoader();
System.out.println(cl);
//输出sun.misc.Launcher$AppClassLoader@18b4aac2
可以看到ClassLoader.getSystemClassLoader()
实际上获取的是AppClassLoader
的类
打断点进行调试,跟进去分析
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> c = cl.loadClass("Person");
首先进入ClassLoader
调用的是AppClassLoader的loadClass方法,然后继续跟进
中间是一些安全检查的代码,走到后面调用父类的loadClass
跟进然后又回到了ClassLoader,继续往下走,调用了findLoadedClass,看这个类是否加载过,这里就是双亲委派的流程,询问Extension ClassLoader、Bootstrap ClassLoader是否加载过,如果没有加载就加载
走到下面判断parant,parant不是父类,是父属性,然后存在ExtClassLoader,调用ExtClassLoader的loadClass
由于ExtClassLoader没有loadClass方法所以又回到了ClassLoader
继续往下走,parent为null了
然后就会去Bootstrap里寻找,因为这是个普通的类,不会用系统加载类去加载,所以还是没找到
继续往下走,走到findClass
由于ClassLoader的findClass的方法是需要重写的一个方法,所以会走到子类findClass
本来是应该走到ExtClassLoader里的findClass,但实际上却走到了URLClassLoader的findClass里
这是因为ExtClassLoader和AppClassLoader都没有findClass方法,而且都继承了URLClassLoader
由于现在是在ExtClassLoader里,肯定是找不到普通类的,所有继续往下走,然后退回到AppClassLoader里了,接着调用findClass
又回到了URLClassLoader,继续跟进
分析findClass
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class"); // 这里是获取类路径比如:Person.clss
Resource res = ucp.getResource(path, false); // ucp是一个URLClassPath类,在当前加载路径去找这个类
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
} catch (ClassFormatError e2) {
if (res.getDataError() != null) {
e2.addSuppressed(res.getDataError());
}
throw e2;
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
这里找到了这个类
最后调用了defineClass,实际上是在这里进行了类的加载,继续跟进,走到了URLClassLoader的defindClass方法,中间都是一些检验的代码,最后又调用了一个defindClass
走到了URLClassLoader的父类SecureClassLoader,又调用了一个defindClass,跟进
然后又回到了ClassLoader的defindClass,这里调用了defineClass1
而defineClass1是由native修饰的,是由c++写的,最后的类加载是在这完成的,到这就结束了
流程:
- 继承关系
- ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoader
- 调用
- loadClass->findClass(重写的方法)->defineClass(从字节码加载类)
使用URLClassLoader进行任意类加载
知识点
- 支持的协议
- file
- http
测试
file
首先定义一个Hello类,进行编译,然后移动到别的目录,在把原本的类删了,进行测试
public class Hello {
static {
System.out.println("Hello");
}
}
成功进行加载
尝试下弹出计算器
import java.io.IOException;
public class Test {
static {
try {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
} catch (IOException e) {
e.printStackTrace();
}
}
}
http
在class路径起个http服务
直接请求当前路径的类,成功加载
同时也收到了请求
使用defindClass进行任意类加载
这里成功加载类
分析
由于defineClass是私有类,只能使用反射进行获取对象,然后调用方法,实现类的加载
(╯‵□′)╯︵┴─┴