三句话总结JDK8的类加载机制:
- 类缓存:每个类加载器对他加载过的类都有一个缓存。
- 双亲委派:向上委托查找,向下委托加载。
- 沙箱保护机制:不允许应用程序加载JDK内部的系统类。
JDK8的类加载体系
类加载器的核心方法
//protected声明可以被子类覆盖
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);//没有父类加载器(表示parent是BootstrapClassLoad)就查询BootstrapClassLoad的缓存不存在则创建,但是BootstrapClassLoad只创建核心类库(java.lang.*、java.util.* 等,非核心类会返回空或者或抛异常)}} catch (ClassNotFoundException e) {}if (c == null) {//BootstrapClassLoaderlong 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;}}
可以保护JDK内部的核心类不会被应用覆盖,因为本加载器缓存没有就会往父类加载器缓存找,核心类在bootstarp加载器缓存中存在。并且缓存相关代码是native无法直接被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;}
Linking链接过程
在ClassLoader的loadClass方法中还有一个resolveClass,是一个native方法,其实现的过程称为linking-链接。
- 加载:通过类加载器将class文件加载到jvm中(应用唯一可插手的步骤)
- 验证:验证class规范
- 准备:为静态变量分配内存赋默认值(例如int为0)
- 解析:符号引用解析为直接引用
- 初始化:静态变量赋值,执行类的静态代码块,初始化当前类的父类
Class.forName默认会直接进行Linking并初始化,ClassLoader.loadClass不会而是只加载(懒加载)
通过类加载器引入外部Jar包
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;}
}
-
哪些jar包适合放到外部加载?
规则引擎、统一审批规则、订单状态规则,因为会直接加载到jvm,要考虑安全性 -
外部jar包可以放到哪些地方?
URLClassLoader可以定义URL从远程Web服务器加载Jar包。
drools规则引擎实现了从maven仓库远程加载核心规则文件。
自定义类加载器实现Class代码混淆
对class进行MD5、对称加密、非对称加密,类加载器再进行解密。同时把解密类加载器通过另一个加载器内网远程加载,或者u盘加载来实现Class代码混淆加密
自定义类加载器实现热加载
在 Java 中,类加载器一旦加载某个类,该类就无法被“卸载”(除非整个类加载器被 GC 回收)。所以热加载的关键是:每次都使用一个新的 ClassLoader 实例来加载 class 文件,不复用旧的 ClassLoader,否则类会复用旧版本
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 {SalaryJARLoader salaryClassLoader = new SalaryJARLoader("/Users/roykingw/lib/SalaryCaler.jar");//每次都创建新的ClassLoader实例来加载 class 文件,新的ClassLoader里当然没有缓存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;}
}
这种热加载机制需要创建出非常多的ClassLoader对象。而这些不用的ClassLoader对象加载过的缓存对象也会随之成为垃圾。这会让JVM中本来就不大的元数据区带来很大的压力,极大的增加GC线程的压力。
把SalaryJARLoader加载过的类打印出来,你会发现,在加载SalaryCaler时,其实不光加载了这个类,同时还加载了Double和Object两个类。这两个类哪里来的?这就是JVM实现的懒加载机制。JVM为了提高类加载的速度,并不是在启动时直接把进程当中所有的类一次加载完成,而是在用到的时候才去加载。也就是懒加载。
打破双亲委派,实现同类多版本共存
外部加载的jar包,使用自定义加载器,这时如果代码中使用一样的类,那么AppClassLoader就有了缓存,就不会走自定义加载器的逻辑了
tomcat打破双亲委派
tomcat的几个主要类加载器:
- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;
- Jsp类加载器:针对每个JSP页面创建一个加载器。这个加载器比较轻量级,所以Tomcat还实现了热加载,也就是JSP只要修改了,就创建一个新的加载器,从而实现了JSP页面的热更新。
使用类加载器能不能不用反射?
强行的类型转换会报错Exception in thread “main” java.lang.ClassCastException: com.roy.oa.SalaryCaler cannot be cast to com.roy.oa.SalaryCaler
JDK的SPI扩展机制
ServiceLoader.load(SalaryCalService.class) 就可以查找到某一个接口的全部实现类。应用所需要的,是提供一个配置文件。 这个配置文件需要放在 ${classpath}/META-INF/services这个固定的目录下。然后文件名是传入接口的全类名。而文件的内容则是一行表示一个实现类的全类名。
SPI机制也是传入了ClassLoader的。
public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}
SPI配置文件可以放入jar包中,但是必须要使用URLClassLoader,由于向上委托查找会找到AppClassLoader,那么我们可以通过构造函数设置它的parent为salaryJARLoader就可以实现了。还可以通过java -cp这个启动参数直接把jar包导入