类加载器
一、 Java中有如下几种类加载器:
启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等。
扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。
应用程序加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
自定义加载器:负责加载用户自定义路径下的类包。
二、双亲委派机制
源码:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded 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) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
双亲委派模型的工作过程如下:
(1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
(2)如果没有找到,就去委托父类加载器去加载(如代码c = parent.loadClass(name, false)所示)。父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父加载器去加载。
(3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用拓展类加载器来尝试加载,继续失败则会使用AppClassLoader来加载,继续失败则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。
比如要加载自己写的String类,自定义一个String类放在某路径下,自定义一个类加载器继承ClassLoader类,并实现findClass方法(在自己的路径下去取String类)。重写loadClass方法让它不走双亲委派,这样他就会直接调用findClass加载自己的String类了。
双亲委派模型的好处:
(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
三、自定义类加载器
问:自定义类加载器怎么实现,其中哪个方法走双亲委派模型,(实现findclass方法,一般用defineclass加载外部类),如何才能不走双亲委派。(重写loadclass方法)
三个重要函数:loadClass,findClass,defineClass
loadClass:调用父类加载器的loadClass,加载失败则调用自己的findClass方法
findClass:根据名称读取文件存入字节数组
defineClass:把一个字节数组转为Class对象
为什么需要自定义类加载器 ?
自定义类的应用场景:
(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。
(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
(3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。
如何自定义类加载器?
- 继承ClassLoader类
- 覆盖findClass(String className)方法
例子如下:
User类
package com.cjc.classload; public class User { private int id; private String name; public User() { } public User(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public void sout(){ System.out.println("自定义类加载器"); } }
自定义MyClassLoad类
package com.cjc.classload; import java.io.FileInputStream; import java.lang.reflect.Method; public class MyClassLoad extends ClassLoader { private String classPath; public MyClassLoad(String classPath) { this.classPath = classPath; } private byte[] loadByte(String name)throws Exception{ name = name.replaceAll("\\.","/"); FileInputStream fis = new FileInputStream(classPath+"/"+name+".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try{ byte[] data = loadByte(name); return defineClass(name,data,0,data.length); }catch (Exception e){ e.printStackTrace(); throw new ClassNotFoundException(); } } public static void main(String[] args)throws Exception{ MyClassLoad classLoad = new MyClassLoad("D:/test"); Class clazz = classLoad.loadClass("com.cjc.classload.User"); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout",null); method.invoke(obj,null); System.out.println(clazz.getClassLoader().getClass().getName()); } }
注意:
运行结果展示: