之前的博客中提到了类加载的过程,提到了双亲委托机制,提到了关于类加载器的概念,这篇博客就来给大家分享一下什么是JVM的类加载器。通过实战的方式来了解一下类加载器器到底是什么。

JVM类加载器分类

  类加载器就是在类加载的过程中负责对于class文件进行加载的对象。也就是说通过这类加载器来确定每个类与JVM的唯一性关系。对于任何一个对象在JVM都是唯一存在的。
  在JVM中类加载器主要分为三类,按照接近内存接近底层的顺序可以分为,Bootstrap ClassLoader,ExtClassLoader,ApplicationClassLoader,以及自定义类加载器。

BootstrapClassLoader(根类加载器)

  对于这个类加载器来说,是作为比较底层的一个类加载器,这个类加载器几乎就是操作到内存层面上,主要是通过由C++语言来编写的。可以通过-Xbootclasspath参数来指定这个加载器器的路径。例如

public class BootstrapClassLoaderTest {public static void main(String[] args) {System.out.println("Bootstrap:"+String.class.getClassLoader());System.out.println(System.getProperty("sun.boot.class.path"));}
}

  在这里我们会发现第一个输出为空,第二个输出是我们类路径下的几个jar包的位置。而这些jar包就是我们操作BootstrapClassLoader的jar包。

ExtClassLoader(扩展类加载器)

  正如上面提到的一样,扩展类加载器其实是BootStrapClassLoader的子类,也就是说到扩展类加载器开始就是使用Java语言来编写。那么首先我们就来看一下关于扩展类加载器的一些信息。首先ExtClassLoader是作为java.long.URLClassLoader的子类出现的可以看到他的全类名在下面结果中也有输出。通过系统属性java.ext.dirs来获取类加载器库的内容。

public class ExtClassLoaderTest {public static void main(String[] args) throws ClassNotFoundException {Class<?> test = Class.forName("com.example.charp10.Test");System.out.println(test.getClassLoader());System.out.println(System.getProperty("java.ext.dirs"));}
}

ApplicationClassLoader(系统类加载器)

  这个类加载器在Java中是比较常用的一个加载器,它的作用就是对classpath的类资源进行加载,按照上面的说法,系统类加载器的父类是扩展类加载器器,如果在项目中引入了第三方的jar包是通过双亲委派机制找到扩展类加载器进行加载,当然系统类加载器也是我们自定义类加载器的父类,它可以由系统参数java.class.path来进行获取。

public class ApplicationClassLoaderTest {public static void main(String[] args) {System.out.println(System.getProperty("java.class.path"));System.out.println(ApplicationClassLoaderTest.class.getClassLoader());}
}

CustomClassLoader(自定义类加载器)

  首先我们要了解一下自定义类加载器为什么要实现自定义的类加载器,因为在有些场景下我们不希望我们写的代码可以被别人直接反编译之后直接使用,所以我们通过自定义类加载器的方式实现对于字节码的加密和解密操作。这样可以保证我们程序的安全性。那么怎么实现一个自定义的类加载器首先我们先来看一下ClassLoader类是什么情况
ClassLoader类
  按照之前类加载器过程来说,首先第一步应该有一个获取类的字节码文件的过程,而这个过程需要调用一系列方法,在ClassLoader中就提供了这样一系列的方法由于方法太多也就不一个一个查看了。完成第一步加载到资源之后,第二步的操作就是连接初始化操作,也看到了在ClassLoader中提供了一些方法去实现初始化操作。

基于这样一个过程,我们按照之前类加载的过程来实现一个自己的类加载器。

public class MyClassLoader extends ClassLoader {//设置默认的class文件存放路径target/classes/com/example/charp10private final static Path DEFAULT_CLASS_DIR= Paths.get("target/classes/com/example/","charp10");private final Path  classDir;public MyClassLoader() {super();this.classDir = DEFAULT_CLASS_DIR;}//允许通过参数传入类路径public MyClassLoader(String classDir) {super();this.classDir = Paths.get(classDir);}public MyClassLoader(ClassLoader parent, Path classDir) {super(parent);this.classDir = classDir;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] classBytes = this.readClassBytes(name);if (null==classBytes||classBytes.length==0){throw new ClassNotFoundException("Can not load the class"+name);}return this.defineClass(name,classBytes,0,classBytes.length);}private byte[] readClassBytes(String name) throws ClassNotFoundException {String classPath = name.replace(".","/");Path classFullPath = classDir.resolve(Paths.get(classPath+".class"));if (!classFullPath.toFile().exists()){throw new ClassNotFoundException("The class "+name+"not fund");}try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {Files.copy(classFullPath,baos);return baos.toByteArray();} catch (IOException e) {throw new ClassNotFoundException("load the class "+name+" occur error.",e);}}@Overridepublic String toString() {return "My ClassLoader";}
}

