虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程就是类加载。
虚拟机规范并没有指明二进制字节流要从一个Class文件获取,或者说根本没有指明从哪里获取、怎样获取。这种开放使得Java在很多领域得到充分运用,例如:
加载:类加载过程的一个阶段,通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象
验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考周志明老师的《深入了解Java虚拟机》)。
初始化:类加载最后阶段,若该类具有超类,则先对其超类进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
Java程序对类的使用方式分为:主动使用和被动使用。 主动使用:
除了以上情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
在类加载的第一阶段“加载”过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器。
将 class 文件二进制数据放入方法区内,然后在堆内(heap)创建一个 java.lang.Class 对象,Class 对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内的数据结构的接口。
ClassLoader.getSystemClassLoader()
方法可以获取到该类加载器在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
自定义类加载器分为两步:
java.lang.ClassLoader
findClass()
方法针对第 1 步,为什么要继承 ClassLoader
这个抽象类,而不继承 AppClassLoader
呢?因为它和 ExtClassLoader
都是 Launcher
的静态内部类,其访问权限是缺省的包访问权限。static class AppClassLoader extends URLClassLoader{...}
第 2 步,JDK 的 loadCalss()
方法在所有父类加载器无法加载的时候,会调用本身的 findClass()
方法来进行类加载,因此我们只需重写 findClass()
方法找到类的二进制数据即可。
这里的四者之间是包含关系,不是上层和下层,也不是继承关系 。
启动类加载器(Bootstrap ClassLoader),由C++实现,没有父类加载器。
拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为Bootstrap ClassLoader
系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
自定义类加载器,父类加载器为AppClassLoader。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。
也就是说,在JVM中表示两个class对象是否为同一个类存在两个必要条件:
即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
这里所指的“相等”,包括代表类的 Class 对象的 equals()
方法、 isAssignableFrom()
方法、isInstance()
方法的返回结果,也包括使用 instanceof
关键字做对象所属关系判定等情况。
sun.misc.Launcher类是java的入口,在启动java应用的时候会首先创建Launcher类,创建Launcher类的时候,会创建应用程序运行中所需要的类加载器。
Launcher的构造器的源码
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
}
Launcher在创建的时候,创建了ExtClassLoader,然后用ExtClassLoader创建了 AppClassLoader。
线程上下文类加载器 是从 JDK 1.2 开始引入的。我们知道JVM虚拟机采用双亲委派模式来加载类,而且在类加载的整个过程中只有在加载阶段可以让程序员操作,加载器通过类的全限定名在class文件的二进制流中加载类,并创建类的唯一一个class对象,作为类的全局访问点。我们知道为了实现程序的动态性,我们可以自定义类加载器,通过重写findClas()方法来实现自定义类加载器,再通过重写loadClass()方法打破双亲委派机制,这是一次打破双亲委派,那么还有一种就是使用线程上下文类加载器来打破双亲委派机制。
为解决基础类无法调用类加载器加载用户提供代码的问题,Java 引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器默认就是 Application 类加载器,并且可以通过 java.lang.Thread.setContextClassLoaser()
方法进行设置。
这里只是简单介绍一下它,不进行深入探讨,总结就是线程上下文类加载器,可以打破双亲委派机制,实现逆向调用类加载器来加载当前线程中类加载器加载不到的类。
public class ClassLoaderTest {
public static void main(String[] args) {
// 系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + systemClassLoader);
// 扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println("扩展类加载器:" + extClassLoader);
// 启动类加载器
ClassLoader bootstrpClassLoader = extClassLoader.getParent();
System.out.println("启动类加载器:" + bootstrpClassLoader);
// 当前类的类加载器
ClassLoader curClassLoader = ClassLoaderTest.class.getClassLoader();
System.out.println("当前类的类加载器:" + curClassLoader);
// java.lang.String类的加载器
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println("String类的类加载器:" + stringClassLoader);
/**
系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
扩展类加载器:sun.misc.Launcher$ExtClassLoader@68f7aae2
启动类加载器:null
当前类的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
String类的类加载器:null
*/
}
}
得到的结果,从结果可以看出启动类加载器无法直接通过代码获取,同时目前用户代码所使用的加载器为系统类加载器。同时我们通过获取String类型的加载器,发现是null,那么说明String类型是通过启动类加载器进行加载的。
Java 默认 ClassLoader 只加载指定目录下的 class,如果需要动态加载类到内存,例如要从远程网络下来类的二进制,然后调用这个类中的方法实现我的业务逻辑,如此,就需要自定义 ClassLoader。
如果没有太过于复杂的需求,可以直接继承URLClassLoader
类,这样就可以避免自己去编写findclass()
方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
先写一个基本测试类
public class TestClass {
@Override
public String toString() {
return " this is TestClass ~";
}
}
自己写一个继承ClassLoader 的类加载器
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 加载指定目录下指定类名的class文件
String property = getClassPath(name);
byte[] classData = getClassData(property);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
// 读取字节流的方法
private byte[] getClassData(String path) {
try (InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()
) {
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// 获取class字节码文件绝对路径
public static String getClassPath(String className){
return System.getProperty("user.dir") + File.separator + "com"+ File.separator + "qiuwen"
+ File.separator + className + ".class";
}
public static void main(String[] args) throws Exception {
// 指定类加载器加载调用
MyClassLoader classLoader = new MyClassLoader();
Class<?> aClass = classLoader.loadClass("com.qiuwen.TestClass");
System.out.println(aClass.newInstance().toString());
// 输出
// this is TestClass ~
}
}
public class MyURLClassLoader extends URLClassLoader {
public MyURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public MyURLClassLoader(URL[] urls) {
super(urls);
}
public MyURLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
// 获取class字节码文件绝对路径
public static String getClassPath(String className) {
return System.getProperty("user.dir") + File.separator + "com" + File.separator + "qiuwen"
+ File.separator + className + ".class";
}
public static void main(String[] args) throws Exception {
String classPath = getClassPath("com.qiuwen.TestClass");
//创建自定义文件类加载器
File file = new File(classPath);
//File to URI
URI uri= file.toURI();
URL[] urls= {uri.toURL()};
URLClassLoader myURLClassLoader = new MyURLClassLoader(urls);
Class<?> aClass = myURLClassLoader.loadClass("com.qiuwen.TestClass");
System.out.println(aClass.newInstance().toString());
// 输出
// this is TestClass ~
}
}