流程说明:
上图 通过java命令运行一个Math类 ,首先java.exe 会调用底层的jvm.dll文件创建java虚拟机(c++实现),然后虚拟机会创建一个引导类加载器。。。。
类加载过程(loadClass)主要分为以下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
加载:在硬盘上查找并通过IO读入字节码文件,使用到类的时候才会加载,例如调用类的main()方法,new对象等,在加载阶段会在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口(从硬盘加载到JVM内存中)
验证:校验字节码文件的正确性
准备:给类的静态变量分配内存,并赋予默认值
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接的过程(类加载期间完成),动态链接是在程序运行期间完成的,将符号引用替换为直接引用;
初始化:对类的静态变量初始化为指定的值,执行静态代码块
注:
1.类加载器加载类信息放到方法区之后,会创建一个class类型的对象实例放到堆(Heap)中,作为开发人员访问方法区中类定义的入口和切入点。
2.类被加载到方法区之后主要包含,运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息
3.主类在运行过程中如果使用到其它类,会逐步加载这些类,jar包或war包里的类不是一次性全部加载的,是使用到时(new对象 或 调用main函数)才加载。
类加载器和双亲委派机制:
1.类加载过程主要通过如下几种类加载器来完成:
1)引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
2)扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
3)应用程序类加载器:负责加载classpath路径下的类包,主要是加载自己写的那些类;
4)自定义加载器:负责加载用户自定义路径下的类包;
以下,来看一段加载器的示例代码:
package com.provider.ClassLoadMachine; import com.sun.crypto.provider.DESKeyFactory; import sun.misc.Launcher; import java.net.URL; public class TestClassLoad { public static void main(String[] args) { //引导类加载器不是JAVA对象 是C++生成的对象 所以再JVAV这边表现不出来 System.out.println("核心类加载器:"+String.class.getClassLoader()); //自己写的类由appClassLoader加载 System.out.println("自己写的类使用的加载器:"+TestClassLoad.class.getClassLoader()); //使用扩展类加载器 System.out.println("扩展类包下的加载器:"+DESKeyFactory.class.getClassLoader().getClass().getName()); System.out.println(); //AppClassloader 由Classloader里的静态方法由getSystemClassLoader进行获取 ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); //appClassLoader的父类时ExtClassLoader ClassLoader extClassLoader = appClassLoader.getParent(); //ExtClassLoader的父类是BootstrapClassLoader ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println("appClassLoader:"+appClassLoader); System.out.println("extClassLoader:"+extClassLoader); System.out.println("bootstrapClassLoader:"+bootstrapClassLoader); System.out.println(); System.out.println("bootstrapLoader加载以下文件:"); URL[] urls = Launcher.getBootstrapClassPath().getURLs(); for(Object url : urls){ System.out.println(url); } //extClassLoader加载以下文件: 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("java.class.path"); } }
执行结果如下:
核心类加载器:null
自己写的类使用的加载器:sun.misc.Launcher$AppClassLoader@58644d46
扩展类包下的加载器:sun.misc.Launcher$ExtClassLoaderappClassLoader:sun.misc.Launcher$AppClassLoader@58644d46
extClassLoader:sun.misc.Launcher$ExtClassLoader@533ddba
bootstrapClassLoader:nullbootstrapLoader加载以下文件:
file:/D:/JDK/jdk1.8.0_20/jre/lib/resources.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/rt.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/sunrsasign.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/jsse.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/jce.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/charsets.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/jfr.jar
file:/D:/JDK/jdk1.8.0_20/jre/classesextClassLoader加载以下文件
D:\JDK\jdk1.8.0_20\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\extappClassLoader加载以下文件
java.class.path
注:以上所有涉及到的应到类加载器打印都是为空,原因是应到类加载器是C++代码生成的对象所以再java这边打印出来为空
类加载器的初始化过程:
JVM启动时会创建JVM启动器实例sun.misc.Launcher.sun.misc.Launcher初始化使用了单例模式设计保证了一个JVM虚拟机内只有一个sun.misc.Launcher实例,在Launcher类的构造方法内,其创建了两个类加载器,分别是ExtClassLoader和AppClassLoader,JVM 默认使用Launcher类的getClassLoader来获取AppClassLoader的实例加载我们的应用程序自己的类:
双亲委派机制:
下图是双亲委派机制的简单流程图:
概念:当一个JAVA类需要加载时 它首先会委托给应用程序加载器,AppClassLoader会向上委托给ExtClassLoader加载,加载成功直接返回,如果加载失败扩展类加载器又会委托给应用程序类加载器加载,BootstrapClassLoader首先再自己核心目录下找目标类,加载失败则退回给应用程序加载器自己调用findClass自己进行加载。。。
沙箱安全机制:自定义加载器加载JDk的核心类式会跑SecurityException 沙箱安全机制
从AppClassLoader源码分析双亲委派机制的简单实现;
自定义加载器示例:
自定义加载器只需要继承java.lang.ClassLoader类该类有两个核心方法,一个是loadClass(String name,Boolean resolve),实现了双亲委派机制,还有一个是findClass,默认实现时空方法,所以我们自定义加载器主要是重写findClass方法。
以下是自定义加载器的代码实现:
package com.provider.ClassLoadMachine; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.lang.reflect.Method; /** *@description:自定义类加载器 *@author: WangJiKui *@date: 2020/8/4 17:52 */ public class MyClassLoaderTest { static class MyClassLoader extends ClassLoader{ private String classPath; public MyClassLoader(String classPath){ this.classPath=classPath; } /** * 将硬盘中的class文件转换为字节数组 * @param name * @return * @throws Exception */ 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; } /** * 重写findClass方法 * @param name * @return * @throws ClassNotFoundException */ @Override 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) { try { //初始化父类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器getSystemClassLoader() MyClassLoader myClassLoader = new MyClassLoader("D:/testCpu"); //这里的ClassName 是class文件中包加类的全路径名,单独建目录的时候,目录名也必须与class文件中的全包名一致 //场景:此类是我从项目中copy到D盘新建的一个文件,当项目中的此文件为删除时,此时打印出的加载器依然是AppClassLoader //因为MyClassLoader会先委派父类去加载,恰好此类之前由AppClassLoader已经加载过了,所以直接返回了(双亲委派) Class> arthas = myClassLoader.loadClass("com.provider.dao.Arthas"); Object object = arthas.newInstance(); Method[] declaredMethods = arthas.getDeclaredMethods(); for (int i=0;i//打印结果
public static void com.provider.dao.Arthas.main(java.lang.String[])
private static void com.provider.dao.Arthas.lambda$cpuHigh$1()
public static void com.provider.dao.Arthas.addHashSetThread()
private static void com.provider.dao.Arthas.deadThread()
public static void com.provider.dao.Arthas.cpuHigh()
private static void com.provider.dao.Arthas.lambda$deadThread$3(java.lang.Object,java.lang.Object)
private static void com.provider.dao.Arthas.lambda$deadThread$2(java.lang.Object,java.lang.Object)
private static void com.provider.dao.Arthas.lambda$addHashSetThread$0()*******************************************以上是被加载类中的方法******************************************************
此时使用的类加载器是:com.provider.ClassLoadMachine.MyClassLoaderTest$MyClassLoader
打破双亲委派机制:就是类加载时不委托给父类加载器加载,直接由自己的加载器加载;
话不多说,直接上代码:(思路:就是在以上自定义加载器的基础上,再重写ClassLoader中的loaderClass()方法,进行相应的逻辑修改)
package com.provider.ClassLoadMachine; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.lang.reflect.Method; /** *@description:自定义类加载器 *@author: WangJiKui *@date: 2020/8/4 17:52 */ public class MyClassLoaderTest { static class MyClassLoader extends ClassLoader{ private String classPath; public MyClassLoader(String classPath){ this.classPath=classPath; } /** * 将硬盘中的class文件转换为字节数组 * @param name * @return * @throws Exception */ 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; } /** * 重写findClass方法 * @param name * @return * @throws ClassNotFoundException */ @Override 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(); } } /** * 重写LoaderClass方法,实现自己的加载逻辑,不委派给双亲加载 * @param name * @param resolve * @return * @throws ClassNotFoundException */ @Override protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { 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.provider.dao")){ //自己的类才打破双亲委派 c = findClass(name); }else{ //否则依然走双亲委派逻辑(因为自定义加载器无法加载JDK的核心类--沙箱安全机制) c = this.getParent().loadClass(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) { try { //初始化父类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器getSystemClassLoader() MyClassLoader myClassLoader = new MyClassLoader("D:/testCpu"); //这里的ClassName 是class文件中包加类的全路径名,单独建目录的时候,目录名也必须与class文件中的全包名一致 //场景:此类是我从项目中copy到D盘新建的一个文件,当项目中的此文件为删除时,此时打印出的加载器依然是AppClassLoader //因为MyClassLoader会先委派父类去加载,恰好此类之前由AppClassLoader已经加载过了,所以直接返回了 Class> arthas = myClassLoader.loadClass("com.provider.dao.Arthas"); Object object = arthas.newInstance(); Method[] declaredMethods = arthas.getDeclaredMethods(); for (int i=0;i//打印结果
public static void com.provider.dao.Arthas.main(java.lang.String[])
private static void com.provider.dao.Arthas.deadThread()
private static void com.provider.dao.Arthas.lambda$cpuHigh$1()
public static void com.provider.dao.Arthas.cpuHigh()
public static void com.provider.dao.Arthas.addHashSetThread()
private static void com.provider.dao.Arthas.lambda$deadThread$2(java.lang.Object,java.lang.Object)
private static void com.provider.dao.Arthas.lambda$addHashSetThread$0()
private static void com.provider.dao.Arthas.lambda$deadThread$3(java.lang.Object,java.lang.Object)----------------------此刻即使父类已经加载过了Arthas这个类,也不会使用,只由MyClassLoader自己去加载----------------------
此时使用的类加载器是:com.provider.ClassLoadMachine.MyClassLoaderTest$MyClassLoader
Tomcat几个主要的类加载器:
commonClassLoader:Tomcat最基本的类加载器,加载路径中的class,可以被Tomcat容器以及各个webapp访问;
catalinaClassLoader:Tomcat容器私有类加载器,加载路径中的class对于webapp不可见
shareClassLoader:各个webapp共享的类加载器,加载路径中的class对于所有的Webapp可见,但对于Tomcat容器不可见
webAppClassLoader:各个webApp私有的类加载器,加载路径中的class只对当前的webapp可见,比如加载war包里相关类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同的war包应用引入不同的Spring版本,这样实现就能加载各自的spring版本
从上述流程图分析委派关系:
commonClassLoader能加载的类都可以被catalinaClassLoader和shareClassLoader使用,从而实现了共有类库的公用,而catalinaClassLoader和SharedClassLoader自己能加载的类则与对方隔离;
WebappClassLoader可以使用ShareClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.class文件:当web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的jsp类加载器来实现JSP文件的热加载功能;
很显然是违背了,tomcat为了实现隔离性没有遵守双亲委派机制,每个webAppClassLoader加载自己目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。
模拟Tomcat的webClassLoader加载自己war包应用内不同版本类实现共存与隔离:(其实就是,自己应用下的类通过自身的WebAppClassLoader去加载,核心类继续使用双亲委派机制(简单实现))
package com.provider.ClassLoadMachine; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.lang.reflect.Method; import java.util.logging.Logger; /** *@description:自定义类加载器 *@author: WangJiKui *@date: 2020/8/4 17:52 */ public class MyClassLoaderTest { static class MyClassLoader extends ClassLoader{ private String classPath; public MyClassLoader(String classPath){ this.classPath=classPath; } /** * 将硬盘中的class文件转换为字节数组 * @param name * @return * @throws Exception */ 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; } /** * 重写findClass方法 * @param name * @return * @throws ClassNotFoundException */ @Override 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(); } } /** * 重写LoaderClass方法,实现自己的加载逻辑,不委派给双亲加载 * @param name * @param resolve * @return * @throws ClassNotFoundException */ @Override protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { 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.provider.dao")){ //自己的类才打破双亲委派 c = findClass(name); }else{ //否则依然走双亲委派逻辑(因为自定义加载器无法加载JDK的核心类--沙箱安全机制) c = this.getParent().loadClass(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) { try { //初始化父类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器getSystemClassLoader() //模拟WebappClassLoader01 MyClassLoader myClassLoader = new MyClassLoader("D:/testCpu"); //这里的ClassName 是class文件中包加类的全路径名,单独建目录的时候,目录名也必须与class文件中的全包名一致 //场景:此类是我从项目中copy到D盘新建的一个文件,当项目中的此文件为删除时,此时打印出的加载器依然是AppClassLoader //因为MyClassLoader会先委派父类去加载,恰好此类之前由AppClassLoader已经加载过了,所以直接返回了 Class> arthas = myClassLoader.loadClass("com.provider.dao.Arthas"); /* Object object = arthas.newInstance(); Method[] declaredMethods = arthas.getDeclaredMethods(); for (int i=0;iarthas01 = myClassLoader01.loadClass("com.provider.dao.Arthas"); System.out.println("testCpu01使用的类加载器为:"+arthas01.getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } } } //打印结果l(两个类的全包名以及类名虽然完全相同,但类加载器不同,就可以区别为不同的类)
此时使用的类加载器是:com.provider.ClassLoadMachine.MyClassLoaderTest$MyClassLoader@685f4c2e
testCpu01使用的类加载器为:com.provider.ClassLoadMachine.MyClassLoaderTest$MyClassLoader@2e5d6d97
l
注意:同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类名,包名是否相同之外,还要看他们的类加载器是否相同;
。。。。。。。。文章或尚存在些许不足,欢迎留言指正。。。。。。。谢谢!!!