虚拟机类的加载机制

概述

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类的加载机制。

类加载的时机

JVM 会在程序第一次主动引用类的时候,加载该类,被动引用时并不会引发类加载的操作。也就是说,JVM 并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。那么什么是主动引用,什么是被动引用呢?

  • 主动引用

    • 遇到 new、getstatic、putstatic、invokestatic 字节码指令,例如:

      • 使用 new 实例化对象;
      • 读取或设置一个类的 static 字段(被 final 修饰的除外);
      • 调用类的静态方法。
    • 对类进行反射调用;
    • 初始化一个类时,其父类还没初始化(需先初始化父类);
      • 这点类与接口具有不同的表现,接口初始化时,不要求其父接口完成初始化,只有真正使用父接口时才初始化,如引用父接口中定义的常量。
    • 虚拟机启动,先初始化包含 main() 函数的主类;
    • JDK 1.7 动态语言支持:一个 java.lang.invoke.MethodHandle 的解析结果为 REF_getStaticREF_putStaticREF_invokeStatic
  • 被动引用
    • 通过子类引用父类静态字段,不会导致子类初始化;
    • Array[] arr = new Array[10]; 不会触发 Array 类初始化;
    • static final VAR 在编译阶段会存入调用类的常量池,通过 ClassName.VAR 引用不会触发 ClassName 初始化。

也就是说,只有发生主动引用所列出的 5 种情况,一个类才会被加载到内存中,也就是说类的加载是 lazy-load 的,不到必要时刻是不会提前加载的,毕竟如果将程序运行中永远用不到的类加载进内存,会占用方法区中的内存,浪费系统资源。


类的生命周期


类的加载过程

加载

加载(Loading)阶段,虚拟机需要完成以下三件事:

  • 通过一个类的全限定名来获取定义这个类对应的二进制字节流
  • 将这个类的二进制字节流所代表的静态存储结构转换为方法区的运行时数据结构
  • 在Java堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区这些数据的访问入口。

