本文内容过于硬核,建议有 Java 相关经验人士阅读。

1. 什么是类的加载?

类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class 对象, Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。

类加载器并不需要等到某个类被 「首次主动使用」 时再加载它, JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误( LinkageError 错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

  1. 加载.class文件的方式
  2. 从本地系统中直接加载
  3. 通过网络下载.class文件
  4. zipjar等归档文件中加载.class文件
  5. 从专有数据库中提取.class文件
  6. Java源文件动态编译为.class文件

2. 类的生命周期

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。

2.1 加载(Loading)

加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取其定义的二进制字节流。

  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在 Java 堆中也创建一个 java.lang.Class 类的对象,这样便可以通过该对象访问方法区中的这些数据。

2.2 验证(Verification)

验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合「Java虚拟机规范」的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

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

  1. 文件格式验证: 验证字节流是否符合 Class 文件格式的规范;例如:是否以 0xCAFEBABE 开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比 javac 编译阶段的语义分析),以保证其描述的信息符合 Java 语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object 之外。
  3. 字节码验证: 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  4. 符号引用验证: 确保解析动作能正确执行。

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

2.3 准备(Preparation)

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

  1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  2. 这里所设置的初始值通常情况下是数据类型默认的零值(如 0 、 0L 、 null 、 false 等),而不是被在 Java 代码中被显式地赋予的值。

2.4 初始化(Initialization)

类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由 Java 虚拟机来主导控制。直到初始化阶段, Java 虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。

在 Java 中对类变量进行初始值设定有两种方式:

  1. 声明类变量是指定初始值。
  2. 使用静态代码块为类变量指定初始值。

3. 类加载器

类加载器就是负责加载所有的类,将其载入内存中,生成一个 java.lang.Class 实例。一旦一个类被加载到 JVM 中之后,就不会再次载入了。

  • 启动类加载器(Bootstrap ClassLoader):其负责加载 Java 的核心类,比如 String 、 System 这些类。
  • 拓展类加载器(Extension ClassLoader):其负责加载 JRE 的拓展类库。
  • 系统类加载器(System ClassLoader):其负责加载 CLASSPATH 环境变量所指定的 JAR 包和类路径。
  • 用户类加载器:用户自定义的加载器,以类加载器为父类。

一个简单的小栗子:

  1. public static void main(String[] args) {
  2. ClassLoader loader = ClassLoader.getSystemClassLoader();
  3. System.out.println(loader);
  4. System.out.println(loader.getParent());
  5. System.out.println(loader.getParent().getParent());
  6. }

输出结果:

  1. sun.misc.Launcher$AppClassLoader@18b4aac2
  2. sun.misc.Launcher$ExtClassLoader@1b6d3586
  3. null

为什么根类加载器为 NULL ?

启动类加载器(Bootstrap Loader)并不是 Java 实现的,而是使用 C 语言实现的,找不到一个确定的返回父 Loader 的方式,于是就返回 null 。

JVM 类加载机制

  1. 全盘负责:当一个类加载器负责加载某个 Class 时,该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
  2. 父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
  3. 缓存机制,缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区寻找该 Class ,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区。这就是为什么修改了 Class 后,必须重启 JVM ,程序的修改才会生效。

4. 双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制:

  1. 当 AppClassLoader 加载一个 class 时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器 ExtClassLoader 去完成。
  2. 当 ExtClassLoader 加载一个 class 时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给 BootStrapClassLoader 去完成。
  3. 如果 BootStrapClassLoader 加载失败(例如在 $JAVA_HOME/jre/lib 里未查找到该 class ),会使用 ExtClassLoader 来尝试加载。
  4. 若 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载,如果 AppClassLoader 也加载失败,则会报出异常 ClassNotFoundException 。

