作一个积极的人编码、改bug、提高本身java
我有一个乐园,面向编程,春暖花开!编程
推荐阅读segmentfault
第一季
0、Java的线程安全、单例模式、JVM内存结构等知识梳理
一、Java内存管理-程序运行过程(一)
二、Java内存管理-初始JVM和JVM启动流程(二)
三、Java内存管理-JVM内存模型以及JDK7和JDK8内存模型对比总结(三)
四、Java内存管理-掌握虚拟机类加载机制(四)
五、Java内存管理-掌握虚拟机类加载器(五)
六、Java内存管理-类加载器的核心源码和设计模式(六)
七、Java内存管理-掌握自定义类加载器的实现(七)
第一季总结:由浅入深JAVA内存管理 Core Storywindows第二季
八、Java内存管理-愚人节new一个对象送给你(八)
【福利】JVM系列学习资源无套路赠送
九、Java内存管理-”一文掌握虚拟机建立对象的秘密”(九)
十、Java内存管理-你真的理解Java中的数据类型吗(十)
十一、Java内存管理-Stackoverflow问答-Java是传值仍是传引用?(十一)
十二、Java内存管理-探索Java中字符串String(十二)设计模式实战
分享一位老师的人工智能教程。零基础!通俗易懂!风趣幽默!
你们能够看看是否对本身有帮助,点击这里查看【人工智能教程】。接下来进入正文。安全
<font color='blue'>勿在流沙筑高台,出来混早晚要还的。</font>服务器
上一篇分析了ClassLoader的类加载相关的核心源码,也简单介绍了ClassLoader的设计思想,读源码相对来讲是比较枯燥的,仍是这个是必需要走的过程,学习源码中的一些思想,一些精髓,看一下大神级人物是怎么写出那么牛逼的代码。咱们可以从中学到一点点东西,那也是一种进步和成长了。本文基于上一篇文章内容,手把手写一个自定义类加载器,而且经过一些简单的案例(场景)让咱们更加细致和静距离的体验类加载器的神奇之处。网络
<font color='red'>本文地图:</font>app
上一篇介绍了ClassLoader中loadClass()
内的一些源码,也介绍了一些核心的API,其中有一个getParent()
是没有作说明的,这里简单说明一下,方便快速理解后续的内容。
// 返回委托的父类加载器。 ClassLoader getParent()
这个方法是获取父类加载器,那么父类加载器是怎么初始化的。上一文也提到了,虽然类加载器的加载模式为双亲委派模型,可是真正在实现上并非使用继承方式。
看下面源码,sun.misc.Launcher
类是java的入口,在启动java应用的时候会首先建立Launcher类,建立Launcher类的时候回准备应用程序运行中须要的类加载器。
/** * 删除了一些其余代码,方便阅读 **/ public class Launcher { // 可自行打印一下 bootClassPath ,看输入内容是什么? private static String bootClassPath = System.getProperty("sun.boot.class.path");// ① private static Launcher launcher = new Launcher(); // ② private ClassLoader loader; public Launcher() { Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } Thread.currentThread().setContextClassLoader(this.loader);// ③ // 省略其余代码.... } }
第一:Launcher做为JAVA应用的入口,根据咱们以前所学的双亲委派模型,Laucher是由JVM建立的,它类加载器应该是BootStrapClassLoader
, 这是一个C++编写的类加载器,是Java应用体系中最顶层的类加载器,负责加载JVM须要的一些类库,classpath配置 (%JAVA_HOME%/jre/lib
)。下面经过简单例子进行说明:
public class TestClassLoader { public static void main(String[] args) { // 能够获取是哪一个类加载器加载 Launcher 这个类 ClassLoader classLoader = Launcher.class.getClassLoader(); System.out.println("classLoader : " + classLoader); System.out.println("-------------------"); String bootClassPath = System.getProperty("sun.boot.class.path"); String[] split = bootClassPath.split(";"); for (int i = 0; i < split.length; i++) { System.out.println(split[i]); } }
输出的结果,我本地机器上路径:
classLoader : null ------------------- C:\Program Files\Java\jdk1.8.0_121\jre\lib\resources.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\sunrsasign.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\jsse.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\jce.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\charsets.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\jfr.jar C:\Program Files\Java\jdk1.8.0_121\jre\classes
第二:当初始化Launcher
类的时候,遇到关键字new 进行初始化,调用构造方法。先获取到ExtClassLoader
类加载器,而后在获取AppClassLoader
类加载器器,而后设置ExtClassLoader
作为它父类加载器。具体设置代码以下:
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { //删掉其余代码... return new Launcher.AppClassLoader(var1x, var0); } AppClassLoader(URL[] var1, ClassLoader var2) { // 看 super的实现 ,var2 就是 ExtClassLoader super(var1, var2, Launcher.factory); this.ucp.initLookupCache(this); } /** * 在AppClassLoader的父类中,java.net.URLClassLoader#URLClassLoader * super(parent); 设置父类加载器,最后能够经过 getParent() 获取父类加载器 */ public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { super(parent); // 这里就不在继续往上跟踪了,在往上代码相对简单,可自行查阅 SecurityManager security = System.getSecurityManager(); //删掉其余代码... }
第三:设置上下文类加载器的思考?
在Java中为何须要上下文类加载器呢,这个就是一个很是有意思的问题。 Java中有两种方式获取类加载器:
第一种:一个类被加载的时候使用哪一个类加载器来加载,也就是类.class.getClassLoader()
或者对象.getClass().getClassLoader()
。
String str = new String(); ClassLoader classLoader1 = String.class.getClassLoader(); ClassLoader classLoader2 = str.getClass().getClassLoader(); System.out.println("String loader1 : " + classLoader1); System.out.println("String loader2 : " + classLoader2); -----输出为null,启动类加载器进行加载-------- String loader1 : null String loader2 : null
第二种:经过Thread的上限文获取类加载器。为何要经过上下文加载呢?
虽然咱们都知道Java类加载的双亲委派模型,在加载一个类的时候,会优先委派给父类加载器,这样保证不会出现类被重复加载,也保证了Java一些基础类(如String类)能够稳定的存在,不会被用户自定义类顶替掉。
可是双亲委派模型并非完美的,在一些场景下会出现一些比较难解决的问题,举个例子,在使用SPI的时候,java.util.ServiceLoader
是经过BootStrapClassLoader
类加载器加载的,在执行到加载用户编写的扩展类的时候,若是使用当前类的类加载器,是确定没法加载到用户编写的类的,这个时候就没法继续执行了,因此这个时候就须要使用Thread的上下文类加载器,查看源码的时候咱们就发现,在用户不主动传递ClassLoader
的时候,会获取当前上下文类加载器,这样应用程序才能正常的执行。
public static <S> ServiceLoader<S> load(Class<S> var0) { ClassLoader var1 = Thread.currentThread().getContextClassLoader(); return load(var0, var1); }
<font color='blue'>小总结:</font>
上面的内容是在上一篇的基础上继续的扩展,若是对尚未看过上一篇内容,请先阅读上一篇内容后,在来看这段内容。整个ClassLoader源码的的部分就分析这么多了,后面在自定义类加载器中有遇到须要分析源码的地方,仍是会继续进行说明和讲解。
前面的两篇文章一直在为自定义加载器作铺垫,本文终于来引来这个神秘的嘉宾了,下面就咱们用"热恋"的掌声欢迎它的出场(活跃一下气氛,由于刚才看源码太安静了)!
首先看一下JDK API中如何教咱们实现从现从网络加载类的类加载器的简单示例。看下面代码:
例如,应用程序能够建立一个网络类加载器,从服务器中下载类文件。示例代码以下所示: ClassLoader loader = new NetworkClassLoader(host, port); Object main = loader.loadClass("Main", true).newInstance(); . . . 网络类加载器子类必须定义方法 findClass 和 loadClassData,以实现从网络加载类。下载组成该类的字节后,它应该使用方法 defineClass 来建立类实例。示例实现以下: class NetworkClassLoader extends ClassLoader { String host; int port; public Class findClass(String name) { byte[] b = loadClassData(name); return defineClass(name, b, 0, b.length); } private byte[] loadClassData(String name) { // load the class data from the connection . . . } }
下面就“手把手”一步步教你如何实现一个自定义类加载器!
第一步: 新建一个MyClassLoader
继承 ClassLoader
public class MyClassLoader extends ClassLoader {}
第二步:添加自定义的类加载器属性和 构造函数
public class MyClassLoader extends ClassLoader { /** * 类加载器名称 */ private String name; /** * 自定义加载路径 */ private String path; /** * 自定义加载文件后缀类型 */ private final String fileType = ".class"; public MyClassLoader(String name,String path){ //让系统类加载器(AppClassLoader)成为该类加载器的父类加载器 super(); this.name = name; this.path = path; } public MyClassLoader(ClassLoader parent,String name,String path){ //显示指定该类的父类加载器 super(parent); this.name = name; this.path = path; } @Override public String toString() { return this.name; } }
第三步:重写findClass
方法,自定义咱们本身的查询类的方式,而后经过defineClass
方法将一个 byte 数组转换为 Class 类的实例。
/** * 加载咱们本身定义的类,经过咱们本身定义的类加载器 * @param name 二进制的文件名称 * @return Class实例对象 * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //获取class文件的字节数组 byte[] resultData = this.loadByteClassData(name); return super.defineClass(name, resultData, 0, resultData.length); } /** * 加载指定路径下面的class文件的字节数组 * @param name 二进制文件名称 ,例如:com.learn.classloader.Demo * @return 二进制字节数组 */ private byte[] loadByteClassData(String name) { byte[] classData = null; InputStream in = null; ByteArrayOutputStream os = null; try { // 好比 有包名 二进制文件名:com.learn.classloader.Demo // 转换为本地路径 com/learnclassloader/Demo.class name = this.path + name.replaceAll("\\.", "/") + fileType; File file = new File(name); os = new ByteArrayOutputStream(); in = new FileInputStream(file); int tmp = 0; while ((tmp = in.read()) != -1){ os.write(tmp); } // 文件流转为二进制字节流 classData = os.toByteArray(); }catch (Exception e){ e.printStackTrace(); }finally { try { // 关闭流 if(in != null){ in.close(); } if(os != null){ os.close(); } }catch (Exception e){ e.printStackTrace(); } } return classData; }
上面三步整合在一块儿,就实现了一个简单的类加载! 查看自定义类加载器的所有代码。
/** * 自定义类加载器 * * @author:dufyun * @version:1.0.0 * @date 2019/3/28 */ public class MyClassLoader extends ClassLoader { /** * 类加载器名称 */ private String name; /** * 自定义加载路径 */ private String path; /** * 自定义加载文件后缀类型 */ private final String fileType = ".class"; public MyClassLoader(String name,String path){ //让系统类加载器(AppClassLoader)成为该类加载器的父类加载器 super(); this.name = name; this.path = path; } public MyClassLoader(ClassLoader parent,String name,String path){ //显示指定该类的父类加载器 super(parent); this.name = name; this.path = path; } /** * 加载咱们本身定义的类,经过咱们本身定义的类加载器 * @param name 二进制的文件名称 * @return Class实例对象 * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //获取class文件的字节数组 byte[] resultData = this.loadByteClassData(name); return super.defineClass(name, resultData, 0, resultData.length); } /** * 加载指定路径下面的class文件的字节数组 * @param name 二进制文件名称 ,例如:com.learn.classloader.Demo * @return 二进制字节数组 */ private byte[] loadByteClassData(String name) { byte[] classData = null; InputStream in = null; ByteArrayOutputStream os = null; try { // 好比 有包名 二进制文件名:com.learn.classloader.Demo // 转换为本地路径 com/learn/classloader/Demo.class name = this.path + name.replaceAll("\\.", "/") + fileType; File file = new File(name); os = new ByteArrayOutputStream(); in = new FileInputStream(file); int tmp = 0; while ((tmp = in.read()) != -1){ os.write(tmp); } // 文件流转为二进制字节流 classData = os.toByteArray(); }catch (Exception e){ e.printStackTrace(); }finally { try { // 关闭流 if(in != null){ in.close(); } if(os != null){ os.close(); } }catch (Exception e){ e.printStackTrace(); } } return classData; } @Override public String toString() { return this.name; } }
第五: 简单的测试自定义类加载器!
MyClassLoader myClassLoaderA = new MyClassLoader("aflyun", "F:\\tmp\\"); System.out.println("myClassLoaderA :" + myClassLoaderA.getParent().getClass().getName()); // 设置父类加载器 为null ,启动类加载器 MyClassLoader myClassLoaderB = new MyClassLoader(null, "aflyun", "F:\\tmp\\"); System.out.println("myClassLoaderB :" + myClassLoaderB.getParent()); ------------ myClassLoaderA :sun.misc.Launcher$AppClassLoader myClassLoaderB :null
看到这里,你若是对本篇第一小节理解的话,这里确定会好奇,MyClassLoader
是怎么设置AppClassLoader
为父类加载器的,在代码中只写了一个 super();
系统类加载器(AppClassLoader)成为该类加载器的父类加载器。仍是看源码 ,使用super();
在初始化的时候调用CLassLoader
的构造函数,在此构造函数中有一个getSystemClassLoader()
获取的就是 AppClassLoader
。
protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); }
咱们在看一下 getSystemClassLoader()
的实现,其中有一个initSystemClassLoader()
方法获取到Launcher
初始化加载的ClassLoader
,而后将此ClassLoader
赋值给 ClassLoader
类 中的 scl
!具体源码以下:
// The class loader for the system private static ClassLoader scl; public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); // 获取系统初始化的ClassLoader if (scl == null) { return null; } SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; } private static synchronized void initSystemClassLoader() { if (!sclSet) { // 省略其余代码..... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); if (l != null) { Throwable oops = null; //从Launcher 中获取ClassLoader也就是 AppClassLoader scl = l.getClassLoader(); // 省略其余代码..... } sclSet = true; } }
最后 this(checkCreateClassLoader(), getSystemClassLoader());
调用了ClassLoader
的另外一个构造函数,具体看下面源码,比较简单,就不作太多说明了。
// 这段代码上一篇文章就介绍过 private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; // 将获取的系统类加载器做为父类加载器,经过getParent()获取! if (ParallelLoaders.isRegistered(this.getClass())) { parallelLockMap = new ConcurrentHashMap<>(); // 省略其余代码..... } else { // no finer-grained lock; lock on the classloader instance parallelLockMap = null; // 省略其余代码..... } } /** * 获取父类加载器 */ public final ClassLoader getParent() { if (parent == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(parent, Reflection.getCallerClass()); } return parent; }
<font color='blue'>一张图在此说明:当前的类加载器就是自定义类加载器MyClassLoader
!</font>
MyClassLoader myClassLoaderA = new MyClassLoader("aflyun", "F:\\tmp\\");
<font color='blue'>tips1 :自定义类加载说明</font>
在实现自定义类加载器过程当中能够重写findClass
也能够重写loadClass
,但通常建议重写findClass
!
<font color='blue'>tips2 :自定义类加载器设置了加载路径path,其实以前介绍过的类加载器也有对应的加载路径。</font>
// BootStrapClassLoader String bootClassPath = System.getProperty("sun.boot.class.path"); // ExtClassLoader String var0 = System.getProperty("java.ext.dirs"); // AppClassLoader String var1 = System.getProperty("java.class.path");
F:\tmp\
下(无包名)一个工程中(有包名)F:盘中代码
public class HelloWorld { public HelloWorld() { System.out.println("--F: --HelloWorld--" + this.getClass().getClassLoader()); } }
工程中代码
:
package com.learn.classloader; public class HelloWorld { public HelloWorld() { System.out.println("--IDEA --HelloWorld--" + this.getClass().getClassLoader()); } }
执行类加载代码
:
MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/"); Class<?> cls = null; try { //加载类文件 cls = myClassLoader.loadClass("HelloWorld"); cls.newInstance();//实例化类。调用构造方法 } catch (Exception e) { e.printStackTrace(); }
思考一下打印的结果是什么呢?
答案:--F: --HelloWorld--myClassLoader
缘由分析:HelloWorld
类的class文件只有在F:/tmp/
下存在,因此就加载的是F盘下的HelloWorld
类文件!
F:\tmp\
,一个在工程中F:盘中代码
package com.learn.classloader; public class HelloWorld { public HelloWorld() { System.out.println("--F:com.learn.classloader --HelloWorld--" + this.getClass().getClassLoader()); } }
工程中的代码
:
package com.learn.classloader; public class HelloWorld { public HelloWorld() { System.out.println("--IDEA --HelloWorld--" + this.getClass().getClassLoader()); } }
执行类加载代码
:
MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/"); Class<?> cls = null; try { cls = myClassLoader.loadClass("com.learn.classloader.HelloWorld"); cls.newInstance(); } catch (Exception e) { e.printStackTrace(); }
思考一下打印的结果是什么呢?
答案:--IDEA --HelloWorld--sun.misc.Launcher$AppClassLoader@18b4aac2
缘由分析:F盘中HelloWorld
和工程中的HelloWorld
具备同样的包名,而且MyClassLoader
没有设置父类加载器,那么默认的父类加载类就是AppClassLoader
,根据以前所学的双亲委派模型,HelloWorld
类文件会首先被父类加载器加载,也就是被AppClassLoader
加载,只要父类加载器加载成功,子类加载器就不会在进行加载!
F: 盘 MyHelloWorld
package com.learn.classloader; public class MyHelloWorld { public MyHelloWorld() { System.out.println("--F:com.learn.classloader --MyHelloWorld--" + this.getClass().getClassLoader()); } }
D:盘 MyHelloWorld
package com.learn.classloader; public class MyHelloWorld { public MyHelloWorld() { System.out.println("--D:com.learn.classloader --MyHelloWorld--" + this.getClass().getClassLoader()); } }
此时新增一个myClassLoaderB,加载MyHelloWorld
!
加载的代码以下:
MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/"); MyClassLoader myClassLoaderB = new MyClassLoader(myClassLoader,"myClassLoader","D:/tmp/"); Class<?> cls = null; try { cls = myClassLoaderB.loadClass("com.learn.classloader.MyHelloWorld"); cls.newInstance(); } catch (Exception e) { e.printStackTrace(); }
思考一下打印的结果是什么呢?
答案:--F:com.learn.classloader --MyHelloWorld--myClassLoader
缘由分析:myClassLoaderB
父类加载器是myClassLoader
,此时在加载 MyHelloWorld
,首先会被父类加载器myClassLoader
加载!
代码示例和示例3同样!只是修改类加载器!代码以下:
MyClassLoader myClassLoaderC = new MyClassLoader(null,"myClassLoaderC","D:/tmp/"); Class<?> cls = null; try { cls = myClassLoaderC.loadClass("com.learn.classloader.MyHelloWorld"); cls.newInstance(); } catch (Exception e) { e.printStackTrace(); }
思考一下打印的结果是什么呢?
答案:--D:com.learn.classloader --MyHelloWorld--myClassLoaderC
缘由分析:myClassLoaderC
父类加载器是启动类加载器
,在启动类加载器
中找不到MyHelloWorld
,转一圈回来仍是须要myClassLoaderC
本身去加载!
本文实现了一个自定义的类加载器,而且经过简单的案例进行讲解和说明,让咱们更加深刻的了解类加载器的双亲委派模式和实现原理。
对类加载器有了比较深刻的学习和思考以后,会对咱们之后写Java代码会有必定帮助,而且在遇到一些Java的异常如ClassNotFoundException
可以快速知道缘由。 其实类加载的知识还有不少,在这里先抛出两个问题:
<font color='blue'>问题一、Java热部署如何实现 ? </font>修改一个Java文件后,不须要启动服务,就能够动态生效! (目前的主流开发工具都支持,如IDEA 在windows下 Ctrl+Shirt+F9,动态编译,动态加载!或者 SpringBoot经过配置devtools实现热部署的原理是什么?)
本质上是更新clas文件内容! 不须要从新启动服务!
<font color='blue'>问题二、以前一直提的类加载模式: 双亲模式模式!可是在Tomcat中你知道 WebappClassLoader 的加载机制吗?</font>
说明:WebappClassLoader 会先加载本身的Class ,找不到在委托给parent,破坏双亲委派模式!
后续有时间会去整理,若是你对这两个问题感兴趣,也欢迎在文末留言,一块儿探讨!
《JDK API 文档》
《深刻理解Java虚拟机》
<font color='red'>备注: 因为本人能力有限,文中如有错误之处,欢迎指正。</font>
谢谢你的阅读,若是您以为这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你天天开心愉快!
<center><font color='red'>Java编程技术乐园</font>:一个分享编程知识的公众号。跟着老司机一块儿学习干货技术知识,天天进步一点点,让小的积累,带来大的改变!</center>
<p/>
<center><font color='blue'>扫描关注,后台回复【资源】,获取珍藏干货! 99.9%的伙伴都很喜欢</font></center>
<p/>
<center>© 天天都在变得更好的阿飞云</center>