类加载运行全过程
package com.tuling.jvm;public class Math { public static final int initData = 666; public static User user = new User(); public int compute() { //一个办法对应一块栈帧内存区域 int a = 1; int b = 2; int c = (a + b) * 10; return c; } public static void main(String[] args) { Math math = new Math(); math.compute(); }}
通过Java命令执行代码的大体流程如下:
其中loadClass的类加载过程有如下几步:
加载 >> 验证 >> 筹备 >> 解析 >> 初始化 >> 应用 >> 卸载
- 加载:在硬盘上查找并通过IO读入字节码文件,应用到类时才会加载,例如调用类的 main()办法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为办法区这个类的各种数据的拜访入口
- 验证:校验字节码文件的正确性
- 筹备:给类的动态变量分配内存,并赋予默认值
解析:将符号援用替换为间接援用,该阶段会把一些静态方法(符号援用,比方 main()办法)替换为指向数据所存内存的指针或句柄等(间接援用),这是所谓的动态链接过 程(类加载期间实现),动静链接是在程序运行期间实现的将符号援用替换为间接援用,下 节课会讲到动静链接
- 初始化: 对类的动态变量初始化为指定的值,执行动态代码块
留神,主类在运行过程中如果应用到其它类,会逐渐加载这些类。 jar包或war包里的类不是一次性全副加载的,是应用到时才加载。
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; //B不会加载,除非这里执行 new B() }}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门路下的类包,次要就是加载你本人写的那些类
- 自定义加载器:负责加载用户自定义门路下的类包
看一个类加载器示例:
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")); }}运行后果:nullsun.misc.Launcher$ExtClassLoadersun.misc.Launcher$AppClassLoaderthe bootstrapLoader : nullthe extClassloader : sun.misc.Launcher$ExtClassLoader@3764951dthe appClassLoader : sun.misc.Launcher$AppClassLoader@14dad5dcbootstrapLoader加载以下文件:file:/D:/dev/Java/jdk1.8.0_45/jre/lib/resources.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/rt.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/sunrsasign.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/jsse.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/jce.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/charsets.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/jfr.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/classesextClassloader加载以下文件:D:\dev\Java\jdk1.8.0_45\jre\lib\ext;C:\Windows\Sun\Java\lib\extappClassLoader加载以下文件:D:\dev\Java\jdk1.8.0_45\jre\lib\charsets.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\deploy.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\access-bridge-64.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\cldrdata.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\dnsns.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\jaccess.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\jfxrt.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\localedata.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\nashorn.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunec.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunjce_provider.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunmscapi.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunpkcs11.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\zipfs.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\javaws.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jce.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jfr.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jfxswt.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jsse.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\management-agent.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\plugin.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\resources.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\rt.jar;D:\ideaProjects\project-all\target\classes;C:\Users\zhuge\.m2\repository\org\apache\zookeeper\zookeeper\3.4.12\zookeeper-3.4.12.jar;C:\Users\zhuge\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;C:\Users\zhuge\.m2\repository\org\slf4j\slf4j-log4j12\1.7.25\slf4j-log4j12-1.7.25.jar;C:\Users\zhuge\.m2\repository\log4j\log4j\1.2.17\log4j-1.2.17.jar;C:\Users\zhuge\.m2\repository\jline\jline\0.9.94\jline-0.9.94.jar;C:\Users\zhuge\.m2\repository\org\apache\yetus\audience-annotations\0.5.0\audience-annotations-0.5.0.jar;C:\Users\zhuge\.m2\repository\io\netty\netty\3.10.6.Final\netty-3.10.6.Final.jar;C:\Users\zhuge\.m2\repository\com\google\guava\guava\22.0\guava-22.0.jar;C:\Users\zhuge\.m2\repository\com\google\code\findbugs\jsr305\1.3.9\jsr305-1.3.9.jar;C:\Users\zhuge\.m2\repository\com\google\errorprone\error_prone_annotations\2.0.18\error_prone_annotations-2.0.18.jar;C:\Users\zhuge\.m2\repository\com\google\j2objc\j2objc-annotations\1.1\j2objc-annotations-1.1.jar;C:\Users\zhuge\.m2\repository\org\codehaus\mojo\animal-sniffer-annotations\1.14\animal-sniffer-annotations-1.14.jar;D:\dev\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar
类加载器初始化过程:
参见类运行加载全过程图可知其中会创立JVM启动器实例sun.misc.Launcher。
在Launcher构造方法外部,其创立了两个类加载器,别离是sun.misc.Launcher.ExtClassLoader(扩大类加载器)和sun.misc.Launcher.AppClassLoader(利用类加载器)。
JVM默认应用Launcher的getClassLoader()办法返回的类加载器AppClassLoader的实例加载咱们的应用程序
//Launcher的构造方法public Launcher() { Launcher.ExtClassLoader var1; try { //结构扩大类加载器,在结构的过程中将其父加载器设置为null var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { //结构利用类加载器,在结构的过程中将其父加载器设置为ExtClassLoader, //Launcher的loader属性值是AppClassLoader,咱们个别都是用这个类加载器来加载咱们本人写的应用程序 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); 。。。 。。。 //省略一些不需关注代码}
双亲委派机制
JVM类加载器是有亲子层级构造的,如下图
这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找指标类,找不到再委托下层父加载器加载,如果所有父加载器在本人的加载类门路下都找不到指标类,则在本人的类加载门路中查找并载入指标类。
比方咱们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩大类加载器加载,扩大类加载器再委托疏导类加载器,顶层疏导类加载器在本人的类加载门路里找了半天没找到Math类,则向下退回加载Math类的申请,扩大类加载器收到回复就本人加载,在本人的类加载门路里找了半天也没找到Math类,又向下退回Math类的加载申请给应用程序类加载器,应用程序类加载器于是在本人的类加载门路里找Math类,后果找到了就本人加载了。。
双亲委派机制说简略点就是,先找父亲加载,不行再由儿子本人加载
咱们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass办法最终会调用其父类ClassLoader的loadClass办法,该办法的大体逻辑如下:
1.首先,检查一下指定名称的类是否曾经加载过,如果加载过了,就不须要再加载,间接返回。
2.如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
3.如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用以后类加载器的findClass办法来实现类加载。
//ClassLoader的loadClass办法,外面实现了双亲委派机制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) { // 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(); //都会调用URLClassLoader的findClass办法在加载器的类门路里查找并加载该类 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; }}
为什么要设计双亲委派机制?
- 沙箱平安机制:本人写的java.lang.String.class类不会被加载,这样便能够避免外围API库被随便篡改
- 防止类的反复加载:当父亲曾经加载了该类时,就没有必要子ClassLoader再加载一次,保障被加载类的唯一性
看一个类加载示例:
package java.lang;public class String { public static void main(String[] args) { System.out.println("**************My String Class**************"); }}运行后果:谬误: 在类 java.lang.String 中找不到 main 办法, 请将 main 办法定义为: public static void main(String[] args)否则 JavaFX 应用程序类必须扩大javafx.application.Application
全盘负责委托机制
“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的应用另外一个ClassLoder,该类所依赖及援用的类也由这个ClassLoder载入。
自定义类加载器示例:
自定义类加载器只须要继承 java.lang.ClassLoader 类,该类有两个外围办法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个办法是findClass,默认实现是空办法,所以咱们自定义类加载器次要是重写findClass办法。
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); //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。 return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } } public static void main(String args[]) throws Exception { //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:/test"); //D盘创立 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录 Class clazz = classLoader.loadClass("com.tuling.jvm.User1"); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); }}运行后果:=======本人的加载器加载类调用办法=======com.tuling.jvm.MyClassLoaderTest$MyClassLoader
突破双亲委派机制
再来一个沙箱平安机制示例,尝试突破双亲委派机制,用自定义类加载器加载咱们本人实现的 java.lang.String.class
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(); } } /** * 重写类加载办法,实现本人的加载逻辑,不委派给双亲加载 * @param name * @param resolve * @return * @throws ClassNotFoundException */ 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) { // 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.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"); //尝试用本人改写类加载机制去加载本人写的java.lang.String.class 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()); }}运行后果:java.lang.SecurityException: Prohibited package name: java.lang at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659) at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
Tomcat突破双亲委派机制
以Tomcat类加载为例,Tomcat 如果应用默认的双亲委派类加载机制行不行?
咱们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
- 一个web容器可能须要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因而要保障每个应用程序的类库都是独立的,保障互相隔离。
- 部署在同一个web容器中雷同的类库雷同的版本能够共享。否则,如果服务器有10个应用程序,那么要有10份雷同的类库加载进虚拟机。
- web容器也有本人依赖的类库,不能与应用程序的类库混同。基于平安思考,应该让容器的类库和程序的类库隔离开来。
- 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 举荐的双亲委派模型了吗?答案是:违反了。
很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有恪守这个约定,每个webappClassLoader加载本人的目录下的class文件,不会传递给父类加载器,突破了双亲委派机制。
模仿实现Tomcat的webappClassLoader加载本人war包利用内不同版本类实现互相共存与隔离
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(); } } /** * 重写类加载办法,实现本人的加载逻辑,不委派给双亲加载 * @param name * @param resolve * @return * @throws ClassNotFoundException */ 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) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); //非自定义的类还是走双亲委派加载 if (!name.startsWith("com.tuling.jvm")){ c = this.getParent().loadClass(name); }else{ c = findClass(name); } // this is the defining class loader; record the stats 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.tuling.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.tuling.jvm.User1"); Object obj1 = clazz1.newInstance(); Method method1= clazz1.getDeclaredMethod("sout", null); method1.invoke(obj1, null); System.out.println(clazz1.getClassLoader()); }}运行后果:=======本人的加载器加载类调用办法=======com.tuling.jvm.MyClassLoaderTest$MyClassLoader@266474c2=======另外一个User1版本:本人的加载器加载类调用办法=======com.tuling.jvm.MyClassLoaderTest$MyClassLoader@66d3c617
==留神==:同一个JVM内,两个雷同包名和类名的类对象能够共存,因为他们的类加载器能够不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都雷同之外,还须要他们的类加载器也是同一个能力认为他们是同一个。
模仿实现Tomcat的JasperLoader热加载
原理:后盾启动线程监听jsp文件变动,如果变动了找到该jsp对应的servlet类的加载器援用(gcroot),从新生成新的JasperLoader加载器赋值给援用,而后加载新的jsp对应的servlet类,之前的那个加载器因为没有gcroot援用了,下一次gc的时候会被销毁。
附下User类的代码:
package com.tuling.jvm;public class User { private int id; private String name; public User() { } public User(int id, String name) { super(); 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("=======本人的加载器加载类调用办法======="); }}
补充:Hotspot源码JVM启动执行main办法流程
本文由博客一文多发平台 OpenWrite 公布!