三. ClassLoader
3.1 ClassLoader
3.1.1 ClassLoader介绍
类加载的三个阶段
- 加载:查找并加载类的二进制数据
链接:
- 验证:确保被加载类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转换为直接引用
- 初始化:为类的静态变量赋予正确的初始值
Java程序对类的使用方式
主动使用:
- new:直接使用---(不包括创建对象数据 new String[])
访问某个类或接口的静态变量,或者对静态变量进行赋值操作---(变量不能是常量)
- 对某个类的静态变量进行读写
- 对接口中的静态变量进行读(接口变量为final.只能进行读操作)
- 调用静态方法
- 反射某个类
- 初始化一个子类
- 启动类
被动使用
- 除以上六种方式外都是被动使用,不会导致类的初始化
注:
- 所有的Java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化,当然现在JVM有可能根据程序的上下文语意推断接下来可能需要的初始化类
3.1.2 类加载详解
类加载的含义
- 类的加载简单来说,就是将class文件中的二进制数据读取到内存中,将其放在方法区中,然后在堆中创建一个java.lang.Class对象,用来封装在存储在方法区中数据结构的实际数据
- 类的加载的最终产物是位于堆区中的Class对象
类加载的方式
- 本地磁盘中直接加载
- 内存中直接加载
- 通过网络加载.class
- 从zip,jar等归档文件中加载.class文件
- 数据库中提取.class文件
- 动态编译
JVM的内存介绍
- 方法区:存放运行时常量池,静态变量,Class的静态信息,数据结构
- 堆:存放真实的数据
- 程数计数器:字节码解析的时候,通过指针指定下一条解析的数据
- 虚拟机栈:每条线程都会创建一个栈帧
Class和Object
- 实际数据存储在堆中,而数据结构和逻辑存储在方法区中
- 句柄方式
- 指针方式
3.1.3 类的链接阶段
类的链接
- 在加载阶段完成后,虚拟机外部的二进制数据就会按照虚拟机所需的格式存储在方法区中(数据结构),然后在堆中创建一个Class对象,这个对象作为程序访问方法区中这些数据结构的外部接口
- 加载阶段与链接阶段的部分内容是可以交叉进行的,比如一部分代码加载完就可以进行验证,从而提高效率
验证:验证主要的目的是确保Class文件中的字节流中包含的信息符合虚拟机的要求,并且不会损害到JVM自身的安全
- VerifyError
文件格式验证
- 魔术因子是否正确
- 主从版本号是否符合当前虚拟机
- 常量池中的常量类型是不是支持
- etc
元数据验证
- 是否有父类
- 父类是不是允许继承
- 是否实现了抽象方法
- 是否覆盖了父类的final字段
- 其他的语意检查
- 字节码验证
- 符号引用验证
- 准备:准备阶段就是给类的静态变量分配内存和初始值
解析:把类中符号引用转换为直接引用
- 类或者接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
3.1.4 类的初始化
为类的静态变量赋予正确的初始值
- 类加载过程的最后一步
- 初始化阶段是执行构造函数<clinit>()方法的工程
- <clinit>()方法是由编译器自动收集类中的所有静态变量的赋值动作和静态语句块中的语句合并产生的
- 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,只能赋值,不能访问
- <clinit>()方法与类的构造函数有点区别,他不需要显示的调用父类的构造函数,虚拟机会保证子类的<clinit>()方法执行之前,先执行父类的<clinit>()方法,因此在虚拟机中首先被执行的是Object<clinit>()方法
- 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块,要优先于子类
- <clinit>()方法是线程安全的
3.1.5 类加载和对象创建流程
- 启动JVM,开始分配内存空间
- 开始加载Test.class文件,加载到方法区中,在加载的过程中静态的内容要进入静态区中
- 在开始运行main方法,这时JVM就会把main调用到栈中运行,开始从方法的第一行往下执行
- 在main方法中new Child(); 这时JVM就会在方法区中查找有没有Child文件,如果没有就加载Child.class文件,如果Child继承Parent类,那么也需要查找有没有Parent文件,如果没有也需要加载Parent.class文件
Child.class和Parent.class中所有的非静态内容会加载到非静态的区域中,而静态的内容会加载到静态区中,静态内容(静态变量,静态代码块,静态方法)
- 类的加载只会执行一次,下次再创建对象时,可以直接在方法区中获取class信息
- 开始给静态区中的所有静态的成员变量开始分配内存和默认初始值
之后给所有的静态成员变量显示初始化和执行静态代码块---<clinit>()方法
- 静态代码块时在类加载的时候执行的,类的加载只会执行一次所以静态代码块也只会执行一次
- 非静态代码块和构造函数中的代码是在对象创建的时候执行的,因此对象创建(new)一次,它们就会执行一次
- 这时Parent.class文件和Child.class文件加载完成
- 开始在堆中创建Child对象,给Child对象分配内存空间,其实就是分配内存地址
- 开始对类中的非静态的成员变量开始默认初始化
开始加载对应的构造方法,执行隐式三步
- 隐式的super()
- 显示初始化(给所有的非静态成员变量)
- 执行构造代码块
- 执行本类的构造方法
- 对象创建完成,把内存的地址赋值给引用对象使用
- 如果后续又创建(new)一个新的Child对象,重复步骤9之后的步骤
3.1.6 JVM类加载器
Java虚拟机自带了以下几种加载器
根(Bootstrap)类加载器
- 该加载器没有父加载器,它负责加载虚拟机的核心类库,如:java.lang.*等,根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库,根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类
扩展(Extension)类加载器
- 它的父加载器为根类加载器,它从java.ext.dirs系统属性所指定的目录加载类库,或从JDK安装目录jre/lib/ext子目录下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载,扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类
系统(System)类加载器
- 也称为应用类加载器,它的父加载器为扩展类加载器,它从环境变量classpath或系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器,系统类的加载器是纯Java类,是java.lang.ClassLoader类的子类
父委托机制案例:
- 当自定义一个java.lang.String类的时候,当主动使用自定义String类,实例化的是java定义的String类
- 这是因为当类加载器区初始化类时,会一层一层往上委托,先由Bootstrap类加载器区初始化,若找不到再由Extendsion类加载器去初始化,最后都找不到字节码文件时,再由应用加载器去初始化
若class存在classpath系统路径中,那么就会由系统类加载器去初始化,不能由我们自己定义的加载器初始化,只有在classpath下不存在才能使用自定义类加载器
public static void main(String[] args){ Class<?> clazz = Class.forName("java.lang.String"); System.out.print(clazz.getClassLoader); //null } public class String{ static { System.out.println("my custom String class"); } }
- 所以在加载String类时使用的是Bootstrap类加载器,若自定义的类加载器优先级更高,那么继承这个类的所有类都会受到影响,所以父委托机制就避免了这种安全性问题
- 父委托机制的优点:能提高提系统的安全性,在此机制下,用户自定义的类加载器不可能加载应该由父加载器加加载的可靠类,因此可以防止恶意代码替代父加载器的可靠代码
- 父子类加载器之间的真实关系---包装关系
3.1.7 自定义类加载器
自定义类加载器
- 使用defineClass()方法
- 重写findClass()方法
- 对外调用loadClass()方法
public class MyClassLoader extends ClassLoader{ private final static String DEFAULT_DIR = "D:\\classloader"; private String dir = DEFAULT_DIR; private String classLoaderName; public MyClassLoader(){ super(); } public MyClassLoader(String classLoaderName){ super(); this.classLoaderName = classLoaderName; } public MyClassLoadeR(String classLoaderName,ClassLoader classLoader){ super(classLoader); this.classLoaderName = classLoaderName; } @Override protected Class<?> findClass(String name) throw ClassNotFoundException{ String classPath = name.replace(".","/"); File classFile = new File(dir,classPath + ".class"); if(!classFile.exists()){ throw new ClassNotFoundException("the class" + name + "not found"); } byte[] classBytes = loadClassBytes(classFile); if(null == classBytes || classBytes.length == 0){ throw new ClassNotFoundException("the class" + name + "load failed"); } return this.defineClass(name,classBytes,0,classBytes.length); } private byte[] loadClassBytes(File file){ try(ByteArrayOutputStream bos = new ByteArrayOutputStream(); FileInputStream fis = new FileInputStream(file)){ byte[] buffer = new byte[1024]; int len; while((len = fis.read(buffer)) != -1){ bos.write(buffer,0,len); } bos.flush(); return bos.toByteArray(); }catch(IOException e){ e.printStackTrace(); return null; } } } -------------------------- publi static void main(String[] args){ MyClassLoader loader = new MyClassLoader("loader1"); load1.setDir("D:\\classloader"); Class<?> aclass = load1.loadClass("com.lsy.Demo1"); System.out.println(aclass); System.out.println((MyClassLoader)aclass.getClassLoader().getClassLoaderName()); }
- loadClass():是加载 类名.class字节码文件的工具
findClass():是类加载器在JVM内部实现查找指定路径下.class文件的机制
- Bootstrap---Ext---App,按照这个顺序进行查找
- 而自定义类加载器就是复写了该方法,将指定目录下的字节码文件,通过ByteArrayOutputStream解密后的字节码文件给JVM去加载
- defineClass():是将你定义的字节码文件经过字节数组流解密后,将该字节数组流生成字节码对象,也就是该类的 类名.class
loadClass():判断是否已加载,使用双亲委派机制,请求父加载器,使用findClass()
finaClass():过呢局名称和位置加载.class字节码的,使用defineClass()方法
- defineClass():解析定义.class字节流,返回class对象
3.1.8 打破双亲委托机制
- 自定义一个类加载器,在类加载器中同时重写loadClass()方法和findClass()方法,外界调用loadClass()方法不是从父类继承来的,而实子类自己的
@Override protexted Class<?> loadClass(String name,boolean resolve) throw ClassNotFoundException{ Class<?> clazz = null; if(name.startWith("java.")){ try{ ClassLoader system = ClassLoader.getSystemClassLoader(); clazz = System.loadClass(name); if(clazz != null){ if(resolve){ resolveClass(clazz); } return clazz; } }catch(Exception e){ e.printStackTrace(); return null; } } try{ clazz = findClass(name); }catch(Exception e){ } if(clazz == null && getParent() != null){ getParent().loadClass(name); } return clazz; }
注:
- 但是对于java.lang包下的类仍然是不能自定义的,因为此包下的类是不允许重名的,所以想自定义java.lang.String来测试双亲委派机制是不行的
3.1.9 名称空间和运行时包
命名空间
类加载器的命名空间
- 每个类的加载器都有自己的命名空间,命名空间由该加载器及其所有父加载器所加载的类组成
- 在同一个命名空间中,不会出现完成的名字
//Boot.Ext.App.SimpleClassLoader.com.lsy.Demo
运行时包
运行时包 = 命名空间 + 包名 + 类名
- 父类加载器看不到子类加载器加载的类
- 子加载器加载的类可以看到父加载器加载的类
- 不同命名空间下的类加载器之间的类互相不可访问
类的写案子及ClassLoader的卸载
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload)
- 该类所有的实例都已经被GC
- 加载该类的ClassLoader实例已经被GC
- 该类的java.lang.Class对象没有在任何地方被引用
- GC的时机我们是不可控的,同样对于Class的卸载也是不可控的
实例对象 ----> ClassLoader -----> Class对象
- 在Class中有一个ClassLoader的引用,所以需要先回收ClassLoader
注:
- 当一个已经被加载的类是无法被更新的,如果试图用用一个ClassLoader再次加载同一个类,就会得到duplicate classdefinition Exception,我们之恶能够重新创建一个新的ClassLoader实例来再次加载新类,至于原来已经加载的类就不需要管它了,因为它可能还有其他案例正在使用,只要相关的实例都被回收,那么JVM就会在适当的时机把类加载器卸载
如何实现一个工程中不同模块加载不同版本的同名JAR包?
- 在JVM里由类名和类加载器区别不同的Java类型,因此,JVM允许我们使用不同的加载器加载相同namespace的java类,而实际上这些相同namespace的java类可以是完全不同的类
- 通常我们都使用默认的类加载器,所以同步类或者同名jar包是唯一的,无法加载同名jar包的不同版本,而在JVM里不同的类加载器可以加载相同namespace的java类
3.1.10 自定义加密解密加载器
public final class EncryptUtils{ private static final byte ENCRYPT_FACTOR = (byte) 0xff; private EncryptUtils(){ //empty... } public static void doEncrypt(String source,String target){ try(FileInputStream fis = new FileInputStream(source); FileOutputStream fos = new FileOutputStream(target)){ int data; while((data = fis.read()) != -1){ fos.write(data ^ ENCRYPT_FACTOR); } fos.flush(); }catch(Exception e){ e.printStackTrace(); } } } ---------------------------------------- private byte[] loadClassBytes(File file){ try(ByteArrayOutputStream bos = new ByteArrayOutputStream(); FileInputStream fis = new FileInputStream(file)){ int data; while((data = fis.read()) != -1){ bos.write(data ^ EncryptUtils.ENCRYPT_FACTOR); } bos.flush(); return bos.toByteArray(); }catch(IOException e){ e.printStackTrace(); return null; } }
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。