以下为 ClassLoader#loadClass 的源码, JDK 版本为 1.8.0_221 。

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // 首先判断该类型是否已经被加载
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. // 如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
  9. long t0 = System.nanoTime();
  10. try {
  11. if (parent != null) {
  12. // 如果存在父类加载器,就委派给父类加载器加载
  13. c = parent.loadClass(name, false);
  14. } else {
  15. // 如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法 native Class findBootstrapClass(String name)
  16. c = findBootstrapClassOrNull(name);
  17. }
  18. } catch (ClassNotFoundException e) {
  19. // ClassNotFoundException thrown if class not found
  20. // from the non-null parent class loader
  21. }
  22. if (c == null) {
  23. // If still not found, then invoke findClass in order
  24. // to find the class.
  25. long t1 = System.nanoTime();
  26. c = findClass(name);
  27. // this is the defining class loader; record the stats
  28. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  29. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  30. sun.misc.PerfCounter.getFindClasses().increment();
  31. }
  32. }
  33. if (resolve) {
  34. resolveClass(c);
  35. }
  36. return c;
  37. }
  38. }

双亲委派模型是为了防止内存中出现多份同样的字节码,保证程序稳定的运行。

5. 自定义类加载器

在最开始,我想先介绍下自定义类加载器的适用场景:

  1. 加密: Java 代码可以轻易的被反编译,如果需要把代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,这样加密后的类就不能再用 Java 的 ClassLoader 去加载类了,这时就需要自定义 ClassLoader 在加载类的时候先解密类,然后再加载。
  2. 从非标准的来源加载代码:如果我们的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。

一个小案例,首先我们创建一个需要加载的目标类:

  1. public class ClassLoaderTest {
  2. public void hello() {
  3. System.out.println("我是由 " + getClass().getClassLoader().getClass() + " 加载的");
  4. }
  5. }

这个类先进行编译,编译后的 class 我放到了 D 盘的根目录,然后删除原本在项目中的 class 文件,如果不删除的话,通过前面的双亲委派模型,我们会知道这个 class 会被 sun.misc.Launcher$AppClassLoader 进行加载。

然后我们定义一个自己的加载类:

  1. public class MyClassLoader extends ClassLoader {
  2. public MyClassLoader(){}
  3. public MyClassLoader(ClassLoader parent){
  4. super(parent);
  5. }
  6. protected Class<?> findClass(String name) throws ClassNotFoundException {
  7. File file = new File("D:\\ClassLoaderTest.class");
  8. try{
  9. byte[] bytes = getClassBytes(file);
  10. //defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
  11. Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
  12. return c;
  13. }
  14. catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. return super.findClass(name);
  18. }
  19. private byte[] getClassBytes(File file) throws Exception {
  20. // 这里要读入.class的字节,因此要使用字节流
  21. FileInputStream fis = new FileInputStream(file);
  22. FileChannel fc = fis.getChannel();
  23. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  24. WritableByteChannel wbc = Channels.newChannel(baos);
  25. ByteBuffer by = ByteBuffer.allocate(1024);
  26. while (true){
  27. int i = fc.read(by);
  28. if (i == 0 || i == -1)
  29. break;
  30. by.flip();
  31. wbc.write(by);
  32. by.clear();
  33. }
  34. fis.close();
  35. return baos.toByteArray();
  36. }
  37. public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
  38. MyClassLoader classLoader = new MyClassLoader();
  39. Class clazz = classLoader.loadClass("com.geekdigging.lesson03.classloader.ClassLoaderTest");
  40. Object obj = clazz.newInstance();
  41. Method helloMethod = clazz.getDeclaredMethod("hello", null);
  42. helloMethod.invoke(obj, null);
  43. }
  44. }

最后打印结果:

  1. 我是由 class com.geekdigging.lesson03.classloader.MyClassLoader 加载的

参考

https://www.cnblogs.com/ityouknow/p/5603287.html

