Day14 JVM类加载机制
类加载机制
类加载就是把.class文件的二进制数据读进内存,经过校验、转换,最终变成JVM能用的class对象。二进制流也可以是字节码工具动态生成的,也可以是网络传输的,不一定非得来自class文件,只要格式对就行
- 类缓存: 每个类加载器对它加载过的类都有缓存
- 双亲委派:向上委托查找,向下委托加载
- 沙箱保护机制:不允许应用程序加载JDK内部系统类
-
类加载器ClassLoader,由底层开始逐层向上寻找
- 启动类加载器(BootStrap) 负责加载<JAVA_HOME>/lib下的核心类库
- 扩展类加载器(Extension) 负责加载<JAVA_HOME>/lib/ext下的扩展库
- 应用程序类加载器(Application) 复杂加载业务代码和第三方jar包

-
JDK8中的类加载器都继承自一个统一的抽象类ClassLoader,以下是类加载的核心方法,此方法中包含核心的双亲委派机制,而且方法声明为protected,意味着我们可以打破双亲委派机制,自行实现加载
什么是双亲委派模型? 当一个类加载器收到加载请求时,不会自己先去加载,而是把请求向上委派给父加载器,一直委派到顶层BootStrap ClassLoader,如果父类加载器加载不了,才轮到自己加载。 这样做的好处?
- 保护核心类库的安全,核心类永远由BootStrap加载,就算写一个同名类放到classpath下,也不会被加载到
- 避免类重复加载,同一个类在JVM中只会被加载一次,保证类的唯一性
▼java复制代码//类加载器的核心方法 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(); // 父类加载起没有加载过,就自行解析class文件加载。 c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } //这一段就是加载过程中的链接Linking部分,分为验证、准备,解析三个部分。 // 运行时加载类,默认是无法进行链接步骤的。 if (resolve) { resolveClass(c); } return c; } } -
类加载过程
整个类加载的过程分为三个大阶段加载、连接、初始化,连接又能拆分成验证、准备、解析三个步骤
- 加载 把二进制流读进内存,在方法区生成类的运行时数据结构,同时在堆里创建一个Class对象作为访问入口。是应用唯一可以介入干涉的步骤。
- 验证 校验二进制流是否符合JDK Class文件规范,为了安全考虑,防止恶意代码
- 准备 为静态变量分配内存并设置默认值(比如static int a = 1;此时的a=0,并不是1),此时变量为半初始化状态。
- 解析 将常量池里的符号引用转换成为直接引用。符号引用:一个字符串标识,java/lang/Object;直接引用是真正的内存地址
- 初始化 执行构造器clinit方法,这时候才真正执行复制操作