需要加载的类

public class Test {static {System.out.println("Test Class init");}public String sayHello(){return "hello";}
}

测试代码

public class MyClassLoaderTest {public static void main(String[] args) throws Exception {MyClassLoader classLoader = new MyClassLoader();Class<?> mClass = classLoader.loadClass("com.example.charp10.Test");System.out.println(mClass.getClassLoader());Object test = mClass.newInstance();System.out.println(test);Method sayHelloMethod = mClass.getMethod("sayHello");String result = (String) sayHelloMethod.invoke(test);System.out.println("Result:"+result);}
}

测试结果

当然上面这只是一个简单的类加载器实现机制,还可重写很多的方法实现其他的特殊的功能的自定义类加载器,可以使用自定义的类加载器去加载一些关键的Class文件对其进行加密操作。

双亲委托机制介绍


如上图,当一个类需要被加载器的时候首先调用的自定类加载器,如果没有自定义的类加载器就调用系统类加载器,调用系统类加载器之后,系统类加载器会委托调用扩展类加载器,扩展类委托根类加载器进行加载,加载完成之后,返回给扩展类加载器,扩展类在告诉系统类加载器加载成功,系统类通知自定义类加载器加载成功,整个过程算是加载成功,如果到中间没有找到对应的类就会加载失败。这个过程的调用是从我们ClassLoader的loadClass开始的。

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
  protected final Class<?> findLoadedClass(String name) {if (!checkName(name))return null;return findLoadedClass0(name);}private native final Class<?> findLoadedClass0(String name);
 private Class<?> findBootstrapClassOrNull(String name){if (!checkName(name)) return null;return findBootstrapClass(name);}

  我们可以看到整个的类加载过程是线程安全的,首先找到需要加载的类是否存在,如果存在就找对应的父类记载在方法进行加载。如果没有父类加载器,就直接调用根类加载器进行加载,如果都没有加载成功,则尝试使用findClass方法进行加载,而这个方法就是自定义类加载器重写的方法
  由于loadCla指定resolve为false,所以不会进行连接阶段的继续,也就是解释了为什么通过类加载器加载的类并不会导致类的初始化,因为到链接阶段它已经停止了。

破坏双清委托机制

  既然我们了解了关于类加载过程以及类加载器的过程,那么我们可以知道如果找到对应的class文件并将其替换掉就可以对Java程序进行破坏了。在实际工作中有时候就需要破坏这种机制,例如之前提到的对自己写的代码进行加密。
  JDK提供的双亲委托机制并不是强制的执行,所以这就允许开发人员对这种机制进行破坏。当然这里的破坏并不是去做黑客,而是在这个基础上开发新功能。也就是常说的热部署,在不用停止应用的情况下对应用进行改变。但是对于JVM内置的三大类加载器我们是没有办法去改变的我们所能改变的就是我们自己实现的类加载器。我们之前实现的类加载器是对findClass方法进行了重写,但是我们知道真正控制整个类加载的是loadClass,所以要对于loadClass方法进行重写才会起作用。

    @Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {Class<?> klass = findLoadedClass(name);if (klass == null) {if (name.startsWith("java.") || name.startsWith("javax.")) {try {klass = getSystemClassLoader().loadClass(name);} catch (Exception e) {throw e;}} else {try {klass = this.findClass(name);} catch (Exception e) {e.printStackTrace();}if (klass == null) {if (getParent() != null) {klass = getParent().loadClass(name);} else {klass = getSystemClassLoader().loadClass(name);}}}}if (null == klass) {throw new ClassNotFoundException("The class " + name + " not found.");}if (resolve) {resolveClass(klass);}return klass;}}

代码解释

  1. 根据类的全类名进行加锁操作,也就是保证了线程安全
  2. 到已经加载的缓存中查看是否已经被加载了如果是则直接返回,如果没有就需要进行加载
  3. 如果缓存中没有,则表示这个类是第一次被加载,对于类进行判断操作
  4. 如果不满足要求则表示使用自定义的类加载器进行加载操作。
  5. 如果自定义加载器没有完成则需要交给父类加载器去进行加载操作
  6. 如果加载不成功的话就抛出异常。

还是之前的问题,既然我们可以破坏双亲委托机制那么我们可以不可以使用自定义的类加载器加载属于我们的String类的class文件呢?这个就是加载完成之后在连接时候为什么会出现验证准备然后才会进行解析操作的原因

