我们知道一个类创建的过程是: 首先是开辟空间,其次是初始化和指向空间地址,后面两个顺序可能不固定. 在此之前,JVM需要加载类文件到JVM,如果不加载那怎么知道该分配多大的存储空间呢,今天我们就来捋捋这个过程.
这次柯南要委托他爹妈了.
类加载的过程 JVM 加载类的机制有点类似懒加载 , 只有在类用到了的时候才会进行加载.
一个类加载进入虚拟机到能够给调用者使用的过程大概是这样的:
1:加载, 将Class文件加载进JVM.
2:验证, 验证Class文件的正确性.
3:准备, 给类的静态变量分配内存,并赋默认值.
4:解析, 将类的静态方法根据JVM指令替换成直接引用.
5:初始化, 类的静态变量初始化 和 执行静态代码块.
类经过上面5个过程,就加载进JVM了, 我们就已经可以使用其静态变量 以及 静态方法了. 如果需要使用其实例对象,那就在执行new
的过程,就是前面说的,开辟空间,初始化然后变量执行该地址.
加载 JVM 中有专门负责加载类的对象ClassLoader
类加载器.
类加载器可以分为4大类:
bootstrap class loader 引导类加载器
extension class loader 扩展类加载器
application class loader 应用类加载器
自定义ClassLoader 自定义类加载器
bootstrap class loader 引导类加载器: 负责加载jdk核心类库,比如:
rt.jar , jce.jar , charsets.jar,jsse.jar 等
extension class loader 扩展类加载器: 负责加载jdk扩展类库(ext目录下的类库),比如:
cldrdata,dnsns,jaccess,jfxrt,localedata,nashorn,sunec,sunjce_provider,sunpkcs11,zipfs
application class loader 应用类加载器:负责加载classPath路径下的类文件.比如自己写的代码,第三方类库.
就像我们在记事本里面编程一样,我们通过 java
命令将.java
文件编译成.class
文件, 在通过javac
执行. java javac
都是 c命令
. 类的加载底层其实也是c
去负责发起加载. C++
发起加载到 最终加载到class
类对象.
过程大概如下:
4种类加载器之间的关系 顺着调用的顺序,我们来看下Launcher
类;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Launcher { private static URLStreamHandlerFactory factory = new Launcher.Factory(); private static Launcher launcher = new Launcher(); private static String bootClassPath = System.getProperty("sun.boot.class.path" ); private ClassLoader loader; private static URLStreamHandler fileHandler; public static Launcher getLauncher () { return launcher; } public Launcher () { Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader" , var10); } try { this .loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader" , var9); } ... } public ClassLoader getClassLoader () { return this .loader; }
bootstrap class loader 引导类加载器 bootstrap class loader 负责加载jdk核心类库,如果我们通过下面代码来看看
1 2 ClassLoader classLoader = String.class.getClassLoader(); System.out.println(" classLoader = " + classLoader);
执行结果发现是
会不会感觉很意外? 因为bootstrap class loader 是C
层面的,所以java
中获取bootstrap class loader 时返回的是null
.
extension class loader 扩展类加载器 我们先来看下ExtClassLoader
的依赖关系.
ExtClassLoader
和 AppClassLoader
继承了URLClassLoader
都是ClassLoader
的子类.ClassLoader
类中维护了一个private final ClassLoader parent;
字段记录其层级关系
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static class ExtClassLoader extends URLClassLoader { public static Launcher.ExtClassLoader getExtClassLoader () throws IOException { return new Launcher.ExtClassLoader(var0); } } public ExtClassLoader (File[] var1) throws IOException { super (getExtURLs(var1), (ClassLoader)null , Launcher.factory); SharedSecrets.getJavaNetAccess().getURLClassPath(this ).initLookupCache(this ); }
从Launcher
类的构造方法可以看到,最终调用 new Launcher.ExtClassLoader(var0)
其层级关系的parent
传的是null
,也就是类似我们获取不到的bootstrap class loader 引导类加载器.
extension class loader 扩展类加载器层级关系的上级是 bootstrap class loader 引导类加载器
application class loader 应用类加载器 从Launcher
类的构造方法中可以看到,AppClassLoader
将ExtClassLoader
作为上级传入其构造.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static class AppClassLoader extends URLClassLoader { final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this ); public static ClassLoader getAppClassLoader (final ClassLoader var0) throws IOException { return new Launcher.AppClassLoader(var1x, var0); } AppClassLoader(URL[] var1, ClassLoader var2) { super (var1, var2, Launcher.factory); this .ucp.initLookupCache(this ); } }
到这里,我们可以认定,引导类加载器,扩展类加载器,应用类加载器的关系如下:
自定义ClassLoader 自定义类加载器 我们自己也可以定义类加载器,实现URLClassLoader / ClassLoader
自己定制加载类的逻辑.
如果是自定义的类加载器,其层级关系又该是怎样的呢?
首先我们来看下ClassLoader
的构造,ClassLoader
有一个无参构造方法;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 protected ClassLoader () { this (checkCreateClassLoader(), getSystemClassLoader()); } public static ClassLoader getSystemClassLoader () { initSystemClassLoader(); if (scl == null ) { return null ; } SecurityManager sm = System.getSecurityManager(); if (sm != null ) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; }
其中scl = l.getClassLoader();
l
是Launcher
.
getSystemClassLoader()
最终获取的类加载器 是 AppClassLoader
.
因此加入自定义加载器后,4 种加载器之间的关系应该是这样子的.
双亲委派机制 类加载的双亲委派机制 : 加载某个类,先从子层级委托父层级查找,如果父层级能找到就直接加载,所以父层级都找不到该类,再向下通知子层级,让子层级自己实现加载的机制.
打个比方来理解:
假如祖孙三代人, 孙子遇见一个问题, 就问父亲, 如果父亲知道问题就会直接告诉儿子,如果父亲也解决不了, 父亲在去问爷爷,爷爷如果知道就会告诉父亲,如果爷爷也不知道该问题, 爷爷就如实告诉父亲,”我解决不了这个问题, 你自己好好研究研究吧”,然后父亲也回来告诉儿子解决不了. 最终儿子自己想办法解决问题.
下面我们看看源码,看其是怎么实现的.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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; } }
这里我没有把自定义类加载器放进去,类加载的双亲委派机制大致流程如下:
jvm 类加载双亲委派机制好处
1 沙箱安全机制.
2 保证类加载的唯一性.
3 加载效率更高.
JVM 采用这种加载机制可以保证沙箱安全机制,以及加载类的唯一性.
沙箱安全机制: jdk提供的核心类库,保证核心类库API不会被篡改.
保证类加载的唯一性: 如果父层级类加载器以及加载该类,其子加载器就无需在加载了.
加载效率更高: 一般我们更多的是运行自己的代码而不是jdk的代码,通过这种自下而上的加载方式, 根据局部性原理,一个类用到了,说明后面很有可能还会被用到, 虽然加载自己的类每次都会绕一圈,但是如果加载过一次,后面就可以自己从 findLoadedClass
方法中获取到. 如果不是使用这种方式,每次都得全局查找一次.
破坏双亲委派机制 破坏双亲委派机制 其实就是破坏这种自下而上的调用方式,中断调用parent.loadClass( )
方法. 我们可以通过自定义加载器来达到这种效果.
自定义类加载器 项目中有两个一模一样的User
只是两个路径不一样, 根据前面所说的,双亲委派机制不会重复加载的特性, 下面打印出来的类加载器信息,应该都是AppClassLoader
,通过实现自定义加载器,实现不同的类用不同的类加载器就可以轻松实现同时加载进两个不同版本的User
类进JVM中.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 public static void main (String[] args) throws Exception { System.out.println("========" ); User user = new User(); user.print(); System.out.println("========" + user.getClass().getClassLoader()); MyClassLoader loader = new MyClassLoader("/Users/zhangwenhao/Desktop/test" ); Class<?> aClass = loader.loadClass("com.iz.study.pojo.User" ); Object o = aClass.newInstance(); Method print = aClass.getMethod("print" , null ); print.invoke(o, null ); System.out.println("========" + aClass.getClassLoader()); } static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader (String classPath) { this .classPath = classPath; } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null ) { long t0 = System.nanoTime(); if (!name.contains("com.iz.study" )) { c = this .getParent().loadClass(name); } 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; } } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte [] bytes = loadClassByte(name); return defineClass(name, bytes, 0 , bytes.length); } catch (Exception e) { e.printStackTrace(); } return super .findClass(name); } private byte [] loadClassByte(String name) throws Exception { name = name.replaceAll("\\." , "/" ); FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class" ); int available = inputStream.available(); byte [] bytes = new byte [available]; inputStream.read(bytes); inputStream.close(); return bytes; } }
Tomcat 底层也是用的类似这种逻辑实现,即使不同war用的相同类库不同版本也一样可以正常运行.