任何程序都需要加载到内存才能与CPU进行交流

同理, 字节码.class文件同样需要加载到内存中,才可以实例化类

ClassLoader的使命就是提前加载.class 类文件到内存中

在加载类时,使用的是Parents Delegation Model(溯源委派加载模型)

Java的类加载器是一个运行时核心基础设施模块,主要是在启动之初进行类的加载、链接、初始化

第一步,Load阶段

读取类文件产生二进制流,并转为特定数据结构,初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class实例

第二步,Link阶段

包括验证、准备、解析三个步骤

  • 验证是更详细的校验,比如final是否合规、类型是否正确、静态变量是否合理等
  • 准备阶段是为静态变量分配内存,并设定默认值,解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局

第三步,Init 阶段

执行类构造器 方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值

类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。

在这个过程中,JVM会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。

某些类在使用时,也可以按需由类加载器进行加载。

全小写的class是关键字,用来定义类

而首字母大写的Class,它是所有class的类

这句话理解起来有难度,类已经是现实世界中某种事物的抽象,为什么这个抽象还是另外一个类Class的对象?

示例代码如下:









● 第1处说明:

Class类下的newInstance()在JDK9中已经置为过时,使用getDeclaredConstructor().newInstance()的方式

着重说明一下new与newInstance的区别

  • new是强类型校验,可以调用任何构造方法,在使用new操作的时候,这个类可以没有被加载过
  • 而Class类下的newInstance是弱类型,只能调用无参构造方法
    • 如果没有默认构造方法,就拋出InstantiationException异常;
    • 如果此构造方法没有权限访问,则拋 IllegalAccessException异常

Java 通过类加载器把类的实现与类的定义进行解耦,所以是实现面向接口编程、依赖倒置的必然选择。

● 第2处说明:

可以使用类似的方式获取其他声明,如注解、方法等

● 第3处说明: private 成员在类外是否可以修改?

通过setccessible(true),即可使用Class类的set方法修改其值

如果没有这一步,则抛出如下异常:

类加载器

类加载器是如何定位具体的类文件并读取的呢?

在类加载器家族中存在着类似人类社会的权力等级制度

  • 最高层的Bootstrap

    在JVM启动时创建的,通常由与操作系统相关的本地代码实现,是最根基的类加载器,负责装载最核心的Java类,比如Object、System、 String ,Java运行时的rt.jar等jar包
  • JDK9的Platform ClassLoader

    负责加载<JAVA_HOME>\lib\ext目录中的,或者java.ext.dirs系统变量指定的路径中的所以类库;

    加载一些扩展的系统类,比如XML、加密、压缩相关的功能类等;

    JDK9之前是Extension ClassLoader.
  • 第三层 Application ClassLoader

    应用类加载器,主要是加载用户定义的CLASSPATH路径下的类

第二、三层类加载器为Java语言实现,用户也可以自定义类加载器

查看本地类加载器的方式如下:



在JDK8环境中,执行结果如下



AppClassLoader的Parent为Bootstrap,它是通过C/C++实现的,并不存在于JVM体系内,所以输出为 null



低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类

如果低层次的类加载器想加载一个未知类,要非常礼貌地向上逐级询问:“请问,这个类已经加载了吗?”

被询问的高层次类加载器会自问两个问题

  • 我是否已加载过此类
  • 如果没有,是否可以加载此类

只有当所有高层次类加载器在两个问题的答案均为“否”时,才可以让当前类加载器加载这个未知类

左侧绿色箭头向上逐级询问是否已加载此类,直至Bootstrap ClassLoader,然后向下逐级尝试是否能够加载此类,如果都加载不了,则通知发起加载请求的当前类加载器,准予加载

在右侧的三个小标签里,列举了此层类加载器主要加载的代表性类库,事实上不止于此

通过如下代码可以查看Bootstrap 所有已加载类库



执行结果

Bootstrap加载的路径可以追加,不建议修改或删除原有加载路径

在JVM中增加如下启动参数,则能通过Class.forName正常读取到指定类,说明此参数可以增加Bootstrap的类加载路径:

-Xbootclasspath/a:/Users/sss/book/ easyCoding/byJdk11/src

如果想在启动时观察加载了哪个jar包中的哪个类,可以增加

-XX:+TraceClassLoading