类加载的名称空间、运行时包、类的卸载等操作

1.类的名称空间
  在每个类加载的时候都有属于自己的名称空间,这个名称空间是由父类加载器所构成的。也就是说每个class在加载的时候都是独一无二的存在。例如

public class NameSpace {public static void main(String[] args) throws ClassNotFoundException {ClassLoader classLoader = NameSpace.class.getClassLoader();Class<?> aClass = classLoader.loadClass("com.example.charp10.Test");Class<?> bClass = classLoader.loadClass("com.example.charp10.Test");System.out.println(aClass.hashCode());System.out.println(bClass.hashCode());System.out.println(aClass == bClass);}
}


分析一下类加载完成之后的内存图

会发现无论使用多少次的类加载都会是同一份的Class对象,这也就是为什么每个类在加载完成之后只有一个并且是唯一的对象在内存中。

那么如果使用不同的类加载器去加载同一个实例,或者使用同一个类加载器加载不同实例那么在堆栈方法区中会产生多少个对象呢?

使用不同类加载器加载同一个class

 public static void main(String[] args) throws Exception {BrokerDelegateClassLoader brokerclassLoader = new BrokerDelegateClassLoader();MyClassLoader classLoader = new MyClassLoader();Class<?> mClass = classLoader.loadClass("com.example.charp10.Test");Class<?> bClass = brokerclassLoader.loadClass("com.example.charp10.Test");System.out.println(mClass.getClassLoader());System.out.println(bClass.getClassLoader());System.out.println(mClass.hashCode());System.out.println(bClass.hashCode());System.out.println(mClass==bClass);}


相同的类加载器加载同一个class

 public static void main(String[] args) throws Exception {MyClassLoader aclassLoader = new MyClassLoader("target/classes/",null);MyClassLoader bclassLoader = new MyClassLoader("target/classes/",null);Class<?> aClass = aclassLoader.loadClass("com.example.charp10.Test");Class<?> bClass = bclassLoader.loadClass("com.example.charp10.Test");System.out.println(aClass.getClassLoader());System.out.println(bClass.getClassLoader());System.out.println(aClass.hashCode());System.out.println(bClass.hashCode());System.out.println(aClass==bClass);
}


分析源码可以知道,在类加载器进行加载过程中,首先加载的就是在缓存中的,如果该类在缓存中已经存在,就说明被加载过了,就不需要重新加载,否则就是第一次加载。如图,相同的对象被不同的类加载器加载之后的内存情况,会在内存中出现多个实例。这个就是应为在同一个Class实例在同一个类加载器的名称空间之下是唯一的。在不同的类加载器的名称空间下是不唯一的。

2.运行时包
  在我们开发程序的时候都需要给类起一个包名,有了包名防止了在同一个包下的Class的冲突。还可以起到封装的作用。而我们知道Class的名称就是有包名加类名的全类名构成,这个也为我们提供了一个权限控制机制,也就是我们提到的public、producted、和private等权限修饰符
3.初始类加载器
  在运行时环境下我们怎么知道哪些类有哪些访问权限呢,这个就需要使用到根类加载器,这个加载器可以加载任何的JDK包下的class。对于第三方的jar则是由我们的系统类加载器来加载。
  根据JVM规范指出,在类加载过程中所有的类加载器包括自定义的类加载器,即使是没有加载过该类,也会被标记为该类的初始加载器。
4.类的卸载
  在JVM启动的时候会有很多的类被加载,但是这些类被加载完成时候什么时候被卸载呢,我们知道类的卸载其实就是GC垃圾回收机制,如果在JVM内存中没有足够的空间则会被GC回收掉一部分类。那么这部分类在满足什么条件的时候被回收呢

  1. 该类没有任何的引用指向的时候,或者说该类的所有的实例都被回收之后
  2. 加载该类型的类加载器被回收了

遇见上面的情况就表示该类在JVM中已经被卸载了

总结

  这里主要说了关于Java虚拟机的类加载器,以及类加载过程,但是这些都是在单个线程下面执行的。没有涉及到多线程的操作。从下一篇文章开始就要开始在多线程下讨论问题了。通过上面对于单线程的类加载机制的了解,也深刻的理解了双亲委派机制。为后面使用自定义的类加载器打下基础。

