第二章 JVM类加载、JVM对象

文章目录

  • 第二章 JVM类加载、JVM对象
  • 一、JVM类加载
    • 1.介绍
    • 2.类加载器
    • 3.类加载机制
    • 4.类加载过程
  • 二、JVM对象
    • 1.对象的内存布局
    • 2.对象的访问定位
    • 3.对象的创建过程
  • 参考链接

初学 JVM,已经尽力在网上寻找最正确的资料,如有纰漏望指正!


一、JVM类加载

1.介绍

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

2.类加载器

类加载器负责加载所有的类,它为所有被载入内存的类生成一个 java.lang.Class 实例对象。一旦一个类被加载到 JVM 中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入 JVM 的类也有一个唯一的标识。在 Java 中,一个类用其全限定类名作为标识。但在 JVM 中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在 pg 的包中有一个名为 Person 的类,被类加载器 ClassLoader 的实例 kl 负责加载,则该 Person 类对应的 Class 对象在 JVM 中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的

JVM 预定义的有三种类加载器,当一个 JVM 启动的时候,Java 开始使用如下三种类加载器

  • 根类加载器/启动类加载器(Bootstrap ClassLoader)
    它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。换句话说,它负责加载 $JAVA_HOME 中 jre/lib/rt.jar 里所有的 class,由 C++ 实现,不是 ClassLoader 子类。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以无法直接通过引用进行操作
  • 扩展类加载器(Extension ClassLoader)
    该加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 DK/jre/lib/ext 目录或者由 java.ext.dirs 系统属性指定的目录中的 jar 包的类。由 Java 语言实现,父类加载器为 null
  • 系统类加载器/应用程序类加载器(System ClassLoader/Application ClassLoader)
    该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。由Java语言实现,父类加载器为ExtClassLoader

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为 JVM 自带的 ClassLoader 只是懂得从本地文件系统加载标准的 java class 文件。因此,如果编写了自己的 ClassLoader,便可以做到如下几点:

  1. 在执行非置信代码之前,自动验证数字签名
  2. 动态地创建符合用户特定需要的定制化构建类
  3. 从特定的场所取得 java class,例如数据库中和网络中

3.类加载机制

  • 全盘负责
    所谓全盘负责,就是当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入
  • 双亲委派
    所谓的双亲委派,就是先让父类加载器试图加载该 Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗地讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载
  • 缓存机制
    缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区中。这就是为什么修改了 Class 后,必须重启 JVM,程序所做的修改才会生效的原因

我们重点关注一下双亲委派机制

双亲委派机制的优势:
采用双亲委派模式的好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类已经加载了该类时,就没有必要让子类加载器再加载一次。其次是考虑到安全因素,保证 Java 核心 API 中定义类型不会被随意替换。假设通过网络传递一个名为 java.lang.Integer 的类,将会通过双亲委派模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,并且该类已经被加载。因此,就不会重新加载网络传递过来的 java.lang.Interger,而是直接返回已加载过的 Integer.class,这样便可以防止核心 API 库被随意替换

4.类加载过程


类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段

加载

查找并加载类的二进制数据。加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

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

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

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

验证

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

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

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

准备

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

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一起分配在 Java 堆中
  • 这里所设置的初始值通常情况下是数据类型默认的零值(如 0、0L、null、false 等),而不是在 Java 代码中被显式赋予的值
  • 如果类字段的字段属性表中存在 ConstValue 属性,即同时被 static 和 final 修饰,那么在准备阶段变量就会被初始化为 ConstValue 属性所指定的值

假设一个类变量的定义为:public static int value = 3

那么变量 value 在准备阶段过后的初始化值为 0,而不是 3,因为这时候尚未开始执行任何 Java 方法,而把 value 赋值为 3 的 putstatic 指令是在程序编译后,存放于类构造器 <clinit>() 方法之中的,所以把 value 赋值为 3 的动作将在初始化阶段才会执行

假设上面的类变量被定义为:public static final int value = 3

编译时 javac 将会为 value 生成 ConstValue 属性,在准备阶段虚拟机就会根据 ConstValue 的设置将 value 赋值为 3。我们可以理解为 static final 常量在编译前就将其结果放入了调用它的类的常量池中

解析

把类中的符号引用转换为直接引用。解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类符号引用进行的。符号引用就是一组符号来描述目标可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄

初始化

初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 java 程序代码(字节码)

在准备阶段,变量已依次经赋过系统要求的初始值(零值)。而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者更直接地说:初始化阶段是执行类构造器 <clinit>() 方法的过程。<clint>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块 static{} 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

初始化步骤

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机,只有当类主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  1. 创建类的实例,也就是 new 一个对象
  2. 访问类或接口的静态变量
  3. 调用类的静态方法
  4. 反射
  5. 初始化一个子类时,会先初始化子类的父类
  6. JVM 启动时标明的启动类,即文件名和类名相同的那个类

注意:
对于一个 final 类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于 “宏变量”,Java 编译器会在编译时直接把这个变量出现的地方替换成它的值。因此,即使程序使用该静态变量,也不会导致该类的初始化。反之,如果 final 类型的静态变量的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化

