- 类加载机制概述
· 我们知道,一个.java文件在编译后会形成相应的一个或多个Class文件(若一个类中含有内部类,则编译后会产生多个Class文件),但这些Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的 类加载机制
二、Java类的生命周期
三、加载
主要是把class文件(磁盘或者网络等方式获取)的二进制字节流读入到jvm中
1通过类的全限定名获取类的二级制文件流
2将字节流所代表的静态存储结构转化为方法区的运行时数据结构
3 再内存中(对于HotSpot虚拟就而言就是方法区)生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
四、连接
4-1 验证
确保加载进来的字节符合jvm规范
- 文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
- 元数据验证,是否符合java语言规范
- 字节码验证
确保程序语义合法,符合逻辑分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现
- 符号引用验证,确保解析能够正常运行
4-2准备
为静态变量在方法区分配内存,并设置默认初始值
4-3解析
虚拟机将常量池内的符号引用替换为直接引用。
五、初始化
根据程序中的赋值语句主动为类变量赋值,初始化阶段是执行类构造器<clinit>()方法的过程,<clinit>()方法是编译由器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法
5-1什么时候需要初始化
1.使用new该类实例化对象的时候
2.读取或者设置类静态字段的时候或者调用类静态方法的时候(但被final修饰的字段,在编译器时就被放入常量池的静态字段除外static final,但如果是static final 修饰的引用类型如Integer,String等还是会加载)
3.初始化一个类的时候,有父类,先初始化父类(接口除外,父接口在调用的时候才会被初始化2.子类引用父类静态字段,只会引发父类的初始化)
4.被标明为启动类的类(即包含main的类)要初始化
5.JVM启动时标明的启动类,即文件名和类名相同的那个类
6.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
六、类加载器
6-1启动类加载器
它负责将JAVA_HOME/lib 负责加载$JAVA_HOME中jre/lib/rt.jar下面的核心类库,或者通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录
6-2拓展类加载器
它负责将JAVA_HOME /lib/ext或者由系统变量-D java.ext.dir指定位置中的类库加载到内存中,还可以加载-D java.ext.dirs选项指定的目录
6-3应用程序类加载器
加载当前应用的classpath的所有类
6-4 自定义加载器
使用场景
加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载
从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
6-5双亲委派模型
如果一个类接收到类加载请求,不会自己去加载类,而是将这个类加载委派给父类加载器,
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先判断该类型是否已经被加载 Class c = findLoadedClass(name); if (c == null) { //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载 try { if (parent != null) { //如果存在父类加载器,就委派给父类加载器加载 c = parent.loadClass(name, false); } else { // 递归终止条件 // 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代 // parent == null就意味着由启动类加载器尝试加载该类, // 即通过调用 native方法 findBootstrapClass0(String name)加载 c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,若加载成功,findClass方法返回的是defineClass方法的返回值 // 注意,若自身也加载不了,会产生ClassNotFoundException异常并向上抛出 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
|
|
双亲委托模型好处
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要 ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法