一、类的加载器
① ClassLoader的作用
1、ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,
ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。
然后交给Java虚拟机进行链接、初始化等操作。
因此,ClassLoader在整个装载(加载)阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。
至于它是否可以运行,则由Execution Engine决定。
2、类加载器最早出现在Java1.0版本中,那个时候只是单纯地为了满足Java Applet应用而研发出来。
但如今类加载器却在OSGI(热部署)、字节码加密解密领域大放异彩。这主要归功于Java虚拟机的设计者当初在设计类加载器的时候,
并没有考虑将它绑定在Jvm内部,这样做的好处就是能够更加灵活和动态地执行类加载操作
② class文件的显式加载与隐式加载
class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式(在日常开发以上两种方式一般会混合使用)
1、显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象
2、隐式加载:则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,
如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。比如 new User()
二、类加载的分类与测试
①类加载器的介绍
1、JVM支持两种类型的类加载器,分别为引导类加载器和自定义加载器。
引导类加载器(Bootstrap ClassLoader) 自定义加载器(User-Defined ClassLoader)
2、从概念来讲,自定义加载器一般是指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这样定义,
而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
3、无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有三种:
除了顶层的启动类加载器外,其余的类加载器都应当有自己的"父类"加载器
②各种类加载器
1、启动类加载器(引导类加载器,Bootstrap ClassLoader)
这个类加载器使用C/C++语言实现的,嵌套在JVM内部。
它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar,resources.jar或者sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
并不继承java.lang.ClassLoader,没有父加载器。
加载扩展类和应用程序类加载器,并指定为它们的父类加载器。
出于安全考虑,Bootstrap启动类加载器只加载包名为java,javax,sun等开头的类。
2、扩展类加载器(Extension ClassLoader)
java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
派生于ClassLoader类
父类加载器为启动类加载器
从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录jre/lib/ext子目录(扩展目录)下加载类库。
如果用户创建的JAR放在此目录下,也为自动由扩展类加载器加载。
3、应用程序类加载器(系统类加载器,AppClassLoader)
java语言编写,由sun.misc.Launcher$AppClassLoader实现
派生于ClassLoader类
父类加载器为扩展类加载器
它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库。
该类加载的程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载。
通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
4、用户自定义类加载
在java的日常应用程序开发中,类的加载几乎由以上三种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
隔离加载类、修改类加载的方式、扩展加载源、防止源码泄露。
5、用户自定义类加载的步骤
说明一
1、开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载,以满足一些特殊需求
2、在jdk1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在jdk1.2之后已
不建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中。
3、在编写自定义类加载时,如果没有太过复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法以及获取字节码流的方式。
使自定义加载器编写更加简洁。
说明二
4、重写loadClass()方法(不推荐,这个方法会保证类的双亲委派机制)。
5、重写findClass()方法 -->推荐。
6、这两种方法本质上差不多,毕竟loadClass()也会调用findClass(),但是从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。
建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用
手写一个简单的自定义加载类
public class UserClassLoader extends ClassLoader { private String rootDir; public UserClassLoader(String rootDir) { this.rootDir = rootDir; } /** * 编写findClass方法的逻辑 */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 获取类的class文件字节数组 byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { //直接生成class对象 return defineClass(name, classData, 0, classData.length); } } /** * 编写获取class文件并转换为字节码流的逻辑 * @param className * @return */ private byte[] getClassData(String className) { // 读取类文件的字节 String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; // 读取类文件的字节码 while ((len = ins.read(buffer)) != -1) { baos.write(buffer, 0, len); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } /** * 类文件的完全路径 */ private String classNameToPath(String className) { return rootDir + "\\" + className.replace(‘.‘, ‘\\‘) + ".class"; } public static void main(String[] args) { String rootDir = "D:\\code\\workspace_teach\\JVMdachang210416\\chapter02_classload\\src\\"; try { //创建自定义的类的加载器1 UserClassLoader loader1 = new UserClassLoader(rootDir); Class clazz1 = loader1.findClass("com.xiaozhi.java3.User"); //创建自定义的类的加载器2 UserClassLoader loader2 = new UserClassLoader(rootDir); Class clazz2 = loader2.findClass("com.xiaozhi.java3.User"); //clazz1与clazz2对应了不同的类模板结构 System.out.println(clazz1 == clazz2); System.out.println(clazz1.getClassLoader()); System.out.println(clazz2.getClassLoader()); Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("com.xiaozhi.java3.User"); System.out.println(clazz3.getClassLoader()); System.out.println(clazz1.getClassLoader().getParent()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
7、获取ClassLoader的途径
方式一:获取当前类的ClassLoader
clazz.getClassLoader()
方式二:获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
方式三:获取系统的classLoader
ClassLoader.getSystemClassLoader()
方式四:获取调用者的ClassLoader
DriverManager.getCallerClassLoader()
8、站在程序的角度看,引导类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加载器。
引导类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。
由于引导类加载器压根儿就不是一个Java类,因此在Java程序中只能打印出空值
9、数组类的Class对象,不是由类加载器去加载的,而是在Java运行期JVM根据需要自动创建的。
对于数组的类加载器来说,是通过Class.getClassLoader()返回的,与数组中元素类型的类加载器是一样的;
如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的(基本数据类型由虚拟机预先定义)
public class ClassLoaderDemo { public static void main(String[] args) { ClassLoader classloader1 = ClassLoader.getSystemClassLoader(); //sun.misc.Launcher$AppClassLoader@18b4aac2 System.out.println(classloader1); //获取到扩展类加载器 //sun.misc.Launcher$ExtClassLoader@424c0bc4 System.out.println(classloader1.getParent()); //获取到引导类加载器 null System.out.println(classloader1.getParent().getParent()); //获取系统的ClassLoader ClassLoader classloader2 = Thread.currentThread().getContextClassLoader(); //sun.misc.Launcher$AppClassLoader@18b4aac2 System.out.println(classloader2); String[]strArr=new String[10]; ClassLoader classLoader3 = strArr.getClass().getClassLoader(); //null,表示使用的是引导类加载器 System.out.println(classLoader3); ClassLoaderDemo[]refArr=new ClassLoaderDemo[10]; //sun.misc.Launcher$AppClassLoader@18b4aac2 System.out.println(refArr.getClass().getClassLoader()); int[]intArr=new int[10]; //null,如果数组的元素类型是基本数据类型,数组类是没有类加载器的 System.out.println(intArr.getClass().getClassLoader());
三、ClassLoader源码解析
① ClassLoader与现有类加载器的关系
1、ClassLoader是一个抽象类。如果我们给定了一个类的二进制名称,类加载器应尝试去定位或生成构成定义类的数据。
一种典型的策略是将给定的二进制名称转换为文件名,然后去文件系统中读取这个文件名所对应的class文件。
2、ClassLoader与现有加载器的关系
3、ExtClassLoader并没有重写loadClass()方法,这足矣说明其遵循双亲委派模式
4、AppClassLoader重载了loadClass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。
② 抽象类ClassLoader的主要方法(内部没有抽象方法)
1、public final ClassLoader getParent():返回该类加载器的超类加载器。
2、public Class<?> loadClass(String name) throws ClassNotFoundException
(加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回ClassNot FoundException 异常。该方法中的逻辑就是双亲委派模式的实现)
3、protected Class<?> findClass (String name) throws ClassNotFoundException
查找二进制名称为name的类,返回结果为java.lang.Class类的实例。
这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用。
4、protected final Class<?> defineClass(String name, byte[] b, int off, int len)
根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。
这是受保护的方法,只有在自定义ClassLoader子类中可以使用。
5、 protected final void resolveClass(Class<?> c)
链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。
前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。
6、源码解析Classloader方法
测试代码: ClassLoader.getSystemClassLoader().loadClass("com.xiaozhi.java.User"); //resolve==true,加载class的同时需要进行解析操作 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 { //parent==null 父类加载器是引导类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } // 当前类的加载器的父类加载器未加载此类 or 当前类的加载器未加载此类 if (c == null) { // 调用当前classloader的findClass long t1 = System.nanoTime(); 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; } }
③ SecureClassLoader与URLClassLoader
1、接着SecureClassLoader扩展了 ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,
一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联。
2、前面说过,ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。
而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。
在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,
这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁
④ Class.forName()与ClassLoader.loadClass()对比
1、Class.forName():是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个 Class 对象。
该方法在将 Class 文件加载到内存的同时,会执行类的初始化。如:Class.forName(“com.atguigu.java.HelloWorld”);
2、ClassLoader.loadClass():这是一个实例方法,需要一个 ClassLoader 对象来调用该方法。
该方法将 Class 文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。
该方法因为需要得到一个 ClassLoader 对象,所以可以根据需要指定使用哪个类加载器。
四、双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。
而且加载某个类的class文件时,java虚拟机采用的双亲委派机制,即把请求交由父类处理,他是一种任务委派模式。
1、工作原理
如果一个类加载器收到了类加载请求,它并不会自己先去加载, 而是把这个请求委托给父类的加载器去执行。
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层 的启动类加载器;
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
2、本质
规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。
3、源码分析
双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)接口中体现。该接口的逻辑如下
1、先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
2、判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name, false)接口进行加载
3、反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassOrNull(name)接口,让引导类加载器进行加载
4、如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用java.lan g.ClassLoader接口的defineClass系列的native接口加载目标Java类
5、双亲委派的模型就隐藏在这第2和第3步中
4、双亲委派机制的优势
避免类的重复加载,确保一个类的全局唯一性(当父ClassLoader已经加载了该类的时候,就没有必要子ClassLoader再加载一次)
保护程序安全,防止核心API被随笔篡改
5、双亲委托模式的弊端
检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,
但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类
6、结论
由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而已。
比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时
首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法
7、破坏双亲委派机制及举例
1、双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的“远古”时代
2、第二次破坏双亲委派机制:线程上下文类加载器(ClassLoader.getSystemClassLoader( ))
3、双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)
五、沙箱安全机制
1、如图,虽然我们自定义了一个java.lang包下的String尝试覆盖核心类库中的String,但是由于双亲委派机制,
启动加载器会加载java核心类库的String类(BootStrap启动类加载器只加载包名为java、javax、sun等开头的类),而核心类库中的String并没有main方法
2、自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加器在加载的过程中会先加载jdk自带的文件
(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码
的保护,这就是沙箱安全机制。
3、沙箱安全机制作用:保证程序安全、保护Java原生的JDK代码
六、其他机制
JDK1.6时期,当前最新的安全机制实现,则引入了域(Domain)的概念
虚拟机会把所有代码加载到不同的系统域和应用域。
系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。
虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示,最新的安全模型(jdk1.6)
七、其他
在JVM中表示两个class对象是否为同一类存在两个必要的条件:
类的完整类名必须一致,包括包名。
加载这个类的classLoader(指classLoader实例对象)必须相同
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的classLoader实例对象不同,
那么这两个类对象也是不相等的。
1、对类加载器的引用
JVM必须知道一个类型是由启动加载器的还是由用户类加载器加载的。如果一个类型是由用户类加载加载的,那么JVM会将这个类加载器的一个引用作为类型信息
的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
2、类的主动使用和被动使用
java程序对类的使用方式分为:主动使用和被动使用
主动使用,又分为七种情况:
创建类的实例、访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法、反射
初始化一个类的子类、Java虚拟器启动时被标明为启动类的类
jdk7开始提供的动态语言支持:
java.lang.invoke.MethodHandle实例的解析结果
REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用java类的方式都是被看作对类的被动使用,都不会导致类的初始化。
引用连接:https://blog.csdn.net/TZ845195485/article/details/116502297