二、JVM对象

1.对象的内存布局

以 HotSpot VM 为例,对象在内存中的结构可以分为三部分:对象头(header)、实例数据(instance data)、对齐填充(padding)

对象头

对象头的结构大体相似,但不同 JVM 的具体实现使得它们略有差别。一般来说,对象头都会包含标记字、类型指针两部分信息,如果对象是数组,还会额外包含数组长度信息

  • 标记字

    存储对象自身的运行时数据(即状态),包括哈希码、GC 分代年龄、锁状态标志、线程持有锁、偏向线程 id、偏向时间戳等。它们的存储结构类似于 C 语言中的 “位字段”,官方称之为 “Mark Word”(标记字)。“ 标记字” 以 “字” 作为基本的存储单元,即在 32 位虚拟机中,数据长度位 32 bit,而在 64 位虚拟机中,数据长度位 64 bit

    以 32 位虚拟机为例,有固定的 2 bit 用于存储锁标志位,随着锁标志位值不同,其他位存储的内容与位长度也不同。这一点类似于 C 语言中的联合结构(union),且联合的每一个成员都是位字段结构

  • 类型指针

    类型指针即对象指向它的类元数据(class metadata)的指针,虚拟机通过该指针确定这个对象是哪个类的实例。但需要注意的是,并非所有虚拟机实现中都会在对象头包含类型指针,也可以采用其他方式保留对象的类型信息

  • 数组长度

    在 Java 中,数组也属于对象,那么理所当然地需要维护数组长度,该信息放在对象头中

实例数据

实例数据即对象的字段(或称为成员变量)存储的数据信息,包含了从父类继承及自己定义的所有字段。且字段在内存中存储的顺序并不等于类中的定义顺序,它受到虚拟机策略的影响(主要考虑到内存对齐以及使用率的问题)

对齐填充

类似于 C 结构体 struct 的内存对齐,Java 对象的内存位置也需要对齐。我们常用的 HotSpot VM 要求每个对象的起始地址位 8 字节的整数倍,也就是说,若一个对象结束地址非 8 字节整数倍,则需要占位符进行填充以保证对齐

2.对象的访问定位

虚拟机规定,需要通过栈上的 “reference”(引用)来操作具体对象。对于该规定,目前有两种主流的实现方式

  • 通过句柄(handler)实现:该种方式会在堆中划出一块 “句柄池” 内存空间,每个栈上的引用直接指向句柄池中的句柄,而句柄又会维护对象指针和类型指针。使用句柄带来的好处是:栈上的 reference 存储稳定的句柄地址,GC 造成的对象移动只会导致句柄中相应的指向地址改变,而 reference 地址不改变
  • 通过直接指针(direct point)实现:即在对象的对象头中维护类型指针。栈的 reference 指向对象,而对象头中的类型指针指向对象类型数据。使用直接指针的好处是:对象的访问速度快,节省了指针二次寻址的开销

3.对象的创建过程

对象的创建过程要经历以下几个阶段

类加载

  • 检查到 new 指令
  • 虚拟机检查在常量池中是否有该类的符号引用,包括该符号引用代表的类是否已被加载、解析、初始化
  • 没有,则先加载类
  • 有的话,直接创建对象

内存分配

1、内存分配的方式

一个对象所需内存在类加载时便可确定,内存分配方式有两种:

  • 指针碰撞法
    若 java 堆中内存是绝对规整的,所有用过的内存都放到一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把指针指向空闲空间那边挪动一段与对象大小相等的距离
  • 空闲列表法
    若堆内存不规整,就无法通过简单的移动指针分配内存。这种情况下虚拟机会维护一个列表记录哪些内存可用,分配时查找并更新列表

使用哪种方式取决于内存是否规整,而内存是否规整又取决于垃圾收集器的 GC 算法。典型如 Serial、ParNew 这两种垃圾收集器,它们在 GC 时带有压缩整理的功能,因此系统会采用 “指针碰撞” 的方式分配内存。而 CMS 这种基于 Mark-Sweep(标记-清除)算法的垃圾收集器,则会采用 “空闲列表法” 分配内存

2、内存分配的安全

需要注意的是,若多个线程同时申请分配内存,如果不加以同步控制,则会导致内存分配不对。不同的虚拟机会采用不同的机制避免线程安全问题

  • 同步锁定:通过 CAS 配上失败重试的方式保证更新操作的原子性
    注:CAS,即 CPU 硬件同步原语,全称为 Compare And Swap,若比较不对则失败
  • TLAB:即线程分配缓冲区。在堆中预先为每个线程分配一小块内存,线程在各自分配的内存上进行内存分配来保证安全。只有当 TLAB 用尽并申请新的 TLAB 时,才进行同步锁定

内存初始化

内存初始化指的是将对象分配到的内存所有位置重置为 0(不包含对象头)。若对象通过 TLAB 分配的,该过程会提前至 “内存分配” 执行

对象头初始化

设置对象的对象头信息

对象实例数据初始化

设置对象的实例数据信息,即成员变量值。只有这步完成了,一个真正的对象才产生并能提供给我们使用


参考链接

