Java - ClassLoader 类加载机制和重写类加载
1.ClassLoader
Java是依赖JVM实现的跨平台开发,程序运行前需要先编译class文件,
Java类初始化的时候会调用java.lang.Classloader来加载字节码,
然后ClasssLoader调用JVM的native方法来定义一个java.lang.Class实例。
2.Java类
public class TestHello { public String hello(){ return "hello,world!"; } }
这里编译成一个java文件
使用javap -c 命令反汇编class文件
JVM再执行我们的TestHello时候会先解析class的二进制内容,其实就是javap命令生成的字节码。
3.类加载方法
所有的java类都必须经过JVM加载后才能运行,ClassLoader主要作用就是用于Java类文件的加载。
在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)
、Extension ClassLoader(扩展类加载器)(接触较少)
、App ClassLoader(系统类加载器)(直接加载我们的代码)
,AppClassLoader
是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类。
可以这么说 用java.io.File.class.getClassLoader()取得是null的对象,就是用Bootstrap去加载的,以为它是用C++去实现的,所以当然得不到对应的对象了!
ClassLoader
类有如下核心方法:
-
loadClass
(加载指定的Java类) -
findClass
(查找指定的Java类) -
findLoadedClass
(查找JVM已经加载过的类) -
defineClass
(定义一个Java类) -
resolveClass
(链接指定的Java类)
4.Java动态加载方式
Java的加载方式有显式与隐式。
显式:Java反射或者ClassLoader来动态加载一个类对象。
隐式:类名.方法名()或者new()类的实例。
我们可以自定义类加载器去加载任意的类
// 反射加载TestHelloWorld示例 Class.forName("com.anbai.sec.classloader.TestHelloWorld"); // ClassLoader加载TestHelloWorld示例 this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");
5.重写classloader
可以通过重写classloader类来加载字节码(为一个不存在的类),加载到JVM里面去,然后通过反射去调用这个类来实例化他的对象调用他的方法。这里就以TestHelloWorld类为例,先注释掉之前写的TestHelloWorld,
完整重写代码
package com.anbai.sec.classloader; import java.lang.reflect.Method; /** * Creator: yz * Date: 2019/12/17 */ public class TestClassLoader extends ClassLoader { // TestHelloWorld类名 public static String TEST_CLASS_NAME = "com.anbai.sec.classloader.TestHelloWorld"; // TestHelloWorld类字节码 public static byte[] TEST_CLASS_BYTES = new byte[]{ -54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0, 16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47, 115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11, 0, 0, 0, 2, 0, 12 }; @Override public Class<?> findClass(String name) throws ClassNotFoundException { // 只处理TestHelloWorld类 if (name.equals(TEST_CLASS_NAME)) { // 调用JVM的native方法定义TestHelloWorld类 return defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length); } return super.findClass(name); } /** * 使用自定义类加载器加载TestHelloWorld类字节码并调用hello方法示例,等价于如下代码: * <p> * * </p> * * @param args */ public static void main(String[] args) { // 创建自定义的类加载器 TestClassLoader loader = new TestClassLoader(); try { // 使用自定义的类加载器加载TestHelloWorld类 Class testClass = loader.loadClass(TEST_CLASS_NAME); // 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld(); Object testInstance = testClass.newInstance(); // 反射获取hello方法 Method method = testInstance.getClass().getMethod("hello"); // 反射调用hello方法,等价于 String str = t.hello(); String str = (String) method.invoke(testInstance); System.out.println(str); } catch (Exception e) { e.printStackTrace(); } } }
首先继承ClassLoader 重写他的方法,这里定义了TEST_CLASS_NAME就是TestHelloWorld的包名到名字
看这个findClass,做一个判断,名字是否是我们所写的TestHelloWorld,然后return defineClass
是调用JVM的方法去定义TestHellowWorld类;
如果不是我们想创建的对象就
return super.findClass(name);
用super调用父类的findclass去创建。
这里代码的main方法就是这样的流程,注意看注释
创建自定义的类加载器loader对象-->用我们重写的类加载器去加载TestHelloWorld类
-->然后通过反射该类,获取对应对象-->然后反射调用对象来invoke调用到hello方法
-->最后通过hello方法的返回str也就是hello world 输出
/** * 使用自定义类加载器加载TestHelloWorld类字节码并调用hello方法示例,等价于如下代码: * <p> * * </p> * * @param args */ public static void main(String[] args) { // 创建自定义的类加载器 TestClassLoader loader = new TestClassLoader(); try { // 使用自定义的类加载器加载TestHelloWorld类 Class testClass = loader.loadClass(TEST_CLASS_NAME); // 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld(); Object testInstance = testClass.newInstance(); // 反射获取hello方法 Method method = testInstance.getClass().getMethod("hello"); // 反射调用hello方法,等价于 String str = t.hello(); String str = (String) method.invoke(testInstance); System.out.println(str); } catch (Exception e) { e.printStackTrace(); } }
ok来打断点走一遍整个调用流程首先是在加载我们的TestHelloWorld类
然后以为名字是if判断对应的名字,所以会进入到defineClass方法里面
defineClass是会返回一个对象的
我们用的是58行断点那里的testClass来接收这个返回值
这个返回值也就是TestHelloWorld的对象了,然后我们通过反射去调用这个对象
然后通过testInstance来反射获取到Hello方法
最后就是通过反射调用hello方法来执行,返回给str 输出了hello world:
刚开始理解起来确实有些难,慢慢来把,学习之路,慢就是快~
上一篇: 深入了解 Java 类加载器(ClassLoader)
下一篇: 19.类加载器说明
推荐阅读
-
双亲委托模型和 Flink 的类加载策略--父类优先的类加载策略
-
Java 类加载过程
-
Android 开发中 nodpi、xhdpi、hdpi、mdpi、ldpi 的概念 - 术语和概念 屏幕尺寸 屏幕的物理尺寸,基于屏幕的对角线长度(如 2.8 英寸、3.5 英寸)。 简而言之,安卓系统将所有屏幕尺寸简化为三大类:大、普通和小。 程序可以为这三种屏幕尺寸提供三种不同的布局选项,然后系统会以合适的方式将布局选项呈现到相应的屏幕上,这个过程不需要程序员用代码进行干预。 屏幕纵横比 屏幕的物理长度与物理宽度之比。程序只需使用系统提供的资源分类器 long(长)和 notlong(不长),就能为具有特定长宽比的屏幕提供配制材料。 分辨率 屏幕的像素总数。请注意,分辨率并不意味着长宽比,尽管在大多数情况下,分辨率表示为 "宽度 x 长度"。在安卓系统中,程序一般不直接处理分辨率。 密度 根据屏幕分辨率,沿屏幕宽度和长度排列的像素数量。 密度较低的屏幕在长度和宽度方向上的像素都相对较少,而密度较高的屏幕通常会在同一区域内排列很多甚至非常非常多的像素。屏幕的密度非常重要;例如,一个界面元素(如按钮)的长度和宽度以像素为单位,在低密度屏幕上会显得很大,但在高密度屏幕上就会显得很小。 独立于密度的像素(DIP)是指程序用来定义界面元素的抽象意义上的像素。它作为一个与实际密度无关的单位,帮助程序员构建布局方案(界面元素的宽度、高度和位置)。 与密度无关的像素在逻辑上与像素密度为 160 DPI 的屏幕上的像素大小相同,而 160 DPI 是安卓平台默认的显示设备。在运行时,平台会以目标屏幕的密度为基准,"透明 "地处理所有所需的 DIP 缩放操作。要将与密度无关的像素转换为屏幕像素,可以使用一个简单的公式:像素 = DIP * (密度 / 160)。例如,在 240 DPI 的屏幕上,1 个 DIP 等于 1.5 个物理像素。强烈建议使用 DIP 来定义程序界面的布局,因为这样可以确保用户界面在所有分辨率的屏幕上都能正常显示。 为了简化程序员在面对各种分辨率时的麻烦,也为了让各种分辨率的平台都能直接运行这些程序,Android 平台将所有屏幕以密度和分辨率作为分类方式,分别分为三类:- 三大尺寸:大、普通、小;- 三种不同密度:高(hdpi)、中(mdpi)和低(ldpi)。DPI 表示 "每英寸点数",即每英寸的像素数。如果需要,程序可以为不同的屏幕尺寸提供不同的资源(主要是布局),为不同的屏幕密度提供不同的资源(主要是位图)。除此之外,程序无需对屏幕尺寸或密度进行任何额外处理。执行时,平台会根据屏幕本身的尺寸和密度特性自动加载相应的资源,并将其从逻辑像素(DIP,用于定义界面布局)转换为屏幕上的物理像素。
-
JVM 类加载机制
-
Java 类加载机制详解
-
jvm 的 java 类加载机制和类加载器(ClassLoader)的详细信息
-
Java 类加载]自定义类加载器
-
JVM 注意事项 (I) Java 类加载过程?类加载器?
-
深入了解 Java 类加载
-
Java - ClassLoader 类加载机制和重写类加载