此参数在解决类冲突时非常实用,毕竟不同的JVM环境对于加载类的顺序并非是一致的

有时想观察特定类的加载上下文,由于加载的类数量众多,调试时很难捕捉到指定类的加载过程,这时可以使用条件断点功能

比如,想查看HashMap的加载过程,在loadClass处打个断点,并且在condition框内输入如图

JVM如何确立每个类在JVM的唯一性

类的全限定名和加载这个类的类加载器的ID

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

  • 隔离加载类

    在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境

    比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包
  • 修改类加载方式

    类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
  • 扩展加载源

    比如从数据库、网络,甚至是电视机机顶盒进行加载
  • 防止源码泄露

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

实现自定义类加载器的步骤

  • 继承ClassLoader
  • 重写findClass()方法
  • 调用defineClass()方法

一个简单的类加载器实现的示例代码如下

由于中间件一般都有自己的依赖jar包,在同一个工程内引用多个框架时,往往被迫进行类的仲裁

按某种规则jar包的版本被统一指定, 导致某些类存在包路径、类名相同的情况,就会引起类冲突,导致应用程序出现异常

主流的容器类框架都会自定义类加载器,实现不同中间件之间的类隔离,有效避免了类冲突。

1 加载的定位

“加载”是“类加载”(Class Loading)过程的第一步

1.1 加载的过程

在加载的过程中,JVM主要做3件事情

  • 通过一个类的全限定名来获取定义此类的二进制字节流(class文件)

    在程序运行过程中,当要访问一个类时,若发现这个类尚未被加载,并满足类初始化的条件时,就根据要被初始化的这个类的全限定名找到该类的二进制字节流,开始加载过程
  • 将这个字节流的静态存储结构转化为方法区的运行时数据结构
  • 在内存中创建一个该类的java.lang.Class对象,作为方法区该类的各种数据的访问入口

程序在运行中所有对该类的访问都通过这个类对象,也就是这个Class对象是提供给外界访问该类的接口

1.2 加载源

JVM规范对于加载过程给予了较大的宽松度.一般二进制字节流都从已经编译好的本地class文件中读取,此外还可以从以下地方读取

  • zip包

    Jar、War、Ear等
  • 其它文件生成

    由JSP文件中生成对应的Class类.
  • 数据库中

    将二进制字节流存储至数据库中,然后在加载时从数据库中读取.有些中间件会这么做,用来实现代码在集群间分发
  • 网络

    从网络中获取二进制字节流.典型就是Applet.
  • 运行时计算生成

    动态代理技术,用PRoxyGenerator.generateProxyClass为特定接口生成形式为"*$Proxy"的代理类的二进制字节流.

1.3 类和数组加载过程的区别

数组也有类型,称为“数组类型”.如:

String[] str = new String[10];

这个数组的数组类型是Ljava.lang.String,而String只是这个数组的元素类型

当程序在运行过程中遇到new关键字创建一个数组时,由JVM直接创建数组类,再由类加载器创建数组中的元素类型.

而普通类的加载由类加载器创建.既可以使用系统提供的引导类加载器,也可以由用户自定义的类加载器完成(即重写一个类加载器的loadClass()方法)

1.4 加载过程的注意点

  • JVM规范并未给出类在方法区中存放的数据结构

    类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,虚拟机规范并没有指定
  • JVM规范并没有指定Class对象存放的位置

    在二进制字节流以特定格式存储在方法区后,JVM会创建一个java.lang.Class类的对象,作为本类的外部访问接口

    既然是对象就应该存放在Java堆中,不过JVM规范并没有给出限制,不同的虚拟机根据自己的需求存放这个对象

    HotSpot将Class对象存放在方法区.
  • 加载阶段和链接阶段是交叉的

    类加载的过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制.也就是说,类加载过程中,必须按照如下顺序开始:

加载 -> 链接 -> 初始化

但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉

2 验证

验证阶段比较耗时,它非常重要但不一定必要(因为对程序运行期没有影响),如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none参数关闭,以缩短类加载时间

2.1 验证的目的

保证二进制字节流中的信息符合虚拟机规范,并没有安全问题

2.2 验证的必要性