Java高并发编程详解系列-JVM类加载器相关推荐

  1. Java高并发编程详解系列-Java线程入门

    根据自己学的知识加上从各个网站上收集的资料分享一下关于java高并发编程的知识点.对于代码示例会以Maven工程的形式分享到个人的GitHub上面.   首先介绍一下这个系列的东西是什么,这个系列自己 ...

  2. Java高并发编程详解系列-7种单例模式

    引言 在之前的文章中从技术以及源代码的层面上分析了关于Java高并发的解决方式.这篇博客主要介绍关于单例设计模式.关于单例设计模式大家应该不会陌生,作为GoF23中设计模式中最为基础的设计模式,实现起 ...

  3. Java高并发编程详解系列-线程上下文类加载

    前面的分享中提到的最多的概念就是关于类加载器的概念,但是当我们查看Thread源码的时候会发现如下的两个方法,这两个方法就是获取或者设置线程的上下文类加载器的方法,那么为什么要设置这两个方法呢?这个就 ...

  4. Java高并发编程详解系列-线程上下文设计模式及ThreadLocal详解

    导语   在之前的分享中提到过一个概念就是线程之间的通信,都知道在线程之间的通信是一件很消耗资源的事情.但是又不得不去做的一件事情.为了保证多线程线程安全就必须进行线程之间的通信,保证每个线程获取到的 ...

  5. Java高并发编程详解系列-类加载

    之前在写关于JVM的时候提到过类加载机制,类加载机制也是在Java面试中被经常问道的一个问题,在这篇博客中就来了解一下关于类加载的知识. 类加载   在JVM执行Java程序的时候实际上执行的编译好的 ...

  6. Java高并发编程详解系列-Future设计模式

    导语   假设,在一个使用场景中有一个任务需要执行比较长的时间,通常需要等待任务执行结束之后或者是中途出错之后才能返回结果.在这个期间调用者只能等待,对于这个结果Future设计模式提供了一种凭据式的 ...

  7. Java高并发编程详解系列-深入理解Thread构造

    上篇分享中主要是对线程的基本概念和基本操作做了一个分享,同时提出了两种常用的创建多线程的方法,当然在后期的分享中也会提及到更多的创建线程的方式,到后期的分享的时候再说. 这次主要是深入的理解一下Thr ...

  8. Java高并发编程详解系列-线程安全数据同步

    在多线程中最为复杂和最为重要的就是线程安全.多个线程访问同一个对象的时候会导致线程安全问题.通过加锁可以避免这种问题.但是在串行执行的过程中又不用考虑线程安全问题,而使用串行程序效率低没有办法将CPU ...

  9. Java高并发编程详解系列-线程通信

      进程间的通信,又被称为是进程内部的通信,我们都知道每个进程中有多个线程在执行,多个线程要互斥的访问共享资源的时候会发送对应的等待信号或者是唤醒线程执行等信号.那么这些信号背后还有什么样的技术支持呢 ...

最新文章

  1. 【存储知识学习】第二章存走进计算机IO世界--《大话存储》阅读笔记
  2. 问题解决:使用angularjs、ionic框架如何实现返回上一页并刷新
  3. Java连接Access数据库的那些坑
  4. 运算符重载为类的成员函数
  5. xcode中遇到的英文名词(更新)
  6. Confluence 6 审查日志的对象
  7. 将dBm转换为W的方法
  8. 论文公式编号MATHTYPE
  9. 实现 RecyclerView 上拉加载及自动加载
  10. kaggel竞赛之员工离职分析
  11. 在AWS Lambda上部署EC2编译的FFmpeg工具——自定义层的方案
  12. c++头文件中的防卫式声明
  13. excel替换固定sql中的某个值实现批量导入
  14. NOI模拟(5.11) BJOID2T3 治疗之雨 (bzoj5292)
  15. Bert模型详解和训练实例
  16. php微擎Goto2C解密研究
  17. 艾宾浩斯记忆遗忘曲线
  18. 产品3C认证在哪里办理
  19. [前端优化]基于H5移动端优化总结
  20. 又土又木到不土不木——《土力学原理十记》全笔记(修正版)

热门文章

  1. java types.varchar_java statement.registerOutParameter(5, java.sql.Types.VARCHAR)返回值长度限制...
  2. linux mysql root用户密码_Linux下修改Mysql的用户(root)的密码
  3. 企业级工作流解决方案(十六)--工作流--工作流实体模型
  4. mysql-bin日志文件清理
  5. 小博老师演示常用JQuery效果 ——图片轮播
  6. MVVM架构~knockoutjs系列之验证信息自定义输出~续
  7. 我是不是在浪费生命?
  8. 为CheckBoxList每个项目添加一张图片
  9. 在win7下搭建php+apache+mysql环境
  10. TNFBA治疗极早期中轴型nrSpA随机双盲试验后长达6年的放射学随访