什么是类加载子系统?
类加载子系统是是负责从本地文件系统或者网络当中加载class文件,加载的信息存放于一块称为方法区的内存空间。除了类的信息之外,方法区中还会存放运行时的常量信息,可能包括字符串字面量和数字常量。
Java的类加载分为三个阶段,分别是加载(Loading)、链接(Linking)、初始化(Initialization),如下图所示:
类的加载过程
Loading
- 通过一个类的全限定名获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
Linking
链接阶段包括验证、准备、解析等三个步骤:
- 验证(verify):验证的目的是确保class文件的字节流中的信息符合当前虚拟机的要求,保证类加载的正确性,不会危害到虚拟机自身安全。主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
- 准备(prepare):为静态变量分配内存并设定默认值,这里不包括final修饰的静态变量,因为final在编译的时候就已经分配了,准备阶段会显示初始化。这里也不会为实例变量分配内存和初始化,类变量会分配在方法区中而实例变量是会随着对象一起分配到堆内存中
- 解析(resolve):是将常量池中的符号引用转换为直接引用的过程,解析操作往往会伴随着JVM执行完初始化之后再执行。解析操作主要针对类或接口、属性、类方法、接口方法、方法类型等。
Initialization
- 初始化阶段就是执行类构造器方法的过程,此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 方法不等同于类的构造器,类的构造器是方法
- 若一个类存在父类,那么JVM会保证子类的方法执行前,父类的方法就已经执行完毕了。
类加载器的分类
Java支持两种类型的类加载器,分别是引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器指的是程序中由开发人员自定义的一类加载器,但是JVM规范中却没有这么定义,而是将所有的派生于抽象类的类加载器都归于自定义类加载器。
在Java程序中,我们常见的类加载器始终只有三个,如下图所示:
类加载器具有等级制度,但是并非继承关系,以组合复用的方式来复用加载器的功能。
- 启动类加载器(Bootstrap ClassLoader):也叫引导类加载器
- 这个类加载器使用c/c++语言实现,嵌套在JVM内部
- 它用来加载Java的核心类库(jre/lib/rt.jar/resources.jar/sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 出于安全考虑,Bootstrap ClassLoader只加载包名为java、javax、sun等开头的类
- 扩展类加载器(Extension ClassLoader):
- 由Java语言编写,派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库,如果用户自定义的类放到此目录下,也会自动由扩展类加载器加载
- 系统类加载器(Application ClassLoader):也叫应用类加载器
- 由Java语言编写,派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负载加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该加载器是程序中默认的类加载器,一般来说,Java应用类都是由此加载器所加载
获取BootstrapClassLoader能够加载的类库路径:
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL.toExternalForm());
}
执行结果如下:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/classes
获取扩展类加载器加载的类库路径:
String extDirs = System.getProperty("java.ext.dirs");
for (String s : extDirs.split(":")) {
System.out.println(s);
}
执行结果如下:
/Users/chency/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java
自定义类加载器
前面我们介绍了Java自带的类加载器,下面我们来讲解一下如何来自定义类加载器,以及什么情况下需要自定义类加载器。
自定义类加载器的场景:
- 隔离加载类:在某些框架内进行中间件与应用模块的隔离,把类加载到不同的环境。比如阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包;
- 修改类加载方式:可以根据实际情况在某个时间节点按需进行动态加载;
- 扩展加载源:可以从数据库、网络中进行加载;
- 防止源码泄露:Java字节码文件容易被反编译和篡改,可以在边编译的时候进行加密。然后自定义类加载器,在类加载的时候对字节码进行解密;
自定义类加载器只需要继承java.lang.ClassLoader抽象类,重写findClass方法,调用defineClass()方法,示例代码如下:
public class CustomerClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomerPath(name);
if(result == null){
throw new FileNotFoundException();
}else {
return defineClass(name,result,0,result.length);
}
}catch (Exception e){
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomerPath(String name){
byte[] bytes = {};
return bytes;
}
public static void main(String[] args) {
CustomerClassLoader customerClassLoader = new CustomerClassLoader();
try {
Class<?> clazz = Class.forName("ClassName", true, customerClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}