虽然Java语言是一门安全的语言,它能确保程序猿无法访问数组边界以外的内存、避免让一个对象转换成任意类型、避免跳转到不存在的代码行.也就是说,Java语言的安全性是通过编译器来保证的.

但是我们知道,编译器和虚拟机是两个独立的东西,虚拟机只认二进制字节流,它不会管所获得的二进制字节流是哪来的,当然,如果是编译器给它的,那么就相对安全,但如果是从其它途径获得的,那么无法确保该二进制字节流是安全的。

通过上文可知,虚拟机规范中没有限制二进制字节流的来源,在字节码层面上,上述Java代码无法做到的都是可以实现的,至少语义上是可以表达出来的,为了防止字节流中有安全问题,需要验证!

2.3 验证的过程

  • 文件格式验证

    验证字节流是否符合Class文件格式的规范,并且能被当前的虚拟机处理.

    本验证阶段是基于二进制字节流进行的,只有通过本阶段验证,才被允许存到方法区

    后面的三个验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流.

通过上文可知,加载开始前,二进制字节流还没进方法区,而加载完成后,二进制字节流已经存入方法区

而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区

也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储至方法区中,继而开始下阶段的验证和创建Class对象等操作

这个过程印证了:加载和验证是交叉进行的

  • 元数据验证

    对字节码描述信息进行语义分析,确保符合Java语法规范.
  • 字节码验证

    本阶段是验证过程的最复杂的一个阶段.

    本阶段对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件.

    字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不会做出危害虚拟机的事,一个类方法体的字节码没有通过字节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全
  • 符号引用验证

    发生在JVM将符号引用转化为直接引用的时候,这个转化动作发生在解析阶段,对类自身以外的信息进行匹配校验,确保解析能正常执行.

3 准备

完成两件事情

  • 为已在方法区中的类的静态成员变量分配内存
  • 为静态成员变量设置初始值

    初始值为0、false、null等

public static final int value = 123;

准备阶段后 a 的值为 0,而不是 123,要在初始化之后才变为 123,但若被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性中,在准备阶段就将constantValue的值赋给该字段(此处将value赋为123).

4 解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程.

5 初始化

真正开始执行类中定义的Java程序代码(或者说是字节码)

初始化阶段就是执行类构造器clinit()的过程.

clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。

5.1 初始化过程的注意点

  • clinit()方法是IDE自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,IDE收集的顺序是由语句在源文件中出现的顺序所决定的.
  • 静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问.
public class Test {
static {
i=0;
System.out.println(i);//编译失败:"非法向前引用"
}
static int i = 1;
}
  • 实例构造器init()需要显示调用父类构造函数,而类的clinit()不需要调用父类的类构造函数,虚拟机会确保子类的clinit()方法执行前已经执行完毕父类的clinit()方法.因此在JVM中第一个被执行的clinit()方法的类肯定是java.lang.Object.
  • 如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会为此类生成clinit()方法.
  • 接口也需要通过clinit()方法为接口中定义的静态成员变量显示初始化。
  • 接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成clinit()方法.不同的是,执行接口的clinit()方法不需要先执行父接口的clinit()方法.只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()方法.
  • 虚拟机会保证在多线程环境中一个类的clinit()方法别正确地加锁,同步.当多条线程同时去初始化一个类时,只会有一个线程去执行该类的clinit()方法,其它线程都被阻塞等待,直到活动线程执行clinit()方法完毕.

其他线程虽会被阻塞,只要有一个clinit()方法执行完,其它线程唤醒后不会再进入clinit()方法.同一个类加载器下,一个类型只会初始化一次.

参考

  • 《码到成功》

  • 《深入理解Java虚拟机第二版》

