ClassLoader加载机制
Java类加载机制 我们编写”*.java”的文件需要先编译成”*.class”文件,当程序运行时,如果需要用到某个类,JVM通过类加载器把对应的class文件加载到内存中,然后再执行代码
JAVA的类加载器体系
三大类加载器:
Bootstrap ClassLoader:启动类加载器
ExtClassLoader:扩展类加载器
AppClassLoader:系统类加载器,又叫应用类加载器
package com.test;import org.apache.commons.dbutils.DbUtils;public class ClassLoaderDemo1 { public static void main (String[] args) throws ClassNotFoundException { ClassLoader cl1 = ClassLoaderDemo1.class.getClassLoader(); System.out.println("cl1 > " + cl1); System.out.println("parent of cl1 > " + cl1.getParent()); System.out.println("grant parent of cl1 > " + cl1.getParent().getParent()); ClassLoader cl2 = String.class.getClassLoader(); System.out.println("cl2 > " +cl2); System.out.println(cl1.loadClass("java.util.List" ).getClass().getClassLoader()); System.out.println("BootStrap ClassLoader加载目录:" + System.getProperty("sun.boot.class.path" )); System.out.println("Extention ClassLoader加载目录:" + System.getProperty("java.ext.dirs" )); System.out.println("Application ClassLoader加载目录:" + System.getProperty("java.class.path" )); } }
第一层结果分析:
首先就是我们程序本身加载的Classloader是AppClassLoader
我们通过getParent可以看到父类是ExtClassLoader,而ExtClassLoader父类是null
ClassLoader cl1 = ClassLoaderDemo1.class.getClassLoader(); System.out.println("cl1 > " + cl1); System.out.println("parent of cl1 > " + cl1.getParent()); System.out.println("grant parent of cl1 > " + cl1.getParent().getParent());
返回结果
cl1 > sun.misc.Launcher$AppClassLoader@18 b4 aac2 parent of cl1 > sun.misc.Launcher$ExtClassLoader@74 a14482 grant parent of cl1 > null
第二层分析:
我们可以看到String、int、List类是通过BootStrap Classloader加载的,返回的都是null
ClassLoader cl2 = String.class.getClassLoader(); System.out.println("cl2 > " +cl2); System.out.println(cl1.loadClass("java.util.List" ).getClass().getClassLoader());
返回结果:
java类加载体系
通过前面我们可以得到下面
BootStrap Classloader > ExtClassLoader > AppClassLoader
第三层分析:
我们可以看到不同的加载器有不同的加载目录
System.out.println("BootStrap ClassLoader加载目录:" + System.getProperty("sun.boot.class.path" )); System.out.println("Extention ClassLoader加载目录:" + System.getProperty("java.ext.dirs" )); System.out.println("Application ClassLoader加载目录:" + System.getProperty("java.class.path" ));
返回结果:
格式化后
BootStrap ClassLoader加载目录: E:\jar\commons-dbutils-1.7 .jar; D:\environment\jdk\jre\lib\resources.jar D:\environment\jdk\jre\lib\rt.jar; D:\environment\jdk\jre\lib\sunrsasign.jar; D:\environment\jdk\jre\lib\jsse.jar; D:\environment\jdk\jre\lib\jce.jar; D:\environment\jdk\jre\lib\charsets.jar; D:\environment\jdk\jre\lib\jfr.jar; D:\environment\jdk\jre\classes Extention ClassLoader加载目录: D:\environment\jdk\jre\lib\ext; C:\WINDOWS\Sun\Java\lib\ext Application ClassLoader加载目录: D:\environment\jdk\jre\lib\charsets.jar; D:\environment\jdk\jre\lib\deploy.jar; D:\environment\jdk\jre\lib\ext\access-bridge-64. jar; D:\environment\jdk\jre\lib\ext\cldrdata.jar; D:\environment\jdk\jre\lib\ext\dnsns.jar; D:\environment\jdk\jre\lib\ext\jaccess.jar; D:\environment\jdk\jre\lib\ext\jfxrt.jar; D:\environment\jdk\jre\lib\ext\localedata.jar; D:\environment\jdk\jre\lib\ext\nashorn.jar; D:\environment\jdk\jre\lib\ext\sunec.jar; D:\environment\jdk\jre\lib\ext\sunjce_provider.jar; D:\environment\jdk\jre\lib\ext\sunmscapi.jar; D:\environment\jdk\jre\lib\ext\sunpkcs11.jar; D:\environment\jdk\jre\lib\ext\zipfs.jar; D:\environment\jdk\jre\lib\javaws.jar; D:\environment\jdk\jre\lib\jce.jar; D:\environment\jdk\jre\lib\jfr.jar; D:\environment\jdk\jre\lib\jfxswt.jar; D:\environment\jdk\jre\lib\jsse.jar; D:\environment\jdk\jre\lib\management-agent.jar; D:\environment\jdk\jre\lib\plugin.jar; D:\environment\jdk\jre\lib\resources.jar; D:\environment\jdk\jre\lib\rt.jar; E:\ideaProject\classTest\target\classes; D:\apache-maven-3.8 .3 \maven-repo\commons-dbutils\commons-dbutils\1.7 \commons-dbutils-1.7 .jar; D:\IntelliJ IDEA 2020.1 .1 \lib\idea_rt.jar
双亲委派 一个java类加载到JVM的过程
从图看每个加载器都有一个加载目录,加载的类都有一个缓存
当我们有个HelloWorld类,通过AppClassLoader进行加载,如果在缓存中加载过则直接返回
如果没有缓存则找父加载器(ExtClassLoader)加载,如果缓存有则返回,
没则找父类(BootStrap)加载器加载,没有则在路径下进行查找
如果路径下没有则是委托下一级加载器(ExtClassLoader)加载,则在路径下进行查找,有的话直接返回
没有则找下一级子类加载器加载,没有则在路径下进行查找,如果最终找不到则报错
总结:
每个加载器对它加载过的类都有一个缓存
向上委托查找,向下委托加载
为什么要有双亲委派?
安全:就算自己定义了一个Java.lang.String,加载器也会通过AppClassLoader->ExtClassLoader->BootstrapLoader路径加载到核心jar包。这样便可以防止核心API库被随意篡改
避免类重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一
90%以上的类都是应用加载器进行加载,虽然第一次加载类的时候需要经历一次AppClassLoader->ExtClassLoader->BootstrapLoader。但是第二次用的时候就不需要了。如果直接从BootstrapLoader找有没有加载的话,第一次很快。但是已加载的类,特别是应用类加载器加载的,每次都需要经历引导类加载器和扩展类加载器,这样就太慢了。
JDK的类加载对象 与上面java类加载器的体系分开
基于一个classLoader接口,实现子接口SecureClassLoader,下面最关键实现类URLClassLoader,后面就是基于URLClassLoader的子类
ClassLoader -> SecureClassLoader -> URLClassLoader -> ExtClassLoader,AppClassLoader
代码拆分 通过URLClassLoader进行代码拆分,使得我们要写的代码分离
我们也可以封装为jar包,放在web服务器上、maven仓库(drools规则引擎)、本地等
LoadClassTest.java
package com.test;import java.net.MalformedURLException;import java.net.URL;import java.net.URLClassLoader;public class LoadClassTest { public static void main (String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///E:\ideaProject\classTest\out\artifacts\classTest_jar\Test.jar!/" )}); Class<?> c = urlClassLoader.loadClass("Test" ); c.newInstance(); } }
Test.jar
import java.io.IOException;public class Test { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } public static void main (String[] args) { } }
代码混淆 jar包我们都可以进行反编译,我们可以对jar包进行混淆
修改class文件的后缀,有效避免反编译
通过本地路径来读取class文件
我们将class后缀修改为myclass,加载的时候需要将后缀修改为class才能被执行,这样我们就要自定义一个类加载器
LoadClassTest.java
package com.test;import java.net.MalformedURLException;import java.net.URL;import java.net.URLClassLoader;public class LoadClassTest { public static void main (String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException { TestClassLoader testClassLoader = new TestClassLoader("E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\" ); Class<?> c = testClassLoader.loadClass("Test" ); c.newInstance(); } }
TestClassLoader.java
package com.test;import com.sun.xml.internal.ws.util.ByteArrayBuffer;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.nio.ByteBuffer;import java.security.SecureClassLoader;public class TestClassLoader extends SecureClassLoader { private String classPath; public TestClassLoader (String classPath) { this .classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String filePath = this .classPath + name.replace("." ,"\\" ).concat(".myclass" ); FileInputStream fis; ByteArrayBuffer ba = new ByteArrayBuffer(); byte [] b; int code; try { fis =new FileInputStream(new File(filePath)); while ((code = fis.read()) != -1 ){ ba.write(code); } b = ba.toByteArray(); return defineClass(name,b,0 ,b.length); } catch (Exception e) { throw new ClassNotFoundException("自定义类文件不存在" ); } } }
Test.myclass
import java.io.IOException;public class Test { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } public static void main (String[] args) { } }
通过读取jar包来读取class文件
读取jar包的文件,并且将文件myclass后缀修改为class后加载
package com.test;import java.net.MalformedURLException;import java.net.URL;import java.net.URLClassLoader;public class LoadClassTest { public static void main (String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException { TestClassLoader2 testClassLoader2 = new TestClassLoader2("E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\Test.jar" ); Class<?> c = testClassLoader2.loadClass("Test" ); c.newInstance(); } }
TestClassLoader2.java
读取的是jar包中的Test.myclass文件
package com.test;import com.sun.xml.internal.ws.util.ByteArrayBuffer;import java.io.File;import java.io.FileInputStream;import java.io.InputStream;import java.net.URL;import java.security.SecureClassLoader;public class TestClassLoader2 extends SecureClassLoader { private String jarFile; public TestClassLoader2 () { this .jarFile = jarFile; } public TestClassLoader2 (String jarFile) { this .jarFile = jarFile; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { URL fileURL; InputStream inputStream; String classPath = name.replace("." ,"/" ).concat(".myclass" ); FileInputStream fis; ByteArrayBuffer ba = new ByteArrayBuffer(); byte [] b; int code; try { fileURL = new URL("jar:file:\\" +this .jarFile+"!/" +classPath); inputStream = fileURL.openStream(); while ((code = inputStream.read()) != -1 ){ ba.write(code); } b = ba.toByteArray(); return defineClass(name,b,0 ,b.length); } catch (Exception e) { throw new ClassNotFoundException("自定义类文件不存在" ); } } }
通过改动二进制文件或者是加密来进行混淆
我们先写一个改变二进制文件,在文件头前面加上一个字符
package com.test;import java.io.*;public class FileCLass { public static void main (String[] args) throws IOException { File file1 = new File("E:\\ideaProject\\classTest\\target\\classes\\Test.class" ); File file2 = new File("E:\\ideaProject\\classTest\\target\\classes\\Test.myclass" ); FileInputStream fis = new FileInputStream(file1); FileOutputStream fos = new FileOutputStream(file2); int code = 0 ; fos.write(1 ); while ((code = fis.read())!= -1 ){ fos.write(code); } } }
自定义ClassLoader
只要在读取文件的时候,去掉一个字节即可
package com.test;import com.sun.xml.internal.ws.util.ByteArrayBuffer;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.nio.ByteBuffer;import java.security.SecureClassLoader;public class TestClassLoader extends SecureClassLoader { private String classPath; public TestClassLoader (String classPath) { this .classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String filePath = this .classPath + name.replace("." ,"\\" ).concat(".myclass" ); FileInputStream fis; ByteArrayBuffer ba = new ByteArrayBuffer(); byte [] b; int code; try { fis =new FileInputStream(new File(filePath)); fis.read(); while ((code = fis.read()) != -1 ){ ba.write(code); } b = ba.toByteArray(); return defineClass(name,b,0 ,b.length); } catch (Exception e) { throw new ClassNotFoundException("自定义类文件不存在" ); } } }
LoadClassTest.java
执行我们的自定义加载器没变
package com.test;import java.net.MalformedURLException;import java.net.URL;import java.net.URLClassLoader;public class LoadClassTest { public static void main (String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException { TestClassLoader testClassLoader = new TestClassLoader("E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\" ); Class<?> c = testClassLoader.loadClass("Test" ); c.newInstance(); } }
热加载 当我们对jar包进行修改或者删除,需要对服务器进行重启才能生效,我们就需要实现热加载,更新后立即程序
首先我们看下loadClass是如何加载类的
我们可以看到有另一个loadClass类调用
这也是双亲委派的重要关键代码
从代码看,我们可以得到每次加载类都会保留一次缓存,正是这次缓存导致我们无法实现热加载
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null ) { long t0 = System.nanoTime(); try { if (parent != null ) { c = parent.loadClass(name, false ); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null ) { long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
我们就可以将我们的远程代码封装为一个方法,每次调用的时候重新加载一次类,而不是从缓存读取
热加载方法
此处while循环是为了模拟运行时的web
可以看到main方法做了循环new 一个ClassLoader方法来实现热加载,这样就不会使用同一个ClassLoader来加载,如果是同一个ClassLoader则会导致读取缓存
LoadClassTest.java
package com.test;import java.net.MalformedURLException;public class LoadClassTest { public static void main (String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException, InterruptedException { while (true ){ Loader(); Thread.sleep(5000 ); } } private static void Loader () throws ClassNotFoundException, IllegalAccessException, InstantiationException { TestClassLoader2 testClassLoader2 = null ; testClassLoader2 = new TestClassLoader2("E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\classTest.jar" ); Class<?> c = testClassLoader2.loadClass("Test1" ); c.newInstance(); System.out.println("加载成功" ); } }
TestClassLoader2.java
我们的classloader没变动,新增一个提示重写加载类的打印
package com.test;import com.sun.xml.internal.ws.util.ByteArrayBuffer;import java.io.File;import java.io.FileInputStream;import java.io.InputStream;import java.net.URL;import java.security.SecureClassLoader;public class TestClassLoader2 extends SecureClassLoader { private String jarFile; public TestClassLoader2 (String jarFile) { this .jarFile = jarFile; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { System.out.println("重新加载类:" +name); URL fileURL; InputStream inputStream; String classPath = name.replace("." ,"/" ).concat(".class" ); FileInputStream fis; ByteArrayBuffer ba = new ByteArrayBuffer(); byte [] b; int code; try { fileURL = new URL("jar:file:\\" +this .jarFile+"!/" +classPath); inputStream = fileURL.openStream(); while ((code = inputStream.read()) != -1 ){ ba.write(code); } b = ba.toByteArray(); return defineClass(name,b,0 ,b.length); } catch (Exception e) { System.out.println("文件正在复制中,下次读取时生效" ); return null ; } } }
这样我们可以替换掉Test.jar文件来进行热加载
为什么loadClass传入resolve
一个类的类加载过程通常分为加载、连接、初始化 三个部分,具体的行为在java虚拟机规范中都有详细的定义, 这里只是大致的说明一下。
加载Loading:这个过程是Java将字节码数据从不同的数据源读取到VM中,并映射成为JVM认可的数据结构。而如果输入的Class不符合JVM的规范,就会抛出异常。这个阶段是用户可以参与的阶段,我们自定义的类加载Loading:这个过程是Java将字节码数据从不同的数据源读取到VM中,并映射成为JVM认可的数据结构。 而如果输入的Class不符合JVM的规范,就会抛出异常。这个阶段是用户可以参与的阶段,我们自定义的类加 载器,就是工作在这个过程。
连接Linking:这个是核心的步骤,又可以大致分为三个小阶段:
验证:检查JVM加载的字节信息是否符合Java虚拟机规范,否则就会报错。这一阶段是JVM的安全大门,防止黑客的恶意信息或者不合规信息危害JVM的正常运行。
准备:这一阶段创建类或接口的静态变量,并给这些静态变量赋一个初始值(不是最终指定的值),这一部分的作用更大的是预分配内存。
解析:这一步主要是将常量池中的符号引用替换为直接引用。例如我们有个类A调用了类B的方法,这些在代码层次还好只是一些对计算机没有意义的符号引用,在这一阶段就会转换成计算机所能理解的堆栈、引用等这些直接引用。
初始化Initialization:这一步才是 真正去执行类初始化的代码逻辑。包括执行static静态代码块,给静态变量赋值等
实际上resolve参数就是表示需不需要进行连接阶段,也就是说初始化阶段。从热加载机制另一个很大问题:热加载将一些在编译阶段可以检查出来的问题全都延迟到了运行时,这对整个程序的安全性是个很大的威胁
打破双亲委派
我们在项目中留一个Test.java文件
可以看到会先执行本地的Test类,而不是我们ClassLoader路径下的文件类
结果输出的是AppClassLoader,这也就可以看到我们的双亲委派是委托父类开始先加载,如果父类加载不成,才是子类进行加载
package com.test;import java.net.MalformedURLException;public class LoadClassTest { public static void main (String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException { TestClassLoader testClassLoader = new TestClassLoader("E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\" ); Class<?> c = testClassLoader.loadClass("Test" ); System.out.println(c.getClassLoader()); c.newInstance(); } }
我们要如何打破双亲委派机制?
我们可以重写loadClass方法来打破这个机制
package com.test;import com.sun.xml.internal.ws.util.ByteArrayBuffer;import java.io.File;import java.io.FileInputStream;import java.io.InputStream;import java.net.URL;import java.security.SecureClassLoader;public class TestClassLoader2 extends SecureClassLoader { private String jarFile; public TestClassLoader2 (String jarFile) { this .jarFile = jarFile; } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.startsWith("com.test" )){ return this .findClass(name); } return super .loadClass(name,resolve); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { System.out.println("重新加载类:" +name); URL fileURL; InputStream inputStream; String classPath = name.replace("." ,"/" ).concat(".class" ); FileInputStream fis; ByteArrayBuffer ba = new ByteArrayBuffer(); byte [] b; int code; try { fileURL = new URL("jar:file:\\" +this .jarFile+"!/" +classPath); inputStream = fileURL.openStream(); while ((code = inputStream.read()) != -1 ){ ba.write(code); } b = ba.toByteArray(); return defineClass(name,b,0 ,b.length); } catch (Exception e) { System.out.println("文件正在复制中,下次读取时生效" ); return null ; } } }
我们运行看看这次结果
重新加载类:com .test.Testcom .test.TestClassLoader2@7 f31245a