类加载机制
一,Java虚拟机中类加载的全过程包括5个阶段:加载、验证、准备、解析、初始化。
1,加载阶段
1)通过一个类的全限定名来获取这个类的二进制字节流(class文件或者其他形式的二进制字节流);
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
3)在内存中生成一个代表这个类的java.lang.Class类型的对象实例,作为方法区这个类的各种数据的访问入口。
(java虚拟机规范没有规定一定放到堆区,HotSpot VM会把Class类的对象实例放到方法区)
接下来程序在运行过程中所有对该类的访问都通过这个类对象实例,也就是这个Class类型的类对象是提供给外界访问该类的接口。
2,验证(连接阶段的第一步)
验证是为了保证二进制字节流中的信息符合虚拟机规范,并没有安全问题。
注:编译器和虚拟机是两个独立的东西,所以JVM不确定加载进的二进制流编译完是否被修改。
验证阶段比较耗时,它非常重要但不一定必要,如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none参数关闭,以缩短类加载时间。
3,准备(连接阶段的第二步)
为类变量(即静态成员变量即被static修饰的变量)在方法区分配内存并设置类变量的初始值。
例1: public static int value =123;
在准备阶段为value设置的初始值为0而不是123;初始化阶段才会赋值123;
但是有特殊情况:被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性中,在准备阶段就将constantValue的值赋给该字段。
例2: public static final int value =123;
4,解析(连接阶段的第三步)
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
5,初始化
到初始化阶段,才真正的开始执行类中定义的java程序代码(或者说字节码);
初始化阶段就是执行类构造器clinit()的过程。
clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。
二,类加载器
1,类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器都有一个独立的类命名空间。直白点说就是比较两个运行时类是否“相等”,只有在两个类是同一个类加载器加载的情况下才有意义,如果两个运行时类源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。
2,类加载器
Java语言系统自带有三个类加载器:
1) Bootstrap ClassLoader 由C/C++编写的最顶层的加载器(不是java类),主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path
被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。
2)Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可加载-D java.ext.dirs
选项指定的目录。
3)Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类。
3,双亲委托
一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
委托过程和加载过程:
1. 请求加载某个Class,类加载器AppClassLoader先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器ExtClassLoader。
2. 如果ExtClassLoader也没有加载过(缓存中没有),则委托给其父加载器Bootstrap ClassLoader,它也去查找自己的缓存。到这就是委托过程
3. 如果Bootstrap ClassLoader也没有加载过(缓存中没有),就去找自己的规定的路径下查找,也就是sun.mic.boot.class
下面的路径。找到就返回,没有找到,让子加载器ExtClassLoade自己去找。
4. 则ExtClassLoader自己在java.ext.dirs
路径中去查找,查找成功就返回,查找不成功,再向下让子加载器AppClassLoader找。
5. AppClassLoader就自己查找,在java.class.path
路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。到这是加载过程。
我们可以发现委托是从下向上,然后具体查找过程却是自上至下。
4,自己写个类加载器 MyClassLoader
自定义步骤:
- 编写一个类继承自ClassLoader抽象类。
- 复写它的
findClass()
方法。 - 在
findClass()
方法中调用defineClass()
。
注意:
1)defineClass() 这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。
2)一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。
实验:
我们自定义一个ClassLoaser,名为 MyClassLoader,默认加载路径为D:\javaee\ClassLoaderTest下的jar包和资源。
public class ClassLoaderTest { public void say(){ System.out.println("自定义加载器MyClassLoader加载了我并对我 Say Hello"); } }
自定义类加载器 MyClassLoader
package classloader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class MyClassLoader extends ClassLoader { private String mLibPath; public MyClassLoader(String path) { mLibPath = path; } //重写findClass()方法 @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String fileName = getFileName(name); File file = new File(mLibPath, fileName); try { FileInputStream is = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len = 0; try { while ((len = is.read()) != -1) { bos.write(len); } } catch (IOException e) { e.printStackTrace(); } byte[] data = bos.toByteArray(); is.close(); bos.close(); //它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。 return defineClass(name, data, 0, data.length); } catch (IOException e) { e.printStackTrace(); } return super.findClass(name); } // 获取要加载 的class文件名 private String getFileName(String name) { int index = name.lastIndexOf('.'); if (index == -1) { return name + ".class"; } else { return name.substring(index + 1) + ".class"; } } }
测试Demo
package classloader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Demo { public static void main(String[] args) { //创建自定义MyClassLoader对象。 MyClassLoader myLoader = new MyClassLoader("D:\\javaee\\ClassLoaderTest"); try { //加载class文件 Class c = myLoader.loadClass("ClassLoaderTest"); //打印类加载器的名字以及类加载器的父加载器的名字 ClassLoader cl = c.getClassLoader(); System.out.println("加载我的类加载器是:"+cl.toString()); System.out.println("加载我的类加载器的父加载器为:"+cl.getParent().toString()); if(c != null){ try { Object obj = c.newInstance(); Method method = c.getDeclaredMethod("say",null); //通过反射调用ClassLoaderTest类的say方法 method.invoke(obj, null); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace(); } } } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
测试结果
1)可以看到加载D:\javaee\ClassLoaderTest下 ClassLoaderTest.class的类加载器为MyClassLoaser;
2)自定义类加载器默认的父加载器为AppClassLoader加载器;
3)自定义加载器MyClassLoader加载了 ClassLoaderTest.class,并成功运行了say函数。
三,类加载过程中“初始化”开始的时机
JVM规范中只定义了类加载过程中初始化过程开始的时机,加载、连接过程都应该在初始化之前开始(解析除外),这些过程具体在何时开始,JVM规范并没有定义,不同的虚拟机可以根据具体的需求自定义。
初始化开始的时机:
1)在运行过程中遇到如下字节码指令时,如果类尚未初始化,那就要进行初始化:new、getstatic、putstatic、invokestatic。这四个指令对应的Java代码场景是:
- 通过new创建对象;
- 读取、设置一个类的静态成员变量(不包括final修饰的静态变量);
- 调用一个类的静态成员函数。
2)使用java.lang.reflect进行反射调用的时候,如果类没有初始化,那就需要初始化;
3)当初始化一个类的时候,若其父类尚未初始化,那就先要让其父类初始化,然后再初始化本类;
4)当虚拟机启动时,虚拟机会首先初始化带有main方法的类,即主类;
主动引用 与 被动引用
JVM规范中要求在程序运行过程中,“当且仅当”出现上述4个条件之一的情况才会初始化一个类。如果间接满足上述初始化条件是不会初始化类的。
其中,直接满足上述初始化条件的情况叫做主动引用;间接满足上述初始化过程的情况叫做被动引用。
被动引用的场景示例
例1:
父类
package com.jp.loader; public class Fu { public static String name = "主动引用与被动引用实验"; static{ System.out.println("父类被初始化!"); } }
子类
package com.jp.loader; public class Zi extends Fu{ static{ System.out.println("子类被初始化!"); } }
测试类
package com.jp.loader; public class Demo { public static void main(String[] args){ System.out.println(Zi.name); } }
输出结果:
原因分析:
本示例看似满足初始化时机的第一条:当要获取某一个类的静态成员变量的时候如果该类尚未初始化,则对该类进行初始化。
但由于这个静态成员变量属于Fu类,Zi类只是间接调用Fu类中的静态成员变量,因此Zi类调用name属性属于间接引用,而Fu类调用name属性属于直接引用,由于JVM只初始化直接引用的类,因此只有Fu类被初始化。
例2:
package com.jp.loader; public class Demo2 { public static void main(String[] args) { Fu[] arr = new Fu[10]; } }
输出结果:
并没有输出“父类被初始化!”
原因分析:
这个过程看似满足初始化时机的第一条:遇到new创建对象时若类没被初始化,则初始化该类。
但现在通过new要创建的是一个数组对象,而非Fu类对象,因此也属于间接引用,不会初始化Fu类。
例3:
package com.jp.loader; public class Fu { public static final String name = "主动引用与被动引用实验"; static{ System.out.println("父类被初始化!"); } }
package com.jp.loader; public class Demo3 { public static void main(String[] args) { System.out.println(Fu.name); } }
输出结果:
原因分析:
本示例看似满足类初始化时机的第一个条件:获取一个类静态成员变量的时候若类尚未初始化则初始化类。
但是,Fu类的静态成员变量被final修饰,它已经是一个常量。被final修饰的常量在Java代码编译的过程中就会被放入它被引用的class文件的常量池中(这里是A的常量池)。所以程序在运行期间如果需要调用这个常量,直接去当前类的常量池中取,而不需要初始化这个类。
例4:
接口和类都需要初始化,接口和类的初始化过程基本一样,不同点在于:类初始化时,如果发现父类尚未被初始化,则先要初始化父类,然后再初始化自己;但接口初始化时,并不要求父接口已经全部初始化,只有程序在运行过程中用到当父接口中的东西时才初始化父接口。
类初始化:
package com.jp.loader; public class Fu { public static final String name = "主动引用与被动引用实验"; static{ System.out.println("父类被初始化!"); } }
package com.jp.loader; public class Zi extends Fu{ public static int age = 21; static{ System.out.println("子类被初始化!"); } }
package com.jp.loader; public class Demo { public static void main(String[] args){ System.out.println(Zi.age); } }
输出结果:
《深入理解java虚拟机》
https://blog.csdn.net/briblue/article/details/54973413
https://blog.csdn.net/u010425776/article/details/51251430