类加载过程
类加载示意图(顺序开始,不一定顺序结束)
加载(Loading)
预加载
虚拟机启动时的加载,加载的是JAVA_HOME/lib/下的rt.jar下的.class文件,此jar包下有java.lang.、java.util.、java.io.*等常用类。
运行时加载
- 虚拟机在用到一个.class文件时,会去内存中查看此.class文件是否被加载,如果没有就会按照类的
全限定名(包名+类名)
来加载这个类。 - 加载过程
获取.class文件的二进制流;
将类信息,静态变量,字节码,常量等.class文件中的信息放入方法区中;
在内存中生成一个代表这个.class文件的java.lang.Class对象,作为访问入口。一般这个Class在堆里,但HotSpot虚拟机将其放到方法区中;
验证
- 目的:确保.class文件的字节流中包含的信息符合当前虚拟机的要求,且不危害虚拟机的安全。
- 原因:因为.class文件未必要从Java源码编译而来,可以使用任何途径产生,虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
- 过程
文件格式验证;
元数据验证;
字节码验证;
符号引用验证
准备
- 准备阶段是正式为类变量(static修饰的)分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配(因此,即使不在后面的初始化阶段为类变量赋指定值,也不会报错,因为此时已经赋予了一个初始值),如public static int a = 123,在准备阶段过后,a的值为0,而不是123。
- 此阶段赋初值的变量是指非final修饰的static变量(final修饰的在编译阶段就已赋值)。
- 格式据类型初值
解析(在某些情况下可能在初始化后才开始)
虚拟机将常量池中的符号引用替换为直接引用。
初始化(类或接口被使用前的最后一项工作)
- 此阶段给static变量赋予用户指定的值,并执行静态代码块,就是执行类构造器方法<clinit>()的过程。
- 虚拟机会保证类的初始化在多线程环境中被正确的加锁,同步。
- 多个线程同时初始化一个类,只有一个线程去执行类的类构造器()方法,其他线程都会阻塞等待,直到活动线程执行()方法完毕,执行完毕后,其他阻塞的线程也不会再执行()方法,在一个类加载器下,一个类只初始化一次。
类一定初始化的场景(主动引用)
- 使用new实例化对象;读取或设置一个类的非final修饰的静态变量;调用一个类的静态方法的时候。
- 使用java.lang.reflect包中的方法对类进行反射调用的时候.
- 初始化一个类,发现其父类还没初始化过的时候,会对父类进行初始化。
- 启动虚拟机的时候,虚拟机会首先初始化包含main()方法的类。
类不初始化的场景(被动引用)
- 子类通过类名直接调用父类的静态字段。
- 通过数组定义引用类,不会触发此类的初始化。
- 调用final修饰的静态常量时,因为常量在编译期就会存入类的常量池中。
类加载器
类加载器的作用
通过类的全限定名加载此类的二进制流。
如何确定类在虚拟机中的唯一性?
通过加载它的类加载器和这个类本身确定此类的唯一性。
类加载器模型
结构图
类加载器分类
- 引导类(启动类)加载器
- 描述:嵌入虚拟机内核中的加载器,由c++编写。
- 作用:负责加载的是JAVA_HOME/lib下的类库,无法被java程序直接调用。
- 自定义类加载器
- 扩展类加载器
描述:
java语言实现,独立于虚拟机外部。
作用:
负责用于加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中所有类库。
java代码调用演示:
public class Test{ public static void main(String[] args){ System.out.println(System.getProperty("java.ext.dirs")); } }
- 应用程序类(系统类)加载器
描述:
java语言实现,独立于虚拟机外部。
作用:
加载项目bin目录下的.class文件。
java代码调用演示:
public class Test{ public static void main(String[] args){ System.out.println(ClassLoader.getSystemClassLoader()); } }
-
自定义类加载器
为什么要自定义类加载器:
【代码加密】: 对代码加密防止反编译,此时就需要自定义的类加载器在加载时,先进行解密。
【从非标准来源加载代码】: 如加载云端、数据库等非标准来源的字节码的时候,需要自定义类加载器。
自定义加载器的实现:
【MyClassLoader的实现】:public class MyClassLoader extends ClassLoader{ //文件路径 private String path; //文件类型 private String type; public MyClassLoader(String path, String type) { this.path = path; this.type = type; } /** * 重写findClass方法 * @param s 类的全限定名 * @return * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String s) throws ClassNotFoundException { try { byte[] result = getClassFromCustomPath(path+s); if (result == null) { throw new FileNotFoundException(); } else { return defineClass(s,result,0,result.length); } } catch (Exception e) { e.printStackTrace(); } return super.findClass(s); } private byte[] getClassFromCustomPath(String name) throws Exception { //将全限定名中的.替换为/ name = name.replaceAll("\\.","/"); name = name+type; 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 i = fc.read(by); if (i == 0 || i == -1) break; by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } }
【准备字节码文件】:
在此文件夹下,创建一个测试用的java文件,然后使用javac命令编译字节码文件即可。【测试】:
public class Test{ public static void main(String[] args){ //设置文件路径(全限定名之前的路径)和文件类型 MyClassLoader classLoader = new MyClassLoader("D:/work/",".class"); try { //使用全限定名 Class<?> clazz = Class.forName("com.classLoader.test.ClassLoaderTest",true,classLoader); Object obj = clazz.newInstance(); System.out.println(obj); System.out.println(obj.getClass().getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } }
【结果】:
结果显示使用了自定义的类加载器。【注意】:
如果读取的字节码文件是idea里自动生成的(即系统类加载器可以读取到),使用的类加载器就变为系统类加载器了(双亲委派模型)。
自定义加载器实现字节码加密解密:
【使用加密算法对上述测试的字节码文件加密】:public class EncryptUtils { public static void main(String[] args) throws FileNotFoundException{ File file = new File("D:/work/com/classLoader/test/ClassLoaderTest.class"); encrypt(file); } /** * 对指定文件进行编码(伪加密,Base64实际上是一个编码算法,并不是加密算法) * * @param file */ public static void encrypt(File file) throws FileNotFoundException { InputStream input = null; OutputStream output = null; String originalPath = file.getPath(); File targetFile = new File(originalPath + ".enc"); try { input = new FileInputStream(file); output = new FileOutputStream(targetFile); transfer(input, output); } catch (IOException e) { e.printStackTrace(); } finally { if (input != null) { try { input.close(); } catch (IOException e) { e.printStackTrace(); } } if (output != null) { try { output.close(); } catch (IOException e) { e.printStackTrace(); } } } //删除原文件 file.delete(); //将暂存文件,重命名为原来的文件名 targetFile.renameTo(new File(originalPath)); } /** * 将每个字节与11111111进行异或运算,模拟加密过程 * * @param input * @param output * @throws IOException */ public static void transfer(InputStream input, OutputStream output) throws IOException { int ch; while (-1 != (ch = input.read())) { //将每个字节和1111 1111 进行异或(模拟加密过程) ch = ch ^ 0xff; output.write(ch); } } }
还是使用上述的自定义加载器进行加载,发现无法加载了。
【自定义解密的加载器】:public class DecryptClassLoader extends ClassLoader{ //文件路径 private String path; //文件类型 private String type; public DecryptClassLoader(String path, String type) { this.path = path; this.type = type; } /** * 重写findClass方法 * @param s 类的全限定名 * @return * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String s) throws ClassNotFoundException { try { byte[] result = decodeFile(path+s); if (result == null) { throw new FileNotFoundException(); } else { return defineClass(s,result,0,result.length); } } catch (Exception e) { e.printStackTrace(); } return super.findClass(s); } private byte[] decodeFile(String name) { byte[] data = new byte[0]; InputStream input = null; ByteArrayOutputStream output = null; //将全限定名中的.替换为/ name = name.replaceAll("\\.", "/"); name = name + type; try { input = new FileInputStream(name); output = new ByteArrayOutputStream(); //将读入的文件流进行解密(对之前异或的结果再次进行异或操作,即可得到原来的值),并写入缓存中 EncryptUtils.transfer(input, output); data = output.toByteArray(); } catch (IOException e) { e.printStackTrace(); } finally { if (input != null) { try { input.close(); } catch (IOException e) { e.printStackTrace(); } } if (output != null) { try { output.close(); } catch (IOException e) { e.printStackTrace(); } } } return data; } }
使用解密类加载器加载
public class Test{ public static void main(String[] args){ //设置文件路径(全限定名之前的路径)和文件类型 DecryptClassLoader classLoader = new DecryptClassLoader("D:/work/",".class"); try { //使用全限定名 Class<?> clazz = Class.forName("com.classLoader.test.ClassLoaderTest",true,classLoader); Object obj = clazz.newInstance(); System.out.println(obj); System.out.println(obj.getClass().getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } }
结果显示加载成功!
双亲委派模型
- 机制
一个类加载器收到加载类的请求时,都会把请求委托给父类加载器,直到父类无法完成加载请求(搜索范围中没有找到请求的类),子类加载器才会自己加载。 - 过程
- 优势
- 避免类重复加载
- 保护java核心API,自定义的与核心API相同包名下的类无法加载。