JVM 第三篇:Java 类加载机制的更多相关文章

  1. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

  2. 大牛带你学会java类加载机制,不要错过,值得收藏!

    很多人对java类加载机制都是非常抗拒的,因为这个太难理解了,但是我们作为一名优秀的java工程师,还是要把java类加载机制研究和学习明白的,因为这对于我们在以后的工作中有很大的帮助,因为它在jav ...

  3. 深入理解Java类加载机制,再也不用死记硬背了

    谈谈"会"的三个层次 在<说透分布式事务>中,我举例里说明了会与会的差别.对一门语言的学习,这里谈谈我理解的"会"的三个层次: 第一层:了解这门语言 ...

  4. 深入探讨Java类加载机制

    一.前言 毕业至今,已经三年光景,平时基本接触不到关于类加载器的技术(工作上),相信很多同行在开始工作后很长一段时间,对于类的加载机制都没有深入的了解过,之前偶然的机会接触了相关的知识,感觉挺有意思, ...

  5. Java类加载机制深度分析

    转自:http://my.oschina.net/xianggao/blog/70826 参考:http://www.ibm.com/developerworks/cn/java/j-lo-class ...

  6. 两道面试题,带你解析Java类加载机制

    文章首发于[博客园-陈树义],点击跳转到原文<两道面试题,带你解析Java类加载机制> 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Gr ...

  7. 【转】两道面试题,带你解析Java类加载机制(类初始化方法 和 对象初始化方法)

    本文转自 https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html 关键语句 我们只知道有一个构造方法,但实际上Ja ...

  8. Java类加载机制及自定义加载器

    转载:https://www.cnblogs.com/gdpuzxs/p/7044963.html Java类加载机制及自定义加载器 一:ClassLoader类加载器,主要的作用是将class文件加 ...

  9. 带你解析Java类加载机制

      目录 Java类加载机制的七个阶段 加载 验证 准备(重点) 解析 初始化(重点) 使用 卸载 实战分析 方法论 树义有话说 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如 ...

  10. 深入理解和探究Java类加载机制

    深入理解和探究Java类加载机制---- 1.java.lang.ClassLoader类介绍 java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字 ...

随机推荐

  1. Web测试经典bug、安全性测试

    典型BUG 表格的排序.翻页.添加.删除的联合测试 输入框的长度检查 数据库表中如果指定utf8长度为150,则可以输入150个中文或英文字母等 (有时候界面判断失误,却只能输入50个汉字) 数据添加 ...

  2. JDK1.7之前的Bug之静态代码块

    程序的主入口是main方法,但是在jdk1.7之前,可以没有main方法也一样能运行,很是不可思议,到底是什么原因呢?,大家都知道在类中定义了静态代码块的话,是首先执行代码块里的语句的,如果把静态代码 ...

  3. Webpack 打包优化之体积篇

    谈及如今欣欣向荣的前端圈,不仅有各类框架百花齐放,如Vue, React, Angular等等,就打包工具而言,发展也是如火如荼,百家争鸣:从早期的王者Browserify, Grunt,到后来赢得宝 ...

  4. windows-android-appium环境搭建

    一.安装jdk 安装jdk1.7以上版本,会生成一个jdk目录,和单独的jre目录(注意:不是jdk里面的jre,时安装过程中设置的那个jre路径)安装完成后并配置环境变量 在系统环境变量中,新建:J ...

  5. Oracle数据库之表与表数据操作

    一.SQL语言 SQL语言分为四种,分别是:数据定义语言(DDL).数据操纵语言(DCL).事务控制语言(TCL).数据控制语言(DML). 1.1 数据定义语言(DDL) 建立.修改.删除数据库对象 ...

  6. 乔悟空-CTF-i春秋-Web-Backdoor

    2020.09.05 每次遇到不会的,想两分钟就放弃了,直接奔wp,一看wp发现,wc,就这?我怎么没想到--心里想着下道题一定自己想,不看wp,然后周而复始

  7. [程序员代码面试指南]递归和动态规划-换钱的方法数(DP,完全背包)

    题目描述 给定arr,arr中所有的值都为正数且不重复.每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim,求组成aim的方法数. 解题思路 完全背包 和"求换钱的 ...

  8. Linux实战(5):Centos8安装python

    Centos8正式版已经发布了,已经尝鲜的小伙伴们会发现与其他Linux发行版不同,CentOS 8默认不安装Python.接下来的操作指导大家如何安装python3. 转自链接 安装python3 ...

  9. 【知识分享】Navicat Premium for Mac的破解教程

    转自Navicat Premium for Mac v12.0.22.0 破解教程,macOS上手动破解,无需补丁,无毒下载了Navicat,没有注册码,突然发现了这篇破解教程,竟爱不释手,顾Copy ...

  10. Prometheus-Alertmanager告警对接到企业微信

    之前写过将Prometheus的监控告警信息通过Alertmanager推送到钉钉群. 最近转移了阵地,需要将Prometheus监控告警信息推送到企业微信群,经过两天的摸索,以及查了网上的一些资料, ...