类加载机制深度解析 类加载过程 多个java文件经过编译打包生成可运行jar包,最终由java命令运行某个主类的main函数启 动程序,这里首先需要通过类加载器把主类加载到JVM。 主类在运行过程中如果使用到其它类,会逐步加载这些类。
类加载到使用整个过程有如下几步:加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
加载 :在硬盘上找到Java字节码文件加载到JVM
验证 :校验字节码文件的正确性
准备 :给类的静态变量分配内存并赋予默认值
解析 :将符号引用直接替换成直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接 引用,下节课会讲到动态链接
初始 化:对类的静态成员变量初始化为指定的值,执行静态代码块
注意,jar包里的类不是一次性全部加载的,是使用到时才加载。
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 public class TestDynamicLoad { static { System.out.println("*************load TestDynamicLoad************" ); } public static void main (String[] args) { new A(); System.out.println("*************load test************" ); B b = null ; } }class A { static { System.out.println("*************load A************" ); } public A () { System.out.println("*************initial A************" ); } }class B { static { System.out.println("*************load B************" ); } public B () { System.out.println("*************initial B************" ); } } 运行结果: *************load TestDynamicLoad************ *************load A************ *************initial A************ *************load test************
类加载器 Java里有如下几种类加载器
启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
自定义加载器:负责加载用户自定义路径下的类包
自定义类加载器 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 public class TestJDKClassLoader { public static void main (String[] args) { System.out.println(String.class.getClassLoader()); System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName()); System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName()); System.out.println(); ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); ClassLoader extClassloader = appClassLoader.getParent(); ClassLoader bootstrapLoader = extClassloader.getParent(); System.out.println("the bootstrapLoader : " + bootstrapLoader); System.out.println("the extClassloader : " + extClassloader); System.out.println("the appClassLoader : " + appClassLoader); System.out.println(); System.out.println("bootstrapLoader加载以下文件:" ); URL[] urls = Launcher.getBootstrapClassPath().getURLs(); for (int i = 0 ; i < urls.length; i++) { System.out.println(urls[i]); } System.out.println(); System.out.println("extClassloader加载以下文件:" ); System.out.println(System.getProperty("java.ext.dirs" )); System.out.println(); System.out.println("appClassLoader加载以下文件:" ); System.out.println(System.getProperty("java.class.path" )); } } 运行结果null sun.misc.Launcher$ExtClassLoader sun.misc.Launcher$AppClassLoader the bootstrapLoader : null the extClassloader : sun.misc.Launcher$ExtClassLoader@7ea987ac the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2 bootstrapLoader加载以下文件: file:/D:/software/JDK/jdk1.8 .0_171 /jre/lib/resources.jar file:/D:/software/JDK/jdk1.8 .0_171 /jre/lib/rt.jar file:/D:/software/JDK/jdk1.8 .0_171 /jre/lib/sunrsasign.jar file:/D:/software/JDK/jdk1.8 .0_171 /jre/lib/jsse.jar file:/D:/software/JDK/jdk1.8 .0_171 /jre/lib/jce.jar file:/D:/software/JDK/jdk1.8 .0_171 /jre/lib/charsets.jar file:/D:/software/JDK/jdk1.8 .0_171 /jre/lib/jfr.jar file:/D:/software/JDK/jdk1.8 .0_171 /jre/classes extClassloader加载以下文件: D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext appClassLoader加载以下文件: D:\software\JDK\jdk1.8 .0_171 \jre\lib\charsets.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\deploy.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\access-bridge-64. jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\cldrdata.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\dnsns.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\jaccess.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\jfxrt.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\localedata.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\nashorn.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\sunec.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\sunjce_provider.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\sunmscapi.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\sunpkcs11.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\ext\zipfs.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\javaws.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\jce.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\jfr.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\jfxswt.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\jsse.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\management-agent.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\plugin.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\resources.jar;D:\software\JDK\jdk1.8 .0_171 \jre\lib\rt.jar;D:\git\code\study\jvm-study\target\classes;D:\software\IDEA\idea20203pj_186103\lib\idea_rt.jar
自定义一个类加载器示例 自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法。
一个是 loadClass(String, boolean),实现了双亲委派机制,大体逻辑
首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载, 直接返回。
如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由 父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加 载器来加载。
如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器 的findClass方法来完成类加载。
另一个方法是findClass,默认实现是抛出异常,所以我们自定义类加载器主要是重写 findClass方法 。
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 class User { public void sout () { System.out.println("=======自己的加载器加载类调用方法=======" ); } private Integer id; private String name; public User (Integer id, String name) { this .id = id; this .name = name; } public User () { } public void setId (Integer id) { this .id = id; } public void setName (String name) { this .name = name; } @Override protected void finalize () throws Throwable { FinalizeTest.LIST.add(this ); System.out.println("关闭资源,userid=" + id + "即将被回收" ); } }public class MyClassLoaderTest { static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader (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 { MyClassLoader classLoader = new MyClassLoader("D:/test" ); Class clazz = classLoader.loadClass("classload.User1" ); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout" , null ); method.invoke(obj, null ); System.out.println(clazz.getClassLoader().getClass().getName()); } } 运行结果: =======自己的加载器加载类调用方法======= classload.MyClassLoaderTest$MyClassLoader
双亲委派机制 加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
JVM类加载器是有亲子层级结构的,如下图
这里类加载其实就有一个双亲委派机制 ,比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。。
双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载
为什么要设计双亲委派机制?
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
举例说明第1点
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 public class MyClassLoaderTest { static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader (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(); } } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null ) { long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } if (resolve) { resolveClass(c); } return c; } } } public static void main (String args[]) throws Exception { MyClassLoader classLoader = new MyClassLoader("D:/test" ); Class clazz = classLoader.loadClass("java.lang.String" ); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout" , null ); method.invoke(obj, null ); System.out.println(clazz.getClassLoader().getClass().getName()); } }
举例说明第2点
因为String已经被父类加载过,所以父类加载的String是不包含main方法的,应用加载器加载不到也加载不了我们自己定义的String类
打破双亲委派机制 Tomcat打破双亲委派机制 以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
1、 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本 ,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
2、部署在同一个web容器中相同的类库相同的版本可以共享 。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
3、 web容器也有自己依赖的类库,不能与应用程序的类库混淆 。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
4、web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。
Tomcat 如果使用默认的双亲委派类加载机制行不行?
答案是不行的。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性 。
第三个问题和第一个问题一样。
我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
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版本;
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
tomcat 这种类加载机制违背了java 推荐的双亲委派模型,每个webappClassLoader加载自己的目录下的class文件,不会默认传递给父类加载器(先尝试自己加载,再从父类那找),打破了双亲委派机制 。
模拟 1、模拟实现Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离
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 79 80 81 82 83 84 85 86 87 88 89 90 91 public class MyClassLoaderTest { static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader (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; } 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(); } } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null ) { long t1 = System.nanoTime(); if (!name.startsWith("com.jvm" )){ c = this .getParent().loadClass(name); }else { c = findClass(name); } sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } if (resolve) { resolveClass(c); } return c; } } } public static void main (String args[]) throws Exception { MyClassLoader classLoader = new MyClassLoader("D:/test" ); Class clazz = classLoader.loadClass("com.jvm.User1" ); Object obj = clazz.newInstance(); Method method= clazz.getDeclaredMethod("sout" , null ); method.invoke(obj, null ); System.out.println(clazz.getClassLoader()); System.out.println(); MyClassLoader classLoader1 = new MyClassLoader("D:/test1" ); Class clazz1 = classLoader1.loadClass("com.jvm.User1" ); Object obj1 = clazz1.newInstance(); Method method1= clazz1.getDeclaredMethod("sout" , null ); method1.invoke(obj1, null ); System.out.println(clazz1.getClassLoader()); } } 运行结果: =======自己的加载器加载类调用方法======= comjvm.MyClassLoaderTest$MyClassLoader@266474c2 =======另外一个User1版本:自己的加载器加载类调用方法======= com.jvm.MyClassLoaderTest$MyClassLoader@66d3c617
注意:同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。
2、模拟实现Tomcat的JasperLoader热加载
原理:后台启动线程监听jsp文件变化,如果变化了找到该jsp对应的servlet类的加载器引用(gcroot),重新生成新的JasperLoader 加载器赋值给引用,然后加载新的jsp对应的servlet类,之前的那个加载器因为没有gcroot引用了,下一次gc的时候会被销毁。