# 类加载器及类加载器执行过程

JDK版本:1.8

# 1、类加载器子系统

下图为类加载子系统:

  • 类加载子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识(CA FE BA BE)。
  • Classloader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
  • 记载的类信息存放在jvm内存中的一块名为Method Area的内存空间中。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)。

# 2、类加载器Classloader的角色

  • class file源码文件存在于本地硬盘上,这一理解为一个模板。而这个模板在执行的时候是需要加载到JVM内存中根据这个文件实例化出n个一摸一样的实例。加载class file的方式是二进制流。
  • class file源码被加载到JVM中,被称为DNA元数据模板,存放在JVM内的Method Area中。
  • .class文件--> JVM --> 最终成为DNA元数据模板。这个过程需要一个运输工具(类装载器Classloader)完成。

# 3、类加载的过程

Java类加载的过程(宏观):

# 3.1、加载(Loading)阶段

加载(Loading)阶段分为3步:

  • 1、通过被加载类的全限定名(包名 + 类名)获取定义此类的二进制字节流。
  • 2、将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。

加载.class文件的方式:

  • 从本地系统中直接加载。
  • 通过网络获取,典型场景:Web Applet
  • zip压缩包中读取,成为日后jarwar格式的基础。
  • 运行时计算生成,使用最多的是:动态代理技术。
  • 由其他文件生成,典型场景:JSP应用。
  • 从专有数据库中提取.class文件。
  • 从加密文件中获取,典型的防止Class文件被反编译的保护措施。

# 3.2、链接(Linkging)阶段

链接(Linking)阶段分为3个步骤:

  • 验证(Verification):

    • 1、确保被加载类的Class文件的二进制字节流中包含的信息符合当前虚拟机的要求。保证被加载类的正确性,不会危害虚拟机自身安全。
    • 2、主要包括四种验证方式:文件格式验证、元数据验证、字节码验证、符号引用验证。
  • 准备(Preparation):

    • 1、为类变量分配内存并设置该类变量的默认初始值,即零值。注意,这里只是分配内存并为其分配默认值,不同类型的默认值也不同。这里并没有对类变量进行实际的具体赋值。
    • 2、这里不包含被final修饰的static变量(这已经不能被称为变量,此时已经是常量),因为final修饰的常量在编译的时候就会分配内存。准备阶段会进行显示初始化,即对其进行具体的赋值。
    • 3、这里不会为实例变量分配初始化(此时对象还未进行创建),类变量会分配在方法区中,而实例变量则是会随着对象一起分配到堆内存中。
  • 解析(Resolution):

    • 1、将常量池内的符号引用转换为直接引用的过程。
    • 2、解析操作往往是会伴随着JVM在执行完初始化后再执行。
    • 3、符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》Class文件格式中。直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄。
    • 4、解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。分别对应常量池中的CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等。
public class HelloLinking {/*** Preparation 阶段只是为其赋初始值0* Initialization 阶段将3赋值给a*/private static int a = 3;public static void main(String[] args) {System.out.println(a);}
}

# 3.3、初始化(Initialization)阶段

  • 初始化阶段就是执行类构造器方法<clinit>()的过程。
  • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。如果类中没有变量需要赋值,是不会出现<clinit>()方法的。
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • <clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())。
  • 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
  • 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。
public class HelloClassLoader {public static void main(String[] args) {Runnable runnable = () -> {System.out.println(getCurrentThreadName() + "开始");new DeadThread();System.out.println(getCurrentThreadName() + "结束");};Thread a = new Thread(runnable, "A线程");Thread b = new Thread(runnable, "B线程");a.start();b.start();}private static class DeadThread {static {System.out.println(getCurrentThreadName() + "正在初始化 [DeadThread] 类");}}private static String getCurrentThreadName() {return Thread.currentThread().getName();}}

最终只会有一个线程获取到锁并初始化DeadThread类。运行程序将会看到只会有一个线程初始化DeadThread类。

B线程开始
A线程开始
B线程正在初始化 [DeadThread] 类
B线程结束
A线程结束Process finished with exit code 0

所以一个类的<clinit>()方法只会被一个线程调用一次。


# 4、类加载器分类

JVM支持两种类型的类加载器 。分别为引导类加载器Bootstrap ClassLoader和自定义类加载器User-Defined ClassLoader

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但在Java虚拟机规范中却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个:

  • 引导类加载器(BootStrap Class Loader)
  • 扩展类加载器(Extension Class Loader)
  • 系统类加载器(System Class Loader)

