前面我们了解完整个JVM的内存结构,Java对象的内存分配,现在说说Java中类是怎么加载的。
一、什么是ClassLoader
”.java”文件经过Java编译器编译后,生成.class文件,.class文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载。 ClassLoader的主要作用就是动态将Java class字节码加载到JVM中,具体包括:
- 负责将class加载到JVM中;
- 审查每个类由谁加载;
- 将class字节码解释成JVM统一的对象格式。
二、Java类加载的过程
类从被加载到虚拟机内存开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载。其中,验证、准备和解析过程称为连接,但注意一点,解析的过程有可能发生在初始化之后,主要是支持Java语言的动态绑定。
2.1 加载
通过一个类的全限定名类获取定义此类的二进制字节流,并将这字节流所代表的静态存储结构转化为方法区运行时数据结构;最后,在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。
数组类的创建过程遵循以下规则:
1)如果数组的组件类型(指数组去掉一个维度的类型)是引用类型,就递归采用上面的过程区加载这个组件类型,数组将加载改组件类型的类加载器的类名称空间上被表示;
2)如果数组的组件类型不是引用类型,jvm会把数组标识为引导类加载器关联;
3)数组的可见性与组件类型的可见性一致,如果组件类型不是引用类i系那个,数组类的可见性默认为public。
2.2 验证
验证阶段会完成4个动作:文件格式验证,元数据验证、字节码验证和符号引用验证。文件格式验证主要验证字节流是否满足class文件格式规范;
元数据验证包括:这个是是否继承了父类,是否继承了不允许继承的类(被final修饰的类);如果类不是抽象类,是否实现了父类或接口中要求实现的方法,类中的字段、方法是否与父类产生矛盾;
字节码验证主要是保证程序的语言是合法的,符合逻辑的,保证类的方法在运行时不会危害虚拟机安全。
符号引用验证:主要验证符号引用对应的字符串能否找到对应的类,符号引用中的类、字段、方法的访问型是否被当前类访问等;
2.3 准备
准备阶段正式为类变量分配内存并设置类变量的初始值,这些变量都在方法区中进行分配。这个阶段内存分配仅包括被static修饰的类变量,不包括实例变量,实例变量会在对象实例化的时候一起分配在堆中。
2.4 解析
解析就是将常量池中的符号引用替换成直接引用的过程。
2.5 初始化阶段
这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
三、ClassLoader的加载机制
3.1 JDK默认提供三个ClassLoader
- BootStrap ClassLoader:类启动加载器,是Java类中最顶层的加载器,负责加载JDK中的核心类库。这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将
<JAVA_HOME>/lib
路径下的核心类库或-Xbootclasspath
参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar。 - Extendion ClassLoader:扩展类加载器,由Java语言实现,负责加载Java的扩展类库,默认加载JAVA_HOME/jre;/liblext目录下的所有jar。
- App ClassLoader:系统类加载器,负责加载classPath下所有的jar和class文件,一般可以通过ClassLoader.getSystemClassLoader()获取。
- 自定义类加载器:可以继承java.lang.ClassLoader来实现自定义类加载器。
加载器的层次为:
3.2 Java的类加载器基于三个机制:委托、单一、可见。
- 委托机制:指的是将加载类的请求传递给父加载器,如果父加载器找不到或者不能加载这个类,那么再加载它。
- 可见性机制:指的是父加载器加载的类都能被子加载器看见,但是子加载器加载的类父加载器是看不见的。
- 单一性机制:指的是一个类只能被同一种加载器加载一次。
3.3 双亲委托机制
除了引导类加载器外,所有的类加载器都有一个父类,通过getParent()方法获取。系统类加载器的父类是扩展类加载器,扩展类加载器的父类是引导类加载器。每一个ClassLoader实例都有一个父类的引用。当一个ClassLoader实例需要加载一个类时,先把这个任务交给父类加载器,这个过程是由上到下依次检查的。如果都没有加载成功,抛出ClassNotFoundException。
双亲委托机制的好处:1)主要是避免类的重复加载,父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。2)安全因素,确保java核心api中定义类型不会被随意替换。假设通过网络传递一个名为java.lang.Integer
的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer
,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
3.4 两个类相同判定条件
JVM判定两个类相同,不仅判定两个类的类名是否相同,还要判定是不是由同一个类加载器实例加载。
3.4 有没有打破双亲委托机制的类加载器?
有,比如tomcat自实现的类加载器(后面详细讲解)
四、类的初始化
4.1 JVM加载class的两种时机
- 隐式加载:碰到new 生成对象时
- 显式加载:class.forName(),this.getClass.getClassLoader.loadClass()。
4.2 什么时候开始初始化类
- new 一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forName("com.xxx"))
- 初始化一个类的子类(会首先初始化子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
4.3 自定义类加载器
自定义类的应用场景:
(1)加密:Java代码可以轻易的被反编译,如果需要把代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。
(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
(3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。
自定义类加载器:
public class MyClassLoader extends ClassLoader {
public MyClassLoader(ClassLoader parent){
super(parent);
}
public MyClassLoader(){}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = new byte[0];
try {
data = getClassData(name);
return this.defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
private byte[] getClassData(String name) throws Exception {
FileInputStream fis = new FileInputStream(name);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while(true){
int flag = fc.read(by);
if(flag ==0 || flag == -1){
break;
}
by.flip();
wbc.write(by);
by.clear();
}
return baos.toByteArray();
}
}