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

1. 什么是类的加载?

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

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

加载.class文件的方式
– 从本地系统中直接加载
– 通过网络下载.class文件
– 从zip,jar等归档文件中加载.class文件
– 从专有数据库中提取.class文件
– 将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 包和类路径。
  • 用户类加载器:用户自定义的加载器,以类加载器为父类。

一个简单的小栗子:

public static void main(String[] args) {
ClassLoader loader = ClassLoader.getSystemClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}

输出结果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
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 。

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先判断该类型是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
// 如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法 native Class findBootstrapClass(String name)
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 stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

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

5. 自定义类加载器

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

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

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

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

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

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

public class MyClassLoader extends ClassLoader {
public MyClassLoader(){} public MyClassLoader(ClassLoader parent){
super(parent);
} protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = new File("D:\\ClassLoaderTest.class");
try{
byte[] bytes = getClassBytes(file);
//defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
return c;
}
catch (Exception e) {
e.printStackTrace();
} return super.findClass(name);
} private byte[] getClassBytes(File file) throws Exception {
// 这里要读入.class的字节,因此要使用字节流
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024); while (true){
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
} public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
MyClassLoader classLoader = new MyClassLoader();
Class clazz = classLoader.loadClass("com.geekdigging.lesson03.classloader.ClassLoaderTest");
Object obj = clazz.newInstance();
Method helloMethod = clazz.getDeclaredMethod("hello", null);
helloMethod.invoke(obj, null);
}
}

最后打印结果:

我是由 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. Unity技巧集合

    地址:http://blog.csdn.net/stalendp/article/details/17114135 这篇文章将收集unity的相关技巧,会不断地更新内容. 1)保存运行中的状态 uni ...

  2. 当年偶然发现的 Java Bug(JDK 9及之前仍未修复)

    背景 15年在中信银行做持续集成时,由于当时的项目是基于三方采购的 Java 配置开发平台做的,平台自己基于 Ant 插件实现了增量和热部署. 其中有几个项目在持续集成部署时,经常发现 Linux 平 ...

  3. Hive 高阶应用开发示例(一)

    Hive的一些常用的高阶开发 内容    1.开窗函数   2.行转列,列转行,多行转一行,一行转多行   3.分组: 增强型group   4.排序  5.关联 本次的内容: 内容1 和内容2,采用 ...

  4. 用navicat进行身份验证连接出现cannot connect to Mongodb authentication failed

    用navicat进行身份验证连接出现cannot connect to Mongodb authentication failed. 解决办法: 1.打开mongoDB连接 win+r --cmd-- ...

  5. js error 错误处理

    (new) Error([message[, fileName[,lineNumber]]]) 单独定义Error()错误,函数继续进行 当像函数一样使用 Error 时 -- 如果没有 new,它将 ...

  6. [01] C#网络编程的最佳实践

    网络框架的选择 C++语言里面有asio和libuv等网络库, 可以方便的进行各种高效编程. 但是C#里面, 情况不太一样, C#自带的网络API有多种. 例如: Socket TcpStream(同 ...

  7. SpringCloud Gateway高阶之Sentinel限流、熔断

    前言 为什么需要服务熔断和降级?微服务是当前业界的一大趋势,原理就是将单一职责的功能模块独立化为子服务,降低服务间的耦合,服务间互相调用.但是这样也会出现一些问题: 上图中大量微服务互相调用,存在大量 ...

  8. Tomcat源码分析(下载、启动)

    1.下载Tomcat源代码: https://tomcat.apache.org/download-80.cgi 2. 解压以及创建必要目录和配置 解压.新建catalina-home目录,同时将目录 ...

  9. 【API进阶之路】用API打造一条自动化内容生产流水线

    摘要:搞定了内容审核之后,我又把抓取工具.内容审核API.文本摘要生成API串联在一起,从抓到审再到编,建立了一条自动化的内容生产流水线,编辑团队只需要做优质内容的推荐就可以了. 上周,运营部将官网上 ...

  10. IDEA项目左侧目录看不到target

    点击如下图所示图标,然后选择Show Excluded Files后即可出现