它们之间的关系是包含关系,并不是上下层或者子父类继承。

可以看到Java中的扩展类加载器ExtClassLoader,它是Launcher中的一个静态内部类。同时也可以获取到AppClassLoader,其也是定义在Launcher类中的一个静态内部类。而BootStrap Class Loader是获取不到的,它是由CC++进行编写的。

使用代码获取加载器:

public class CustomClassLoader {public static void main(String[] args) {// 获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();// sun.misc.Launcher$AppClassLoader@18b4aac2System.out.println("systemClassLoader = " + systemClassLoader);// 获取其上层即扩展类加载器ClassLoader extensionClassLoader = systemClassLoader.getParent();// sun.misc.Launcher$ExtClassLoader@1b6d3586System.out.println("extensionClassLoader = " + extensionClassLoader);// 试图获取 BootStrap Class LoaderClassLoader bootStrapClassLoader = extensionClassLoader.getParent();// nullSystem.out.println("bootStrapClassLoader = " + bootStrapClassLoader);// 获取用户自定义类使用的类加载器ClassLoader classLoader = CustomClassLoader.class.getClassLoader();// sun.misc.Launcher$AppClassLoader@18b4aac2System.out.println("classLoader = " + classLoader);ClassLoader stringClassLoader = String.class.getClassLoader();// nullSystem.out.println("stringClassLoader = " + stringClassLoader);}}

输出结果:

systemClassLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
extensionClassLoader = sun.misc.Launcher$ExtClassLoader@1b6d3586
bootStrapClassLoader = null
classLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
stringClassLoader = nullProcess finished with exit code 0

对于开发者自定义的类,默认使用的是系统类加载器进行加载。

对于Java的核心类库都是使用的是引导类加载器进行加载。


# 5、虚拟机自带的类加载器

# 1、启动类加载器(引导类加载器,Bootstrap Class Loader)