JVM实战---类加载的过程的更多相关文章

  1. JVM虚拟机 类加载过程与类加载器

    目录 前言 类的生命周期 类加载过程 加载 连接 验证 准备 解析 初始化 类加载器 三大类加载器 双亲委派模型 概念 为什么要使用双亲委派模型 源码分析 反双亲委派模型 参考 前言 类装载器子系统是 ...

  2. jvm(1)类加载(一)(加载过程,双亲加载)

    JVM类加载器机制与类加载过程 jvm虚拟机的种类: Hotspot(Oracle)(基本上都是在说这个) J9, JikesRVM(IBM) Zulu, Zing (Azul) Launcher是一 ...

  3. JVM的类加载过程以及双亲委派模型详解

    JVM的类加载过程以及双亲委派模型详解 这篇文章主要介绍了JVM的类加载过程以及双亲委派模型详解,类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象 ...

  4. 类文件的结构、JVM 的类加载过程、类加载机制、类加载器、双亲委派模型

    一.类文件的结构 我们都知道,各种不同平台的虚拟机,都支持 "字节码 Byte Code" 这种程序存储格式,这构成了 Java 平台无关性的基石.甚至现在平台无关性也开始演变出 ...

  5. JVM的类加载过程

    每日一句 人到情多情转薄,而今真个不多情. 每日一句 The frog in the well knows nothing of the great ocean. 井底之蛙,不知大海. JVM 的类加 ...

  6. JVM(二)类加载的时机及其过程

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的的整个生命周期包括: 加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化 ...

  7. 【JVM】JVM之类加载器

    一.前言 首先,小小测试,看是否已经掌握了JVM类加载的过程 1.1.测试一 class Singleton { private static Singleton sin = new Singleto ...

  8. JVM的类加载机制

    虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 类加载的过程: 包括加载.链接(含验证.准备 ...

  9. JVM学习一:JVM之类加载器概况

    18年转眼就3月份都快结束了,也就是说一个季度就结束了:而我也因为年前笔记本坏了,今天刚修好了,那么也应该继续学习和博客之旅了.今年的博客之旅,从JVM开始学起,下面我们就言归正传,进入正题. 一.J ...

随机推荐

  1. 前端Web浏览器基于Flash如何实时播放监控视频画面(前言)之流程介绍

    [关键字:前端浏览器如何播放RTSP流画面.前端浏览器如何播放RTMP流画面] 本片文章只是起到抛砖引玉的作用,能从头到尾走通就行,并不做深入研究.为了让文章通俗易懂,尽量使用白话描述. 考虑到视频延 ...

  2. 记一次linux通过jstack定位CPU使用过高问题或排查线上死锁问题

    一.java定位进程 在服务器中终端输入命令:top 可以看到进程ID,为5421的cpu这列100多了. 记下这个数字:5421 二.定位问题进程对应的线程 然后在服务器中终端输入命令:top -H ...

  3. HDU 1074:Doing Homework(状压DP)

    http://acm.hdu.edu.cn/showproblem.php?pid=1074 Doing Homework Problem Description Ignatius has just ...

  4. scrapy基础知识之 RedisCrawlSpider:

    这个RedisCrawlSpider类爬虫继承了RedisCrawlSpider,能够支持分布式的抓取.因为采用的是crawlSpider,所以需要遵守Rule规则,以及callback不能写pars ...

  5. SpringMVC——MVC

    一.了解MVC mvc这种设计模式,分为三个基本部分:模型(Model).视图(View)和控制器(Controller),不光运用于Web领域,而且也能用于非Web领域:可以特指一种表现层设计模式, ...

  6. 接口文档注解:@ApiOperation

    @ApiOperation不是spring自带的注解是swagger里的 com.wordnik.swagger.annotations.ApiOperation; @ApiOperation和@Ap ...

  7. vue项目缓存最佳实践

    需求 在开发vue的项目中有遇到了这样一个需求:一个视频列表页面,展示视频名称和是否收藏,点击进去某一项观看,可以收藏或者取消收藏,返回的时候需要记住列表页面的页码等状态,同时这条视频的收藏状态也需要 ...

  8. Facebook Libra - 第一笔交易

    第一笔交易 假定 运行的是Linux或者macOS系统 网络连接正常 git已安装 macOS中安装了Homebrew Linux中安装了yum或者apt-get 提交一笔交易的步骤 克隆并构建Lib ...

  9. jekyll搭建个人博客1

    目录 配置环境 使用模板 配置环境 简介 jekyll是一个简单的免费的,生成静态网页的工具,不需要数据库支持.但是可以配合第三方服务,例如Disqus.最关键的是jekyll可以免费部署在Githu ...

  10. Linux系统-CENTOS7使用笔记

    复制文件夹下的所有文件到另一个文件夹下 cp ~/dirname/* ~/otherdirname 解压rar文件 PS:在liunx下原本是不支持rar文件的,需要安装liunx下的winrar版本 ...