网易首页 > 网易号 > 正文 申请入驻

一文带你深扒ClassLoader内核,揭开它的神秘面纱!

0
分享至

  本文转载自 我没有三颗心脏 作者:我没有三颗心脏 id: wmyskxz

  「MoreThanJava」 宣扬的是 「学习,不止 CODE」

  如果觉得 「不错」 的朋友,欢迎 「关注 + 留言 + 分享」,文末有完整的获取链接,您的支持是我前进的最大的动力!

  前言

  ClassLoader 可以说是 Java 最为神秘的功能之一了,好像大家都知道怎么回事儿 ,又都说不清楚具体是怎么一回事 。

  今天,我们就来深度扒一扒,揭开它神秘的面纱!

  Part 1. 类加载是做什么的?

  首先,我们知道,Java 为了实现 「一次编译,到处运行」 的目标,采用了一种特别的方案:先 编译与任何具体及其环境及操作系统环境无关的中间代码(也就是 .class 字节码文件),然后交由各个平台特定的 Java 解释器(也就是 JVM)来负责 解释 运行。

  ClassLoader 就是那个把字节码交给 JVM 的搬运工 。它负责将 字节码形式 的 Class 转换成 JVM 中 内存形式 的 Class 对象。

  字节码可以是来自于磁盘上的 .class 文件,也可以是 jar 包里的 *.class,甚至是来自远程服务器提供的字节流。字节码的本质其实就是一个有特定复杂格式的字节数组 byte[]。

  另外,类加载器不光可以把 Class 加载到 JVM 之中并解析成 JVM 统一要求的对象格式,还有一个重要的作用就是 审查每个类应该由谁加载

  而且,这些 Java 类不会一次全部加载到内存,而是在应用程序需要时加载,这也是需要类加载器的地方。

  Part 2. ClassLoader 类结构分析

  以下就是 ClassLoader 的主要方法了:

  defineClass() 用于将 byte 字节流解析成 JVM 能够识别的 Class 对象。有了这个方法意味着我们不仅可以通过 .class 文件实例化对象,还可以通过其他方式实例化对象,例如通过网络接收到一个类的字节码。

  findClass() 通常和 defineClass() 一起使用,我们需要直接覆盖 ClassLoader 父类的 findClass() 方法来实现类的加载规则,从而取得要加载类的字节码。

  protectedClassfindClass(Stringname)throwsClassNotFoundException{thrownewClassNotFoundException(name);}

  如果你不想重新定义加载类的规则,也没有复杂的处理逻辑,只想在运行时能够加载自己制定的一个类,那么你可以用 this.getClass().getClassLoader().loadClass("class") 调用 ClassLoader 的 loadClass() 方法来获取这个类的 Class 对象,这个 loadClass() 还有重载方法,你同样可以决定再什么时候解析这个类。

  loadClass() 用于接受一个全类名,然后返回一个 Class 类型的对象。

  resolveClass() 用于对 Class 进行 链接,也就是把单一的 Class 加入到有继承关系的类树中。如果你想在类被加载到 JVM 中时就被链接(Link),那么可以在调用 defineClass() 之后紧接着调用一个 resolveClass() 方法,当然你也可以选择让 JVM 来解决什么时候才链接这个类(通常是真正被实实例化的时候)。

  ClassLoader 是个抽象类,它还有很多子类,如果我们要实现自己的 ClassLoader,一般都会继承 URLClassLoader 这个子类,因为这个类已经帮我们实现了大部分工作。

  例如,我们来看一下 java.net.URLClassLoader.findClass() 方法的实现:

  //入参为Class的binaryname,如java.lang.StringprotectedClassfindClass(finalStringname)throwsClassNotFoundException{//以上代码省略//通过binaryname生成包路径,如java.lang.String->java/lang/String.classStringpath=name.replace('.','/').concat(".class");//根据包路径,找到该Class的文件资源Resourceres=ucp.getResource(path,false);if(res!=null){try{//调用defineClass生成java.lang.Class对象returndefineClass(name,res);}catch(IOExceptione){thrownewClassNotFoundException(name,e);}}else{returnnull;}//以下代码省略}

  Part 3. Java 类加载流程详解

  以下就是 ClassLoader 加载一个 class 文件到 JVM 时需要经过的步骤。

  事实上,我们每一次在 IDEA 中点击运行时,IDE 都会默认替我们执行以下的命令:

  javac Xxxx.java 找到源文件中的 public class,再找 public class 引用的其他类,Java 编译器会根据每一个类生成一个字节码文件;

  java Xxxx 找到文件中的唯一主类 public class,并根据 public static 关键字找到跟主类关联可执行的 main 方法 ,开始执行。

  在真正的运行 main 方法之前,JVM 需要 加载、链接 以及 初始化 上述的 Xxxx 类。

  这一步是读取到类文件产生的二进制流(findClass()),并转换为特定的数据结构(defineClass()),初步校验 cafe babe 魔法数 、常量池、文件长度、是否有父类等,然后在 Java 中创建对应类的 java.lang.Class 实例,类中存储的各部分信息也需要对应放入 运行时数据区 中(例如静态变量、类信息等放入方法区)。

  以下是一个 Class 文件具有的基本结构的简单图示:

  如果对 Class 文件更多细节感兴趣的可以进一步阅读:https://juejin.im/post/6844904199617003528

  这里我们可能会有一个疑问,为什么 JVM 允许还没有进行验证、准备和解析的类信息放入方法区呢?

  答案是加载阶段和链接阶段的部分动作(比如一部分字节码文件格式验证动作)是 交叉进行 的,也就是说 加载阶段还没完成,链接阶段可能已经开始了。但这些夹杂在加载阶段的动作(验证文件格式等)仍然属于链接操作。

  Link 阶段包括验证、准备、解析三个步骤。下面我们来详细说说。

  验证是连接阶段的第一步,这一阶段的目的是 为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成 4 个阶段的检验动作:

  文件格式验证: 验证字节流是否符合 Class 文件格式的规范;例如:是否以 0xCAFEBABE 开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

  元数据验证: 对字节码描述的信息进行语义分析(注意:对比 javac 编译阶段的语义分析),以保证其描述的信息符合 Java 语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object 之外。

  字节码验证: 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  符号引用验证: 确保解析动作能正确执行。

  验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在 方法区 中分配。对于该阶段有以下几点需要注意:

  1 这时候进行内存分配的 仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。

  2 这里所设置的 初始值通常情况下是数据类型默认的零值(如 00Lnullfalse等),而不是被在 Java 代码中被显式地赋予的值。

  3 如果类字段的字段属性表中存在 ConstantValue 属性,即 同时被 final 和 static 修饰,那么在准备阶段变量 value 就会被初始化为 ConstValue 属性所指定的值。

  例如,假设这里有一个类变量 public static int value = 666;,在准备阶段时初始值是 0 而不是 666,在 初始化阶段 才会被真正赋值为 666

  假设是一个静态类变量 public static final int value = 666;,则再准备阶段 JVM 就已经赋值为 666 了。

  解析阶段是虚拟机将常量池内的 符号引用 替换为 直接引用 的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。

  符号引用 的作用是在编译的过程中,JVM 并不知道引用的具体地址,所以用符号引用进行代替,而在解析阶段将会将这个符号引用转换为真正的内存地址。

  直接引用 可以理解为指向 类、变量、方法 的指针,指向 实例 的指针和一个 间接定位 到对象的对象句柄。

  为了理解上面两种概念的区别,来看一个实际的例子吧:

  publicclassTester{publicstaticvoidmain(String[]args){Stringstr="关注【我没有三颗心脏】,关注更多精彩";System.out.println(str);}}

  我们先在该类同级目录下运行 javac Tester 编译成 .class 文件然后再利用 javap -verbose Tester 查看类的详细信息 :

  //上面是类的详细信息省略...{//.....publicstaticvoidmain(java.lang.String[]);descriptor:([Ljava/lang/String;)Vflags:(0x0009)ACC_PUBLIC,ACC_STATICCode:stack=2,locals=2,args_size=10:ldc#7//String关注【我没有三颗心脏】,关注更多精彩2:astore_13:getstatic#9//Fieldjava/lang/System.out:Ljava/io/PrintStream;6:aload_17:invokevirtual#15//Methodjava/io/PrintStream.println:(Ljava/lang/String;)V10:returnLineNumberTable:line4:0line5:3line6:10}SourceFile:"Tester.java"

  可以看到,上面定义的 str 变量在编译阶段会被解析称为 符号引用,符号引用的标志是 astore_,这里就是 astore_1

  store_1的含义是将操作数栈顶的 关注【我没有三颗心脏】,关注更多精彩 保存回索引为 1 的局部变量表中,此时访问变量 str 就会读取局部变量表索引值为 1 中的数据。所以局部变量 str 就是一个符号引用。

  再来看另外一个例子:

  publicclassTester{publicstaticvoidmain(String[]args){System.out.println("关注【我没有三颗心脏】,关注更多精彩");}}

  这一段代码反编译之后得到如下的代码:

  //上面是类的详细信息省略...{//......publicstaticvoidmain(java.lang.String[]);descriptor:([Ljava/lang/String;)Vflags:(0x0009)ACC_PUBLIC,ACC_STATICCode:stack=2,locals=1,args_size=10:getstatic#7//Fieldjava/lang/System.out:Ljava/io/PrintStream;3:ldc#13//String关注【我没有三颗心脏】,关注更多精彩5:invokevirtual#15//Methodjava/io/PrintStream.println:(Ljava/lang/String;)V8:returnLineNumberTable:line4:0line5:8}SourceFile:"Tester.java"

  我们可以看到这里直接使用了 ldc 指令将 关注【我没有三颗心脏】,关注更多精彩 推送到了栈,紧接着就是调用指令 invokevirtual,并没有将字符串存入局部变量表中,这里的字符串就是一个 直接引用

  初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。在 Java 中对类变量进行初始值设定有两种方式:

  1 声明类变量是指定初始值;

  2 使用静态代码块为类变量指定初始值;

  JVM 初始化步骤:

  1 假如这个类还没有被加载和连接,则程序先加载并连接该类

  2 假如该类的直接父类还没有被初始化,则先初始化其直接父类

  3 假如类中有初始化语句,则系统依次执行这些初始化语句

  类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下几种:

  创建类的实例,也就是 new 的方式

  访问某个类或接口的静态变量,或者对该静态变量赋值

  调用类的静态方法

  反射(如 Class.forName("com.wmyskxz.Tester")

  初始化某个类的子类,则其父类也会被初始化

  Java 虚拟机启动时被标明为启动类的类,直接使用 java.exe 命令来运行某个主类

  使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHanlde 实例最后的解析结果为 REF_getstaticREF_putstaticREF_invokeStaticREF_newInvokeSpecial 四种类型的方法句柄时,都需要先初始化该句柄对应的类

  接口中定义了 JDK 8 新加入的默认方法(default修饰符),实现类在初始化之前需要先初始化其接口

  Part 4. 深入理解双亲委派模型

  我们在上面已经了解了一个类是如何被加载进 JVM 的——依靠类加载器——在 Java 语言中自带有三个类加载器:

  Bootstrap ClassLoader 最顶层的加载类,主要加载 核心类库%JRE_HOME%lib 下的rt.jarresources.jarcharsets.jarclass 等。

  Extention ClassLoader 扩展的类加载器,加载目录 %JRE_HOME%libext 目录下的 jar 包和 class 文件。

  Appclass Loader 也称为 SystemAppClass 加载当前应用的 classpath 的所有类。

  我们可以通过一个简单的例子来简单了解 Java 中这些自带的类加载器:

  publicclassPrintClassLoader{publicstaticvoidmain(String[]args){printClassLoaders();}publicstaticvoidprintClassLoaders(){System.out.println("Classloaderofthisclass:"+PrintClassLoader.class.getClassLoader());System.out.println("ClassloaderofLogging:"+com.sun.javafx.util.Logging.class.getClassLoader());System.out.println("ClassloaderofArrayList:"+java.util.ArrayList.class.getClassLoader());}}

  上方程序打印输出如下:

  Classloaderofthisclass:sun.misc.Launcher$AppClassLoader@18b4aac2ClassloaderofLogging:sun.misc.Launcher$ExtClassLoader@60e53b93ClassloaderofArrayList:null

  如我们所见,这里分别对应三种不同类型的类加载器:AppClassLoader、ExtClassLoader 和 BootstrapClassLoader(显示为 null)。

  一个很好的问题是:Java 类是由 java.lang.ClassLoader 实例加载的,但类加载器本身也是类,那么谁来加载类加载器呢?

  我们假装不知道,先来跟着源码一步一步来看。

  在 JDK 源码 sun.misc.Launcher 中,蕴含了 Java 虚拟机的入口方法:

  publicclassLauncher{privatestaticLauncherlauncher=newLauncher();privatestaticStringbootClassPath=System.getProperty("sun.boot.class.path");publicstaticLaunchergetLauncher(){returnlauncher;}privateClassLoaderloader;publicLauncher(){//CreatetheextensionclassloaderClassLoaderextcl;try{extcl=ExtClassLoader.getExtClassLoader();}catch(IOExceptione){thrownewInternalError("Couldnotcreateextensionclassloader",e);}//Nowcreatetheclassloadertousetolaunchtheapplicationtry{loader=AppClassLoader.getAppClassLoader(extcl);}catch(IOExceptione){thrownewInternalError("Couldnotcreateapplicationclassloader",e);}//设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解Thread.currentThread().setContextClassLoader(loader);}/**Returnstheclassloaderusedtolaunchthemainapplication.*/publicClassLoadergetClassLoader(){returnloader;}/**Theclassloaderusedforloadinginstalledextensions.*/staticclassExtClassLoaderextendsURLClassLoader{}/***Theclassloaderusedforloadingfromjava.class.path.*runsinarestrictedsecuritycontext.*/staticclassAppClassLoaderextendsURLClassLoader{}}

  源码有精简,但是我们可以得到以下信息:

  1 Launcher 初始化了 ExtClassLoader 和 AppClassLoader。

  2 Launcher 没有看到 Bootstrap ClassLoader 的影子,但是有一个叫做 bootClassPath 的变量,大胆一猜就是 Bootstrap ClassLoader 加载的 jar 包的路径。

  3 ExtClassLoader 和 AppClassLoader 都继承自 URLClassLoader,进一步查看 ClassLoader 的继承树,传说中的双亲委派模型也并没有出现。

  4 注意以下代码:

  ClassLoaderextcl;extcl=ExtClassLoader.getExtClassLoader();loader=AppClassLoader.getAppClassLoader(extcl);

  分别跟踪查看到这两个 ClassLoader 初始化时的代码:

  //一直追踪到最顶层的ClassLoader定义,构造器的第二个参数标识了类加载器的父类privateClassLoader(Voidunused,ClassLoaderparent){this.parent=parent;//代码省略.....}//Ext设置自己的父类为nullpublicExtClassLoader(File[]var1)throwsIOException{super(getExtURLs(var1),(ClassLoader)null,Launcher.factory);SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);}//手动把Ext设置为App的parent(这里的var2是传进来的extc1)AppClassLoader(URL[]var1,ClassLoadervar2){super(var1,var2,Launcher.factory);this.ucp.initLookupCache(this);}

  由此,我们得到了这样一个类加载器的关系图:

  奇怪,为什么 ExtClassLoader 的 parent 明明是 null,我们却一般地认为 Bootstrap ClassLoader 才是 ExtClassLoader 的父加载器呢?

  答案的一部分就藏在 java.lang.ClassLoader.loadClass() 方法里面:(这也就是著名的「双亲委派模型」现场了)

  protectedClassloadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){//首先检查是否已经加载过了Classc=findLoadedClass(name);if(c==null){longt0=System.nanoTime();try{if(parent!=null){//父加载器不为空则调用父加载器的loadClass方法c=parent.loadClass(name,false);}else{//父加载器为空则调用BootstrapClassLoaderc=findBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){//ClassNotFoundExceptionthrownifclassnotfound//fromthenon-nullparentclassloader}if(c==null){//Ifstillnotfound,theninvokefindClassinorder//tofindtheclass.longt1=System.nanoTime();//父加载器没有找到,则调用findclassc=findClass(name);//thisisthedefiningclassloader;recordthestatssun.misc.PerfCounter.getParentDelegationTime().addTime(t1-t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if(resolve){//调用resolveClass()resolveClass(c);}returnc;}}

  代码逻辑很好地解释了双亲委派的原理。

  1 当前 ClassLoader 首先从 自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。(每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。)

  2 当前 ClassLoader 的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到 Bootstrap ClassLoader。(当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。)

  所以,答案的另一部分是因为最高一层的类加载器 Bootstrap 是通过 C/C++ 实现的,并不存在于 JVM 体系内 ,所以输出为 null

  OK,我们理解了为什么 ExtClassLoader 的父加载器为什么是表示为 null 的 Bootstrap 加载器,那我们 自己实现的 ClassLoader 父加载器应该是谁呢?

  观察一下 ClassLoader 的源码就知道了:

  protectedClassLoader(ClassLoaderparent){this(checkCreateClassLoader(),parent);}protectedClassLoader(){this(checkCreateClassLoader(),getSystemClassLoader());}

  类加载器的 parent 的赋值是在 ClassLoader 对象的构造方法中,它有两个情况:

  1 由外部类创建 ClassLoader 时直接指定一个 ClassLoader 为 parent

  2 由 getSystemClassLoader() 方法生成,也就是在 sun.misc.Laucher 通过 getClassLoader() 获取,也就是 AppClassLoader。直白的说,一个 ClassLoader 创建时如果没有指定 parent,那么它的 parent 默认就是 AppClassLoader。(建议去看一下源码)

  简单来说,主要是为了 安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String,同时也 避免了重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类,如果相互转型的话会抛 java.lang.ClassCaseException

  如果我们要实现自己的类加载器,不管你是直接实现抽象类 ClassLoader,还是继承 URLClassLoader 类,或者其他子类,它的父加载器都是 AppClassLoader。

  因为不管调用哪个父类构造器,创建的对象都必须最终调用 getSystemClassLoader() 作为父加载器 。而该方法最终获取到的正是 AppClassLoader 。

  这也就是我们熟知的最终的双亲委派模型了。

  Part 5. 实现自己的类加载器

  在学习了类加载器的实现机制之后,我们知道了双亲委派模型并非强制模型,用户可以自定义类加载器,在什么情况下需要自定义类加载器呢?

  1 隔离加载类。在某些框架内进行中间件与应用的模块隔离,把类加载器到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar 包不会影响到中间件运行时使用的 jar 包。

  2 修改类加载方式。类的加载模型并非强制,除了 Bootstrap 外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需的动态加载。

  3 扩展加载源。比如从数据库、网络,甚至是电视机顶盒进行加载。(下面我们会编写一个从网络加载类的例子)

  4 防止源码泄露。Java 代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。

  实现一个自定义的类加载器比较简单:继承 ClassLoader,重写 findClass() 方法,调用 defineClass() 方法,就差不多行了。

  我们先来编写一个测试用的类文件:

  publicclassTester{publicvoidsay(){System.out.println("关注【我没有三颗心脏】,解锁更多精彩!");}}

  在同级目录下执行 javac Tester.java 命令,并把编译后的 Tester.class 放到指定的目录下(我这边为了方便就放在桌面上啦 /Users/wmyskxz/Desktop

  我们编写自定义 ClassLoader 代码:

  importjava.io.ByteArrayOutputStream;importjava.io.File;importjava.io.FileInputStream;importjava.io.IOException;publicclassMyClassLoaderextendsClassLoader{privatefinalStringmLibPath;publicMyClassLoader(Stringpath){//TODOAuto-generatedconstructorstubmLibPath=path;}@OverrideprotectedClassfindClass(Stringname)throwsClassNotFoundException{//TODOAuto-generatedmethodstubStringfileName=getFileName(name);Filefile=newFile(mLibPath,fileName);try{FileInputStreamis=newFileInputStream(file);ByteArrayOutputStreambos=newByteArrayOutputStream();intlen=0;try{while((len=is.read())!=-1){bos.write(len);}}catch(IOExceptione){e.printStackTrace();}byte[]data=bos.toByteArray();is.close();bos.close();returndefineClass(name,data,0,data.length);}catch(IOExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}returnsuper.findClass(name);}//获取要加载的class文件名privateStringgetFileName(Stringname){//TODOAuto-generatedmethodstubintindex=name.lastIndexOf('.');if(index==-1){returnname+".class";}else{returnname.substring(index+1)+".class";}}}

  我们在 findClass() 方法中定义了查找 class 的方法,然后数据通过 defineClass() 生成了 Class 对象。

  我们需要删除刚才在项目目录创建的 Tester.java 和编译后的 Tester.class 文件来观察效果:

  importjava.lang.reflect.InvocationTargetException;importjava.lang.reflect.Method;publicclassClassLoaderTester{publicstaticvoidmain(String[]args){//创建自定义的ClassLoader对象MyClassLoadermyClassLoader=newMyClassLoader("/Users/wmyskxz/Desktop");try{//加载class文件Classc=myClassLoader.loadClass("Tester");if(c!=null){try{Objectobj=c.newInstance();Methodmethod=c.getDeclaredMethod("say",null);//通过反射调用Test类的say方法method.invoke(obj,null);}catch(InstantiationException|IllegalAccessException|NoSuchMethodException|SecurityException|IllegalArgumentException|InvocationTargetExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}}}catch(ClassNotFoundExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}}}

  运行测试,正常输出:

  关注【我没有三颗心脏】,解锁更多精彩!

  突破了 JDK 系统内置加载路径的限制之后,我们就可以编写自定义的 ClassLoader。你完全可以按照自己的意愿进行业务的定制,将 ClassLoader 玩出花样来。

  例如,一个加密解密的类加载器。(不涉及完整代码,我们可以来说一下思路和关键代码)

  首先,在编译之后的字节码文件中动一动手脚,例如,给文件每一个 byte 异或一个数字 2:(这就算是模拟加密过程)

  Filefile=newFile(path);try{FileInputStreamfis=newFileInputStream(file);FileOutputStreamfos=newFileOutputStream(path+"en");intb=0;intb1=0;try{while((b=fis.read())!=-1){//每一个byte异或一个数字2fos.write(b^2);}fos.close();fis.close();}catch(IOExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}}catch(FileNotFoundExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}

  然后我们再在 findClass() 中自己解密:

  Filefile=newFile(mLibPath,fileName);try{FileInputStreamis=newFileInputStream(file);ByteArrayOutputStreambos=newByteArrayOutputStream();intlen=0;byteb=0;try{while((len=is.read())!=-1){//将数据异或一个数字2进行解密b=(byte)(len^2);bos.write(b);}}catch(IOExceptione){e.printStackTrace();}byte[]data=bos.toByteArray();is.close();bos.close();returndefineClass(name,data,0,data.length);}catch(IOExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}

  其实非常类似,也不做过多讲解,直接上代码:

  importjava.io.ByteArrayOutputStream;importjava.io.InputStream;importjava.net.URL;publicclassNetworkClassLoaderextendsClassLoader{privateStringrootUrl;publicNetworkClassLoader(StringrootUrl){//指定URLthis.rootUrl=rootUrl;}//获取类的字节码@OverrideprotectedClassfindClass(Stringname)throwsClassNotFoundException{byte[]classData=getClassData(name);if(classData==null){thrownewClassNotFoundException();}else{returndefineClass(name,classData,0,classData.length);}}privatebyte[]getClassData(StringclassName){//从网络上读取的类的字节Stringpath=classNameToPath(className);try{URLurl=newURL(path);InputStreamins=url.openStream();ByteArrayOutputStreambaos=newByteArrayOutputStream();intbufferSize=4096;byte[]buffer=newbyte[bufferSize];intbytesNumRead=0;//读取类文件的字节while((bytesNumRead=ins.read(buffer))!=-1){baos.write(buffer,0,bytesNumRead);}returnbaos.toByteArray();}catch(Exceptione){e.printStackTrace();}returnnull;}privateStringclassNameToPath(StringclassName){//得到类文件的URLreturnrootUrl+"/"+className.replace('.','/')+".class";}}

  Part 6. 必要的扩展阅读

  学习到这里,我们对 ClassLoader 已经不再陌生了,但是仍然有一些必要的知识点需要去掌握 ,希望您能认真阅读以下的材料:

  1 能不能自己写一个类叫 java.lang.System 或者 java.lang.String? - https://blog.csdn.net/tang9140/article/details/42738433

  2 深入理解 Java 之 JVM 启动流程 - https://cloud.tencent.com/developer/article/1038435

  3 真正理解线程上下文类加载器(多案例分析) - https://blog.csdn.net/yangcheng33/article/details/52631940

  4 曹工杂谈:Java 类加载器还会死锁?这是什么情况? - https://www.cnblogs.com/grey-wolf/p/11378747.html#_label2

  5 谨防JDK8重复类定义造成的内存泄漏 - https://segmentfault.com/a/1190000022837543

  7 Tomcat 类加载器的实现 - https://juejin.im/post/6844903945496690695

  8 Spring 中的类加载机制 - https://www.shuzhiduo.com/A/gVdnwgAlzW/

  参考资料

  《深入分析 Java Web 技术内幕》 | 许令波 著

  Java 类加载机制分析 - https://www.jianshu.com/p/3615403c7c84

  Class 文件解析实战 - https://juejin.im/post/6844904199617003528

  图文兼备看懂类加载机制的各个阶段,就差你了!- https://juejin.im/post/6844904119258316814

  Java面试知识点解析(三)——JVM篇 - https://www.wmyskxz.com/2018/05/16/java-mian-shi-zhi-shi-dian-jie-xi-san-jvm-pian/

  一看你就懂,超详细Java中的ClassLoader详解 - https://blog.csdn.net/briblue/article/details/54973413

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
反卡美国脖子!仅中国掌控的尖端设备不租不卖,美国三次求购遭拒

反卡美国脖子!仅中国掌控的尖端设备不租不卖,美国三次求购遭拒

搞笑的阿票
2024-04-25 17:46:16
杭州临安清凉峰女子与朋友爬山失联后续:人已找到,网友提醒丈夫

杭州临安清凉峰女子与朋友爬山失联后续:人已找到,网友提醒丈夫

宝哥精彩赛事
2024-04-25 12:13:12
朱芳雨回应周琦被重罚:完全尊重处罚决定 全力以赴拿下G4晋级

朱芳雨回应周琦被重罚:完全尊重处罚决定 全力以赴拿下G4晋级

醉卧浮生
2024-04-25 17:14:16
中方绝不许缅甸变天!大批解放军已兵临边境,中国这次要来硬的了

中方绝不许缅甸变天!大批解放军已兵临边境,中国这次要来硬的了

小lu侃侃而谈
2024-04-23 16:21:34
60岁女人再过夫妻生活,是怎么样的感受,几位女人说出了真心话!

60岁女人再过夫妻生活,是怎么样的感受,几位女人说出了真心话!

小阿眭说说吖
2024-04-15 15:28:32
杨幂每次说完话嘴唇收回就是瘪嘴的状态,很出戏

杨幂每次说完话嘴唇收回就是瘪嘴的状态,很出戏

阿芒娱乐说
2024-04-24 07:34:35
威廉和凯特封面合照新鲜出炉! 脸瘦脖子粗, 传递王妃新的健康信息

威廉和凯特封面合照新鲜出炉! 脸瘦脖子粗, 传递王妃新的健康信息

子稚说体育
2024-04-25 03:17:58
许玮甯和曾之乔电梯合照

许玮甯和曾之乔电梯合照

娱乐圈酸柠檬
2024-04-25 09:46:32
难说再见,现役四位巨星已进入生涯倒计时,保罗老詹上榜

难说再见,现役四位巨星已进入生涯倒计时,保罗老詹上榜

篮球圈里的那些事
2024-04-25 17:49:40
咖啡厅旁 留有余香

咖啡厅旁 留有余香

普陀动物世界
2024-04-25 01:14:38
他曾是省会城市最年轻一把手,今担任云南省委常委,仪表堂堂

他曾是省会城市最年轻一把手,今担任云南省委常委,仪表堂堂

历史留尘
2024-04-25 09:54:38
凌晨4点30!中国女排揭幕战PK韩国,朱婷立军令状,CCTV

凌晨4点30!中国女排揭幕战PK韩国,朱婷立军令状,CCTV

元爸体育
2024-04-25 11:49:23
河南“双面书记”落马:卖官鬻爵疯狂敛财,曾令数百人无家可归

河南“双面书记”落马:卖官鬻爵疯狂敛财,曾令数百人无家可归

天闻地知
2024-04-25 09:43:26
女子深夜用长勺从垃圾桶捞地沟油,官方回应:已受理 正调查

女子深夜用长勺从垃圾桶捞地沟油,官方回应:已受理 正调查

极目新闻
2024-04-25 14:19:31
大S正式开始工作了!没说具体接什么工作,据说很多代言广告找她

大S正式开始工作了!没说具体接什么工作,据说很多代言广告找她

元气少女侃娱乐
2024-04-25 15:07:41
大学的女生公共浴室发生过什么颠覆三观的事?看看网友怎么说的吧

大学的女生公共浴室发生过什么颠覆三观的事?看看网友怎么说的吧

蛙斯基娱乐中
2024-04-24 21:07:35
产后没母乳,婆婆推荐我去按摩,技师竟然是个黑人

产后没母乳,婆婆推荐我去按摩,技师竟然是个黑人

温酒与茶
2024-04-03 09:33:43
湖人消息:老詹霸气表态,两将取消复出,G3出场更新

湖人消息:老詹霸气表态,两将取消复出,G3出场更新

冷月小风风
2024-04-25 10:23:24
河南女子相亲1米8帅气小伙,一眼沦陷,女子:不要彩礼都愿嫁

河南女子相亲1米8帅气小伙,一眼沦陷,女子:不要彩礼都愿嫁

佑宛故事汇
2024-04-24 14:55:26
别轻易进厂?广西一00后打暑假工,被大8岁男生追到手,还怀孕了

别轻易进厂?广西一00后打暑假工,被大8岁男生追到手,还怀孕了

唐小糖说情感
2024-04-25 10:15:05
2024-04-25 18:48:50
杨建荣的数据库笔记
杨建荣的数据库笔记
专注于数据库和开源技术
412文章数 1863关注度
往期回顾 全部

科技要闻

北京车展,被穿红衣服的他们占领

头条要闻

俄副防长被抓:进国防部3年升副部长 系绍伊古"老搭档"

头条要闻

俄副防长被抓:进国防部3年升副部长 系绍伊古"老搭档"

体育要闻

当胜利变成意外,就不要再提未来……

娱乐要闻

心疼!伊能静曝儿子曾被狗仔追到洗手间

财经要闻

曙光已现?瑞银开始转而看好中国地产业

汽车要闻

全新哈弗H9亮相 大号方盒子硬派SUV入列

态度原创

游戏
时尚
手机
艺术
本地

《庄园领主》IGN7分:或能复制《博德3》的成功之路

我该怎么办?总是觉得渣男很有魅力!

手机要闻

联想旗下来酷X1 5G手机上架 搭载6nm国产芯 1599元起

艺术要闻

艺术名画︱爱尔兰画家大卫·科因的刀画作品

本地新闻

云游中国|苗族蜡染:九黎城的“潮”文化

无障碍浏览 进入关怀版