  • BootStrap类加载使用C/C++语言实现的,嵌套在JVM内部。
  • BootStrap类加载用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jarresources.jarsun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
  • BootStrap类加载并不继承自java.lang.ClassLoader,没有父加载器。
  • BootStrap类加载器加载扩展类和应用程序类加载器,并指定为他们的父类加载器
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为javajavaxsun等开头的类。

2、扩展类加载器(Extension Class Loader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类。
  • 父类加载器为启动类加载器。
  • java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

# 3、应用类加载器(系统类加载器,App Class Loader)

  • Java语言编写,由sun.misc.Launchers$AppClassLoader实现。
  • 派生于ClassLoader类。
  • 父类加载器为扩展类加载器。
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。
  • 通过ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器。
public class TestClassLoader {private static final String SEMICOLON = ";";private static final String EXT_URL = "java.ext.dirs";public static void main(String[] args) {URL[] urLs = Launcher.getBootstrapClassPath().getURLs();Arrays.stream(urLs).forEach(System.out::println);System.out.println("\n");// 从上面的路劲中随便挑选一个类, 查看其类加载器 :ClassLoader proxyClassClassLoader = Proxy.class.getClassLoader();// null --> BootStrap Class LoaderSystem.out.println("proxyClassClassLoader = " + proxyClassClassLoader);System.out.println("\n");System.out.println("===========extension class loader spilt line===========");// 寻找 jre/lib/ext 目录下的 class 获取 ClassLoaderClassLoader ecKeyFactoryClassLoader = ECKeyFactory.class.getClassLoader();// sun.misc.Launcher$ExtClassLoader@7cca494bSystem.out.println("ecKeyFactoryClassLoader = " + ecKeyFactoryClassLoader);System.out.println("\n");String extensionUrl = System.getProperty(EXT_URL);Arrays.stream(extensionUrl.split(SEMICOLON)).forEach(System.out::println);// 获取自定义类的 ClassLoaderClassLoader classLoader = CustomClassLoader.class.getClassLoader();// sun.misc.Launcher$AppClassLoader@18b4aac2System.out.println("classLoader = " + classLoader);}}

# 6、用户自定义类加载器

Java的日常应用程序开发中,类的加载几乎是由BootStrap Class LoaderExtension Class LoaderApp Class Loader类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?

  • 隔离加载类.。

  • 修改类加载的方式。

  • 扩展加载源。

  • 防止源码泄漏。

用户自定义类加载器实现步骤:

  • 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求。
  • JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass() 方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass() 方法,而是建议把自定义的类加载逻辑写在findClass()方法中。
  • 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加优雅简洁。

自定义一个CustomClassLoader类加载器派生于ClassLoader

public class CustomClassLoader extends ClassLoader {private String classPath;public CustomClassLoader(String classPath) {this.classPath = classPath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] classFromCustomPath = getClassFromCustomPath();if (classFromCustomPath == null) {throw new FileNotFoundException();} else {return super.defineClass(name, classFromCustomPath, 0, classFromCustomPath.length);}} catch (FileNotFoundException e) {e.printStackTrace();}throw new ClassNotFoundException();}/*** 以二进制流的方式将指定的 class 文件读取到系统中来* 如果指定路劲的字节码进行了加密, 则需要在此方法中进行解密操作, 解密之后将其还远为字节数组** @return byte[]*/private byte[] getClassFromCustomPath() {if (this.classPath == null || "".equals(this.classPath)) {return null;}File file = new File(this.classPath);if (file.exists()) {try (FileInputStream fileInputStream = new FileInputStream(file);ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {byte[] buffer = new byte[1024];int size;while ((size = fileInputStream.read(buffer)) != -1) {byteArrayOutputStream.write(buffer, 0, size);}return byteArrayOutputStream.toByteArray();} catch (IOException e) {e.printStackTrace();}}return null;}}

创建一个Log类使用自定义类加载器进行加载:

public class Log {public static void main(String[] args) {System.out.println("Log class load by Custom Class Loader Success!");}}

运行Log类中的main方法,运行完成之后在out文件目录下找到该类的class文件即Log.class文件。

创建一个TestCustomClassLoader类用于测试自定义类加载器:

public class TestCustomClassLoader {// Log 类的全类名private static final String ALL_PACKAGE_NAME = "com.kapcb.ccc.jvm.classload.Log";// Log 类的 class 文件路径private static final String LOG_CLASS_PATH = "D:/DevelopTools/IDEA/IDEA-workspace/Java-Kapcb/out/production/Java-Kapcb/com/kapcb/ccc/jvm/classload/Log.class";public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {CustomClassLoader customClassLoader = new CustomClassLoader(LOG_CLASS_PATH);Class<?> LogClass = customClassLoader.loadClass(ALL_PACKAGE_NAME);ClassLoader classLoader = LogClass.getClassLoader();System.out.println("Log 类的类加载器是 : [ " + classLoader + " ]");// 获取 Log 类中的 main 方法Method mainMethod = LogClass.getDeclaredMethod("main", String[].class);// 实例化 Log 类Object object = LogClass.newInstance();// 随便传入一个参数String[] arg = new String[]{"ad"};// 反射激活 Log 类中的 main 方法mainMethod.invoke(object, (Object) arg);}}

启动测试类的main方法输出结果:

Log 类的类加载器是 : [ sun.misc.Launcher$AppClassLoader@18b4aac2 ]
Log class load by Custom Class Loader Success!Process finished with exit code 0

# 7、ClassLoader的常用API

ClassLoader类是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),其常用API有以下几个方法:

方法名称 描述
getParent() 返回当前类加载器的超类加载器
loadClass(String name) 加载名称为name的类(这里的name是全类名),返回结果为java.lang.Class类的实例
findClass(String name) 查找名称为name的类(这里的name是全类名),返回结果为java.lang.Class类的实例
findLoadedClass(String name) 查找类名为name的已经被加载过的类,返回结果为java.lang.Class类的实例
defineClass(String name, byte[] b, int off, int len) 将字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例
resolveClass(Class<?> cla) 链接指定的一个Java

sun.misc.Launcher是一个java虚拟机的入口应用:

获取ClassLoader的途径:

方式一:获取当前类的ClassLoader

clazz.getClassLoader();

方式二:获取当前线程上下文的ClassLoader

Thread.currentThread().getContextClassLoader();

方式三:获取系统的ClassLoader

ClassLoader.getSystemClassLoader();

方式四:获取调用者的ClassLoader

DriverManager.getCallerClassLoader();

GitHub源码地址:https://github.com/kapbc/Java-Kapcb/tree/master/src/main/java/com/kapcb/ccc/jvm

备注:此文为笔者学习JVM的笔记,鉴于本人技术有限,文中难免出现一些错误,感谢大家批评指正。

JVM - 类加载器相关推荐

  1. Java高并发编程详解系列-JVM类加载器

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

  2. 《Java虚拟机原理图解》5. JVM类加载器机制与类加载过程

    参考网址:http://blog.csdn.net/luanlouis/article/details/50529868 0.前言 读完本文,你将了解到: 一.为什么说Jabalpur语言是跨平台的 ...

  3. 彻底搞懂JVM类加载器:基本概念

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 作者 | 阿杜的世界 来源 | javaadu 在Java面试中,在考察完项目经验.基础技术 ...

  4. jvm:类加载器与双亲委派模型

    两个类相等需要类本身相等,并且使用同一个类加载器进行加载.这是因为每一个类加载器都拥有一个独立的类名称空间. 这里的相等,包括类的 Class 对象的 equals() 方法.isAssignable ...

  5. jvm类加载器以及双亲委派

    首先来了解几个概念: 类加载: 概念:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验--转换解析--初始化,最终形成能被java虚拟机直接使用的java类型,就是jvm的类加载机制. ...

  6. jvm运行时类加载机制_JVM体系结构:JVM类加载器和运行时数据区

    jvm运行时类加载机制 各位读者好! 在JVM系列的上一篇文章中,开发人员了解了Java虚拟机(JVM)及其体系结构. 本教程将帮助开发人员正确回答以下主题的问题: ClassLoader子系统 运行 ...

  7. JVM体系结构:JVM类加载器和运行时数据区

    各位读者好! 在JVM系列的上一篇文章中,开发人员了解了Java虚拟机(JVM)及其体系结构. 本教程将帮助开发人员正确回答以下主题的问题: ClassLoader子系统 运行时数据区 1.简介 在继 ...

  8. JVM 类加载器(引导类加载器、扩展类加载器、系统类加载器、用户自定义类加载器)

    1 引导类加载器 引导类加载器(Boostrap ClassLoader),又叫启动类加载器. 由C/C++语言实现,嵌套在JVM内部. 用来加载 Java 的核心库(JAVA_HOME/jre/li ...

  9. 22-09-02 西安 JVM 类加载器、栈、堆体系、堆参数调优、GC垃圾判定、垃圾回收算法、对象的finalize机制

    这篇文章不少地方都截图了宋红康老师的课件,实在他jvm这块讲的真好.连接地址如下: 尚硅谷宋红康JVM全套教程(详解java虚拟机)_哔哩哔哩_bilibili JVM入门 1.JVM结构图 JVM是 ...

  10. JVM 类加载器与双亲委派模型

    1. 类加载器 我们知道,虚拟机在加载类的过程中需要使用类加载器进行加载,而在 Java 中,类加载器有很多,那么当 JVM 想要加载一个 .class 文件的时候,到底应该由哪个类加载器加载呢?这时 ...

最新文章

  1. 虚拟化数据中心服务器硬件配置建议
  2. shodan 撒旦 新手入坑指南
  3. python3在线-python在线练习
  4. 深入理解Spark 2.1 Core (四):运算结果处理和容错的原理与源码分析
  5. 【OpenStack】OpenStack系列9之Compute节点安装
  6. fritz 使用手册_Fritz对象检测指南:使用机器学习在Android中构建宠物监控应用
  7. 中科大软件学院硕士:实习秋招百多轮面试总结(中)
  8. HTML5从入门到精通
  9. uniapp 小程序授权登录时 获取不到用户头像等信息 只能得到灰色缺省头像以及nickname 为“微信用户”等
  10. XRD如何分析残余应力
  11. ios刺客信条一直显示连接服务器,刺客信条本色iOS进不去怎么办
  12. 某音init_gorgon(),x-gorgon分析和数据的获取(3)
  13. 函数简介篇——环境变量函数
  14. 阮一峰:值得分享给开发者的 Authing 身份云
  15. 使用MapReduce计算Pi
  16. android 自动调整屏幕分辨率
  17. oracle外部表kup-04023,kup-04040访问外部表时
  18. 【信息安全案例】——网络信息面临的安全威胁(学习笔记)
  19. python爬虫selenium操作下拉框详解
  20. SIM卡PIN码到底有什么用?华为手机如何设置SIM卡锁?

热门文章

  1. 佐罗一键新机数据导出导入文件夹
  2. android磁盘管理工具,易我分区大师(磁盘分区管理工具)
  3. mysql进阶教程pdf_SQL进阶教程 (MICK著) 高清pdf完整版
  4. 安装了谷歌服务框架还是闪退_小米手机安装谷歌服务(Google play)
  5. es6学习推荐网址(阮一峰)
  6. 用Java简单实现单例模式
  7. Python 免费翻译API
  8. 201671010410 冯婷秀 实验十四 个人总结
  9. 联想计算机不能使用ghost,如何解决联想台式机不能重装系统
  10. 35 红外接收头在linux内核里的驱动