Java虚拟机(JVM)你只要看这一篇就够了!
java类的加载机制
jvm之java类加载机制和类加载器(ClassLoader)的详解
JVM类生命周期概述:加载时机与加载过程

05-JVM对象探秘

【JVM】第二章 JVM类加载、JVM对象相关推荐

  1. [Effective Java]第二章 创建和销毁对象

    第一章      前言 略... 第二章      创建和销毁对象 1.            考虑用静态工厂方法代替构造器 创建对象方法:一是最常用的公有构造器,二是静态工厂方法.下面是一个Bool ...

  2. 《Effective Java》学习笔记 第二章 创建和销毁对象

    第二章 创建和销毁对象 何时以及如何创建对象,何时以及如何避免创建对象,如何确保他们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作. 1 考虑用静态工厂方法代替构造器 一般在某处获取一 ...

  3. 用Java实现JVM第二章《搜索class文件》

    案例简述 本章节主要了解Java虚拟机从哪里寻找class文件并且读取class内字节码 环境准备 1.jdk 1.8.0 2.IntelliJ IDEA Community Edition 2018 ...

  4. 经典中的品味:第二章 C++基本的对象,类型和值(上)

    摘要: 原创出处: http://www.cnblogs.com/Alandre/ 泥沙砖瓦浆木匠 希望转载,保留摘要,谢谢! 自律,是以积极而主动的态度,去解决人生的痛苦~ 上一章,我们大谈了Hel ...

  5. Java编程思想学习-《第二章 一切都是对象》

    第2章 一切都是对象 尽管Java是基于C++的,但是相比之下,Java是一种更"纯粹"的面向对象程序设计语言.Java语言假设我们只进行面向对象的程序设计.也就是说,在开始用Ja ...

  6. 深入理解JVM—第二章:Java内存区域与内存溢出异常

    1,概述 Java较C.C++,Java可以利用虚拟机的自动内存管理机制,避免繁琐的内存分配与回收.不容易出现内存泄漏和内存溢出问题. 内存泄漏:指程序申请到的内存空间不再归还(无法归还),可使用完该 ...

  7. 程序设计与算法三~C++面向对象程序设计~北大郭炜MOOC学习笔记~第二章:类和对象初步(新标准C++程序设计)

    以下内容为笔者手打,望读者珍惜,如有转载还请注明. chapter2:类和对象初步 数据结构+算法=程序 $2.1结构化程序设计的不足     结构化程序设计也称面向过程的程序设计,过程是用函数实现的 ...

  8. 一篇文章掌握整个JVM,JVM超详细解析!!!

    JVM 先想想一些问题 1 我们开发人员编写的Java代码是怎么让电脑认识的 2 为什么说java是跨平台语言 3 Jdk和Jre和JVM的区别 4 为什么要学习JVM 深入学习JVM 1 JVM运行 ...

  9. 超硬核!!!一篇文章掌握整个JVM,JVM超详细解析!!!

    JVM 先想想一些问题 1 我们开发人员编写的Java代码是怎么让电脑认识的 2 为什么说java是跨平台语言 3 Jdk和Jre和JVM的区别 4 为什么要学习JVM 深入学习JVM 1 JVM运行 ...

最新文章

  1. 呼伦湖国家级自然保护区管理局投放草料保野生黄羊过冬
  2. 洛谷P4550 收集邮票(概率期望)
  3. 数据结构实验之链表一:顺序建立链表(SDUT 2116)
  4. 定义分销渠道(distribution channel)
  5. ielts indicator 考前准备 checklist
  6. mybatis和ehcache整合
  7. 64位Ubuntu kylin 16.04安装wine QQ
  8. Unity 编辑器知识(—)如何绘制色块
  9. 场景编辑器开发第五天,设计架构重回flash,很多问题不是出在技术上而是策划上
  10. MarkdownPad在win10安装不能预览的问题Awesomium安装
  11. 基于51单片机的交通灯设计
  12. 【C#】基于Opencv/Emgucv的身份证识别
  13. Win10+Android+夜神安卓模拟器 搭建ReactNative开发环境
  14. 训练报错:RuntimeError: CUDA error: device-side assert triggered
  15. python可视化之matplotlib绘图--蜘蛛侠钢铁侠(组合分区绘图)
  16. 2022年模式识别高峰论坛学习与个人感悟
  17. linux及时释放内存,LINUX释放内存
  18. 上手python之字典
  19. RabbitMQ在特来电的深度应用
  20. C#毕业设计——基于C#+asp.net+Access的视频点播系统设计与实现(毕业论文+程序源码)——视频点播系统

热门文章

  1. html css图片展开动画,8个实用炫酷的HTML5图片动画应用
  2. java不等长二维数组_Java中关于二维数组的理解与使用
  3. leetcode378 Kth Smallest Element in a Sorted Matrix
  4. Linux监控命令之==sar
  5. 当Java代码遇上抽象、重载加重写,一切都不美好了
  6. MySQL-based databases CVE-2016-6664 本地提权
  7. 题目1198:a+b
  8. mysql 乱码解决方案
  9. bcp生成excel文件优化方案
  10. 无法登录a6服务器可以修复么,航天A6登录常见问题