-
沙箱保护机制
前文讲到双亲委派机制最大的作用就是保护核心类库不会被应用覆盖,而java为了确保内部核心类的安全性,在双亲委派的基础上,还增加了一层保护机制,简单粗暴的禁止加载以java.开头的类加载
▼java复制代码// ClassLoader.java private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) { if (!checkName(name)) throw new NoClassDefFoundError("IllegalName: " + name); // 不允许加载核心类 if ((name != null) && name.startsWith("java.")) { throw new SecurityException ("Prohibited package name: " + name.substring(0, name.lastIndexOf('.'))); } if (pd == null) { pd = defaultDomain; } if (name != null) checkCerts(name, pd.getCodeSource()); return pd; } -
实战演练
-
通过类加载器加载外部jar包
什么时候适合外部加载? 流程统一,但实现经常变化的场景,比如工作流引擎
▼java复制代码public class OADemo2 { public static void main(String[] args) throws Exception { Double salary = 15000.00; Double money = 0.00; // 也可以加载远程文件 URL jarPath = new URL("file:/Users/roykingw/DevCode/ClassLoadDemo/out/artifacts/SalaryCaler_jar/SalaryCaler.jar"); URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {jarPath}); //模拟不停机状态 while (true) { try { money = calSalary(salary,urlClassLoader); System.out.println("实际到手Money:" + money); }catch(Exception e) { e.printStackTrace(); System.out.println("加载出现异常 :"+e.getMessage()); } Thread.sleep(5000); } } private static Double calSalary(Double salary,ClassLoader classloader) throws Exception { Class<?> clazz = classloader.loadClass("com.roy.oa.SalaryCaler"); if(null != clazz) { Object object = clazz.newInstance(); return (Double)clazz.getMethod("cal", Double.class).invoke(object, salary); } return -1.00; } } -
自定义加载器
▼java复制代码// SalaryJARLoader.java public class SalaryJARLoader extends SecureClassLoader { private String jarFile; public SalaryJARLoader(String jarFile) { this.jarFile = jarFile; } @Override protected Class<?> findClass(String fullClassName) throws ClassNotFoundException { String classFilepath = fullClassName.replace('.', '/').concat(".class"); System.out.println("重新加载类:"+classFilepath); int code; try { // 访问jar包的url URL jarURL = new URL("jar:file:" + jarFile + "!/" + classFilepath); // InputStream is = jarURL.openStream(); URLConnection urlConnection = jarURL.openConnection(); // 不使用缓存 不然有些操作系统下会出现jar包无法更新的情况 urlConnection.setUseCaches(false); InputStream is = urlConnection.getInputStream(); // 此时可以对.class文件里面的数据做一些加密操作 ByteArrayOutputStream bos = new ByteArrayOutputStream(); while ((code = is.read()) != -1) { bos.write(code); } byte[] data = bos.toByteArray(); is.close(); bos.close(); return defineClass(fullClassName, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); System.out.println("加载出现异常 :"+e.getMessage()); throw new ClassNotFoundException(e.getMessage()); // return null; } } } // OADemo3.java public class OADemo3 { public static void main(String[] args) throws Exception { Double salary = 15000.00; Double money = 0.00; SalaryClassLoader salaryClassLoader = new SalaryClassLoader("/Users/roykingw/DevCode/ClassLoadDemo/out/production/SalaryCaler/"); //模拟不停机状态 while (true) { try { money = calSalary(salary,salaryClassLoader); System.out.println("实际到手Money:" + money); }catch(Exception e) { System.out.println("加载出现异常 :"+e.getMessage()); System.exit(-1); } Thread.sleep(5000); } } private static Double calSalary(Double salary,ClassLoader classloader) throws Exception { Class<?> clazz = classloader.loadClass("com.roy.oa.SalaryCaler"); if(null != clazz) { Object object = clazz.newInstance(); return (Double)clazz.getMethod("cal", Double.class).invoke(object, salary); } return -1.00; } } -
自定义类加载器实现热加载
类加载什么时候会触发?
- 遇到new、getstatic、putstatic、invokestatic这四条字节码指令
- 使用java.lang.reflect 反射包对类进行反射调用
- 初始化子类时发现父类还未初始化,此时除法父类加载
- JVM启动时指定的主类执行(包含main方法的那个类)
▼java复制代码public class OADemo5 { public static void main(String[] args) throws Exception { Double salary = 15000.00; Double money = 0.00; //模拟不停机状态 while (true) { try { money = calSalary(salary); System.out.println("实际到手Money:" + money); }catch(Exception e) { System.out.println("加载出现异常 :"+e.getMessage()); } Thread.sleep(5000); } } private static Double calSalary(Double salary) throws Exception { // 每次调用都重新new一个实例出来,虽然能实现热更新,但是问题也很明显 // 创建的对象过多会造成GC线程压力 SalaryJARLoader salaryClassLoader = new SalaryJARLoader("/Users/roykingw/lib/SalaryCaler.jar"); System.out.println(salaryClassLoader.getParent()); Class<?> clazz = salaryClassLoader.loadClass("com.roy.oa.SalaryCaler"); if(null != clazz) { Object object = clazz.newInstance(); return (Double)clazz.getMethod("cal", Double.class).invoke(object, salary); } return -1.00; } } -
打破双亲委派
现实中打破双亲委派的场景
- SPI机制 使用Thread.currentThread().getContextClassLoader()获取线程上下文类加载器进行加载
- Tomcat实现应用隔离和热部署
▼java复制代码public class SalaryJARLoader6 extends SecureClassLoader { private String jarFile; public SalaryJARLoader6(String jarFile) { this.jarFile = jarFile; } @Override public Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException { // 把双亲委派机制反过来,先到子类加载器中加载,加载不到再去父类加载器中加载。 Class<?> c = null; synchronized (getClassLoadingLock(name)) { c = findLoadedClass(name); if(c == null){ c = findClass(name); if(c == null){ c = super.loadClass(name,resolve); } } } return c; } @Override protected Class<?> findClass(String fullClassName) throws ClassNotFoundException { String classFilepath = fullClassName.replace('.', '/').concat(".class"); System.out.println("重新加载类:"+classFilepath); int code; try { // 访问jar包的url URL jarURL = new URL("jar:file:" + jarFile + "!/" + classFilepath); URLConnection urlConnection = jarURL.openConnection(); urlConnection.setUseCaches(false); InputStream is = urlConnection.getInputStream(); // InputStream is = jarURL.openStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); while ((code = is.read()) != -1) { bos.write(code); } byte[] data = bos.toByteArray(); is.close(); bos.close(); return defineClass(fullClassName, data, 0, data.length); } catch (Exception e) { // e.printStackTrace(); //当前类加载器出现异常,就会通过双亲委派,交由父加载器去加载 // System.out.println("加载出现异常 :"+e.getMessage()); // throw new ClassNotFoundException(e.getMessage()); return null; } } } -
如何不在类加载里面使用反射而达到相同效果?
如果直接做强制类型转换,会报错。这类加载器的隔离性问题,因为判定两个类是否是同一个,需要符合两个条件,一是全限定类名一致,二是加载他的ClassLoader一致,这里是由两个不同的Loader加载的,尽管字节码内容一模一样,JVM依旧会判定是两个不同的类型,因此不能强转。 这里如果使用Class.forName来加载是可以的,会拿到调用者的ClassLoader加载,就可以进行强转
Class.forName和ClassLoader区别
- Class.forName会初始化静态变量而ClassLoader仅会加载并赋默认值(Class<?> clazz = Class.forName("MyClass", false, this.getClass().getClassLoader());false参数可以实现不初始化)
- Class.forName使用的Loader是调用者的ClassLoader,一般是当前线程上下文或者过应用类加载器,而ClassLoader取决你调用的哪个实现类
- Class.forName适合需要立即初始化使用的场景,如JDBC、配置注册等;ClassLoader适合只想加载但不初始化的场景,如性能优化,延迟加载等
▼java复制代码Class<?> clazz = salaryJARLoader.loadClass("com.roy.oa.SalaryCaler"); Object obj = clazz.newInstance(); SalaryCaler caler = (SalaryCaler)obj; // 得到报错 // Exception in thread "main" java.lang.ClassCastException: com.roy.oa.SalaryCaler cannot be cast to com.roy.oa.SalaryCaler使用SPI机制,load方法通过传入Thread.currentThread().getContextClassLoader()线程上下文加载器来加载
-
▼java复制代码// ServiceLoader.java public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } ```