分类

  • 非数组类

    • 系统提供的引导类加载器
    • 用户自定义的类加载器
  • 数组类
    • 不通过类加载器,由 Java 虚拟机直接创建
    • 创建动作由 newarray 指令触发,new 实际上触发了 [L全类名 对象的初始化
    • 规则
      • 数组元素是引用类型

        • 加载:递归加载其组件
        • 可见性:与引用类型一致
      • 数组元素是非引用类型
        • 加载:与引导类加载器关联
        • 可见性:public

类的显式加载和隐式加载

  • 显示加载:

    • 调用 ClassLoader#loadClass(className)Class.forName(className)
    • 两种显示加载 .class 文件的区别:
      • Class.forName(className) 加载 class 的同时会初始化静态域,ClassLoader#loadClass(className) 不会初始化静态域;
      • Class.forName 借助当前调用者的 class 的 ClassLoader 完成 class 的加载。
  • 隐式加载:
    • new 类对象;
    • 使用类的静态域;
    • 创建子类对象;
    • 使用子类的静态域;
    • 其他的隐式加载,在 JVM 启动时:
      • BootStrapLoader 会加载一些 JVM 自身运行所需的 Class;
      • ExtClassLoader 会加载指定目录下一些特殊的 Class;
      • AppClassLoader 会加载 classpath 路径下的 Class,以及 main 函数所在的类的 Class 文件。

验证

目的: 确保 .class 文件中的字节流信息符合虚拟机的要求。

4 个验证过程:

  • 文件格式验证:是否符合 Class 文件格式规范,验证文件开头 4 个字节是不是 “魔数” 0xCAFEBABE

    • 魔数:每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的class文件。
  • 元数据验证:保证字节码描述信息符号 Java 规范(语义分析)
  • 字节码验证:程序语义、逻辑是否正确(通过数据流、控制流分析)
  • 符号引用验证:对类自身以外的信息(常量池中的符号引用)进行匹配性校验

这个操作虽然重要,但不是必要的,可以通过 -Xverify:none 关掉。

准备

描述: 为 static 变量在方法区分配内存。

  • static 变量准备后的初始值:

    • public static int value = 123;

      • 准备后为 0,value 的赋值指令 putstatic 会被放在 <clinit>() 方法中,<clinit>()方法会在初始化时执行,也就是说,value 变量只有在初始化后才等于 123。
    • public static final int value = 123;
      • 准备后为 123,因为被 static final 赋值之后 value 就不能再修改了,所以在这里进行了赋值之后,之后不可能再出现赋值操作,所以可以直接在准备阶段就把 value 的值初始化好。

解析

描述: 将常量池中的 “符号引用” 替换为 “直接引用”。

在此之前,常量池中的引用是不一定存在的,解析过之后,可以保证常量池中的引用在内存中一定存在。

什么是 “符号引用” 和 “直接引用” ?

  • 符号引用:以一组符号描述所引用的对象(如对象的全类名),引用的目标不一定存在于内存中。
  • 直接引用:直接指向被引用目标在内存中的位置的指针等,也就是说,引用的目标一定存在于内存中。

初始化

描述: 执行类构造器 () 方法的过程。

  • <clinit>() 方法

    • 包含的内容:

      • 所有 static 的赋值操作;
      • static 块中的语句;
    • <clinit>() 方法中的语句顺序:
      • 基本按照语句在源文件中出现的顺序排列;
      • 静态语句块只能访问定义在它前面的变量,定义在它后面的变量,可以赋值,但不能访问。
    • <init>() 的不同:
      • 不需要显示调用父类的 <clinit>() 方法;
      • 虚拟机保证在子类的 <clinit>() 方法执行前,父类的 <clinit>() 方法一定执行完毕。也就是说,父类的 static 块和 static 字段的赋值操作是要先于子类的。
    • 接口与类的不同:
      • 执行子接口的 <clinit>() 方法前不需要先执行父接口的 <clinit>() 方法(除非用到了父接口中定义的 public static final 变量);
    • 执行过程中加锁:
      • 同一时刻只能有一个线程在执行 <clinit>() 方法,因为虚拟机要保证在同一个类加载器下,一个类只被加载一次。
    • 非必要性:
      • 一个类如果没有任何 static 的内容就不需要执行 () 方法。

本小节的补充:<clinit><init> 方法

概述

在编译生成class文件时,会自动产生两个方法,一个是类的初始化方法<clinit>, 另一个是实例的初始化方法<init>

<clinit>:在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行

<init>:在实例创建出来的时候调用,包括调用new操作符;调用Class或java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;通过java.io.ObjectInputStream类的getObject()方法反序列化。

<clinit>方法

先理解 类初始化阶段 的含义: 该阶段负责为类变量赋予正确的初始值, 是一个类或接口被首次使用前的最后一项工作

<clinit>方法的执行时期: 类初始化阶段(该方法只能被jvm调用, 专门承担类变量的初始化工作)

<clinit>方法的内容: 所有的类变量初始化语句和类型的静态初始化器

类的初始化时机: 即在java代码中首次主动使用的时候, 包含以下情形:

  • (首次)创建某个类的新实例时--new, 反射, 克隆 或 反序列化;
  • (首次)调用某个类的静态方法时;
  • (首次)使用某个类或接口的静态字段或对该字段(final 字段除外)赋值时;
  • (首次)调用java的某些反射方法时;
  • (首次)初始化某个类的子类时;
  • (首次)在虚拟机启动时某个含有 main() 方法的那个启动类

注意: 并非所有的类都会拥有一个方法, 满足下列条件之一的类不会拥有方法:

  1. 该类既没有声明任何类变量,也没有静态初始化语句;

  2. 该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化;

  3. 该类仅包含静态 final 变量的类变量初始化语句,并且类变量初始化语句是编译时常量表达式;

<init>方法

<init>方法的执行时期: 对象的初始化阶段

实例化一个类的四种途径:

  1. 调用 new 操作符
  2. 调用 Class 或 java.lang.reflect.Constructor 对象的newInstance()方法
  3. 调用任何现有对象的clone()方法
  4. 通过 java.io.ObjectInputStream 类的 getObject() 方法反序列化

类加载器

类加载过程中的“通过一个类的全限定名来获取描述这个类的二进制字节流”这个动作是放在Java虚拟机的外部来实现的,以便于让应用程序自己来决定如何去获取所需要的类,实现这个动作的代码模块被称为“类加载器”

类加载器虽然只用于实现类的加载动作,但是它的作用却远远不限于此,比较两个类是否“相等”,不仅仅要确认这两个类是否来源于同一个class文件,还需要加载这两个类的类加载器相同。

如何判断两个类 “相等”

  • “相等” 的要求

    • 同一个 .class 文件
    • 被同一个虚拟机加载
    • 被同一个类加载器加载
  • 判断 “相等” 的方法
    • instanceof 关键字
    • Class 对象中的方法:
      • equals()
      • isInstance()
      • isAssignableFrom()

类加载器的分类

站在虚拟机的角度,只存在两种类加载器:

启动类加载器(Bootstrap ClassLoader),使用C++实现,是虚拟机的一部分

其他类加载器,由Java语言实现,独立于虚拟机之外的,全部继承自抽象类 java.lang.ClassLoader

从开发人员的角度,类加载器可以划分得更细致一些:

  • 启动类加载器(Bootstrap):负责将存放在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。

    • <JAVA_HOME>/lib
    • -Xbootclasspath 参数指定的路径
  • 扩展类加载器(Extension):负责加载 <JAVA_HOME>\lib\ext 目录下的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
    • <JAVA_HOME>/lib/ext
    • java.ext.dirs 系统变量指定的路径
  • 应用程序类加载器(Application):负责加载用户类路径(ClassPath)上所指定的类库,一般情况下这个就是程序中默认的类加载器。
    • -classpath 参数

以上加载器互相配合来加载我们自己的应用程序,如果有必要,我们还可以加入自己定义的加载器。这些加载器之间的关系一般如下图示:

双亲委派模型

类加载器的双亲委派模型(Parent Delegation Model):要求除了顶层的启动类加载器外,其余的类加载器都必须有自己的父类加载器。(注意!这里类加载器之间的父子关系一般不会以继承(Inheritance)来实现,而是使用组合(Composition)来复用父加载器的代码)。这种模型被广泛使用于几乎所有的Java程序中,但是它并不是一个强制性的约束,只是Java设计者推荐给开发者使用的一种类加载器实现方式。

  • 工作过程

    • 当前类加载器收到类加载的请求后,先不自己尝试加载类,而是先将请求委派给父类加载器。因此,所有的类加载请求,都会先被传送到启动类加载器。
    • 只有当父类加载器加载失败时,当前类加载器才会尝试自己去自己负责的区域加载
  • 实现
    • 检查该类是否已经被加载
    • 将类加载请求委派给父类
      • 如果父类加载器为 null,默认使用启动类加载器
      • parent.loadClass(name, false)
    • 当父类加载器加载失败时
      • catch ClassNotFoundException 但不做任何处理
      • 调用自己的 findClass() 去加载
        • 我们在实现自己的类加载器时只需要 extends ClassLoader,然后重写 findClass() 方法而不是 loadClass() 方法,这样就不用重写 loadClass() 中的双亲委派机制了
  • 优点
    • 自己写的类库同名类不会覆盖类库的类
    • java类随着它的类加载器一起具备了一种带有优先层级的层次关系,保证了Java程序的稳定运行。

深入理解java虚拟机笔记Chapter7的更多相关文章

  1. Java内存区域与内存溢出异常——深入理解Java虚拟机 笔记一

    Java内存区域 对比与C和C++,Java程序员不需要时时刻刻在意对象的创建和删除过程造成的内存溢出.内存泄露等问题,Java虚拟机很好地帮助我们解决了内存管理的问题,但深入理解Java内存区域,有 ...

  2. 深入理解java虚拟机笔记Chapter12

    (本节笔记的线程收录在线程/并发相关的笔记中,未在此处提及) Java内存模型 Java 内存模型主要由以下三部分构成:1 个主内存.n 个线程.n 个工作内存(与线程一一对应) 主内存与工作内存 J ...

  3. 深入理解Java虚拟机笔记

    1. Java虚拟机所管理的内存 2. 对象创建过程 3. GC收集 4. HotSpot算法的实现 5. 垃圾收集器 6. 对象分配内存与回收细节 7. 类文件结构 8. 虚拟机类加载机制 9.类加 ...

  4. 深入理解java虚拟机笔记之一

    Java的技术体系主要有支撑java程序运行的虚拟机,提供各开发领域接口支持Java API,java编程语言及许多第三方java框架( 如Spring,Structs等)构成. 可以把Java程序设 ...

  5. 深入理解Java虚拟机笔记——虚拟机类加载机制

    目录 概述 动态加载和动态连接 类加载的时机 类的生命周期 被动引用 例子一(调用子类继承父类的字段) 例子二(数组) 例子三(静态常量) 类加载的过程 加载 验证 准备 解析 符号引用 直接引用 初 ...

  6. 【转载】深入理解Java虚拟机笔记---运行时栈帧结构

    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法的局部变量表,操作 ...

  7. 深入理解java虚拟机笔记Chapter8

    运行时栈帧结构 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法 ...

  8. 深入理解java虚拟机笔记Chapter2

    java虚拟机运行时数据区 首先获取一个直观的认识: 程序计数器 线程私有.各条线程之间计数器互不影响,独立存储. 当前线程所执行的字节码行号指示器.字节码解释器工作时通过改变这个计数器值选取下一条需 ...

  9. 类文件结构——深入理解Java虚拟机 笔记三

    在之前的笔记中记录过,Java程序变成可执行文件的步骤是:源代码-->经过编译变成class文件-->经过JVM虚拟机变成可执行的二进制文件.因此,为了对JVM执行程序的过程有一个好的了解 ...

随机推荐

  1. drozer源码学习:app

    源码下载:https://github.com/mwrlabs/drozer:模块的源码位于src.drozer.modules,根据模块名来划分文件夹: app.auxiliary.exploit. ...

  2. POJ2308连连看dfs+bfs+优化

    DFS+BFS+MAP+剪枝 题意:       就是给你一个10*10的连连看状态,然后问你最后能不能全部消没? 思路:      首先要明确这是一个搜索题目,还有就是关键的一点就是连连看这个游戏是 ...

  3. c/c++ 指针数组 和 数组指针

    看这个标题都要晕了,我们不妨把他拆开来理解,比较容易 指针数组:对象是一个数组,数组元素的类型是指针 指针数组的定义方式: 类型名 *数组名[数组长度]; 如: int *p[8]; 数组指针:对象是 ...

  4. Ubuntu Linux 学习篇 配置DNS服务器

    BIND9 DNS(Domain Name Server,域名服务器)是进行域名(domain name)和与之相对应的IP地址 (IP address)转换的服务器.DNS中保存了一张域名(doma ...

  5. 【maven】mvn不是内部命令 也不是可运行的程序

    按解压.配置环境变量,重启cmd,还是出现这个问题 使用java -version确定是不是安装了jdk.因为maven是java开发,需要依赖jdk 将系统变量中Path的%MAVEM_HOME%\ ...

  6. 剖析XAML语言

    这节剖析一下XAML(读作:zaml)--这一WPF中的UI设计语言. XAML 在wpf中,UI部分使用xaml语言来编写,xaml语言是由xml语言派生而来的语言,所以在xaml中我们可以看到很多 ...

  7. 理解微信小程序的双线程模型

    有过微信小程序开发经验的朋友应该都知道"双线程模型"这个概念,本文简单梳理一下双线程模型的一些科普知识,学识浅薄,若有错误欢迎指正. 我以前就职于「小程序·云开发」团队,在对外的一 ...

  8. SE_Work0_回顾与展望

    项目 内容 课程:北航-2020-春-软件工程 博客园班级博客 要求:阅读推荐博客并回答问题 热身作业阅读部分要求 我在这个课程的目标是 提升团队管理及合作能力,开发一项满意的工程项目 这个作业在哪个 ...

  9. 基于虹软人脸识别,实现RTMP直播推流追踪视频中所有人脸信息(C#)

    前言 大家应该都知道几个很常见的例子,比如在张学友的演唱会,在安检通道检票时,通过人像识别系统成功识别捉了好多在逃人员,被称为逃犯克星:人行横道不遵守交通规则闯红灯的路人被人脸识别系统抓拍放在大屏上以 ...

  10. ACM基础板子

    新生赛以后就正式成为一名acmer啦 ~虽然没有打过比赛呜呜呜 要好好学算法,拿一个牌牌嘛~ 这里就记录算法学习情况,也怕自己偷懒,学一个就记录,看看长时间拖更就是在摸鱼,摸鱼和鸽子都是本质 ,加油! ...