JVM类加载机制

JVM笔记 - 图1

Java类生命周期

JVM笔记 - 图2

  1. 这张图表现了一个类的生命周期,完整一点的话,可以在最开始加上javac编译阶段。而==“类加载”只包括加载、连接、初始化==这三个过程。
  2. 需要区分==“类加载”与“加载”==,加载只是类加载的第一个环节。
  3. 解析部分是灵活的,它可以在初始化环节之后再进行,实现所谓的“后期绑定”,其他环节的顺序不可改变。

加载

加载是一个读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的 java.lang.Class类型的对象的过程。

JVM笔记 - 图3

连接:验证

  • 文件格式验证(在加载阶段基本就完成)
  • 元数据、字节码验证
  • 符号引用验证(解析阶段)

简单概括来说就是对 class静态结构进行语法和语义上的分析,保证其不会产生危害虚拟机的行为。

验证包含很多个步骤,分散在各个不同的阶段内。

连接:准备

  1. Class A {
  2. private int a = 1;
  3. static private int b; // b=0 此处的是静态变量为赋0,并非成员变量
  4. static {
  5. b = 1;
  6. }
  7. }

JVM笔记 - 图4

方法区为抽象概念,元空间是实现方式。

在JDK8之前,类的元信息、常量池、静态变量等都存储在永久代这种具体实现中。

子JDK8及以后,常量池、静态变量被移除“方法区”,转移到堆中,元信息等依然保留在方法区中,具体存储方式改为了元空间。

连接:解析

将符号引用替换为直接引用

JVM笔记 - 图5

静态解析:A调用的B是一个具体的实现类,解析的目标类很明确。

动态解析:Java上层使用了多态,B为抽象类或者接口,可能有两个具体的实现类,具体实现并不明确。

当然也就不知道使用哪个具体类的直接引用来进行替换,知道运用过程中发生了调用,此时虚拟机调用栈中将会得到具体的类型信息,再进行解析,就会有明确的直接引用来替换符号引用。

初始化

主动的资源初始化动作:不是指的构造函数,而是 class层面的,比如说成员变量的赋值动作、静态变量的赋值动作,以及静态代码块的逻辑。

显式的调用new指令:会调用构造函数进行对象实例化。

JVM主导 用户主导
加载 剩余部分 读取二进制流
连接 JVM主导
初始化 整个初始化部分

只有加载步骤中的读取二进制流与初始化部分,能够被上层开发者,也就是大部分的Java程序员控制,而剩下的所有步骤,都是由JVM掌控,其中细节由JVM的开发人员处理,对上层开发者来说是个黑盒。

JVM笔记 - 图6

面向对象SOLID原则:单一功能、开闭、里氏替换、接口隔离、依赖反转。

类加载器

JVM笔记 - 图7

类加载器的分类

JVM笔记 - 图8

双亲委派

类加载的命名空间

JⅥM规范:每个类加载器都有属于自己的命名空间。

需求:默认情况下,一个限定名的类只会被一个类加载器加载并解析使用,这样在程序中,它就是唯一的,不会产生歧义。

JVM笔记 - 图9

上面非继承关系,而是组合而成的。

先由父亲加载器加载,无法加载,再交给儿子加载器加载。

  1. 不同的类加载器,除了读取二进制流的动作和范围不一样,后续的加载逻辑是否也不一样?

    除了 Bootstrap ClassLoader,所有的非 Bootstrap ClassLoader都继承了java.lang.ClassLoader,都由这个类的 defineClass进行后续处理。

2.遇到限定名一样的类,这么多类加载器会不会产生混乱?

越核心的类库被越上层的类加载器加载,而某限定名的类一旦被加载过了,被动情况下,就不会再加载相同限定名的类。这样,就能够有效避免混乱。

破坏双亲委派

重写 loadClass方法

破坏双亲委派—第一次

JVM笔记 - 图10

破坏双亲委派—第二次

JVM笔记 - 图11

JDBC

破坏双亲委派—第三次

模块化

思考题:能不能自己写一个限定名为 java.lang.String的类,并在程序中调用它?

如果自定义一个java.lang.String类 那么根据双亲委派原则,会先从AppClassLoader依次向上查找是否已经加载过这个类,最后会查找到BootStrapClassLoader中已经加载过,所以自定义的String无法被加载调用

JVM内存分区

JVM笔记 - 图12

  • 线程共享:方法区、堆
  • 线程隔离:虚拟机栈、程序计数器、本地方法栈

程序计数器

硬件层面:一种寄存器,存储指令地址提供给处理器执行

JVM软件层面:类似,存储字节码指令地址

虚拟机栈

也称Java方法栈:程序执行的过程对应方法的调用,而方法的调用对应栈帧的入栈出栈。

  1. public void funcA() {
  2. int a = 1;
  3. funcB(1);
  4. // do something
  5. }
  6. public void funcB(int arg) {
  7. // do something
  8. }

JVM笔记 - 图13

先调用A方法,将A方法封装成“栈帧”入栈,由于A方法调用了B方法,B方法封装成“栈帧”入栈,

先执行B中的逻辑,B栈帧出栈,执行A方法,A栈帧出栈

1.栈帧2.栈帧的生成时机3.栈帧的构成

超出方法栈的最大深度 - StackOverFlow

栈帧

栈帧构成

  1. 局部变量表(Local Variable)
    1. 主要存储方法的参数、定义在方法內的局部变量、包括基本数据类型(8大),对象的引用地址,返回值地址。
    2. 局部变量表中存储的基本单元为变量槽(Slot),32位(4字节)以内的数据类型占一个slot,64位(long,double)的占两个slot。
    3. 局部变量表是一个数字数组,byte、short、char都会被转化为int,boolean类型也会被转化为int,0代表false、非0代表true。
    4. 局部变量表的大小是在编译期间决定下来的,所以在运行时它的大小是不会变的。
    5. 局部变量表中含有直接或者间接指向的引用类型变量时,不会被垃圾回收处理。
  2. 操作数栈(Operand Stack)
    1. 用来存储操作数的栈
    2. 操作数大部分就是方法内的变量
    3. 栈帧-操作数栈JVM笔记 - 图14
    4. 两个栈帧时JVM笔记 - 图15
  3. 动态链接(Dynamic Linking)
    1. OOP的主要特性之一就是多态
    2. Java中的多态,就是通过栈帧中的动态链接来实现
    3. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接( Dynamic Linking)。
  4. 返回地址(Return Address)
  5. 其他附加信息

本地方法栈

JVM内存区域

JVM笔记 - 图16

方法区

JDK8以前,Hotspot的开发者将面向堆的分代设计复用在了方法区上,使用永久代作为方法区的实现,

JDK8以后,开始借鉴验了JRockit的设计思路使用了元空间来代替永久代作为新的实现方式。

JVM笔记 - 图17

方法区是抽象概念,永久代和元空间是实现方式

使用“永久代”实现“方法区”的缺点:

  1. 可能引起内存溢出。
  2. 永久代本身的复杂设计并不是方法区需要的,并可能带来未知异常。

所以使用基手直接内存的元空间来代替永久代。

方法区-类型信息

JVM笔记 - 图18

常量池

在虚拟机加载字节码的时候,首先加载的是些静态的“符号引用”。然后在类加载的“链接”或者说程序运行时
将符号引用转化为直接引用。上面说到的符号引用既然是从字节码中加载进来的,那么在字节中怎么体现呢?
Constant Pool(常量池)内的数据体现了符号引用写一些其他的静态引用,这个常量池更像链接表。

运行时常量池(Runtime)

Constant Pool)

  1. 编译期间产生的,主要字节码中定义的静态信息,比如:
    • 由字节码生成的Class对象(Constant Pool 包含在Class对象内)
    • 由字节码生成的字面量
  2. 运行行期间产生的,这部分比较灵活,虚拟机开发者可以将必要的数据都放进去,比如:
    • 运行时会将一部分符号引用转换为直接引用,那么这些直接引用可以存储进来
    • 常见的字符串常量池

Java中的常量池(字符串常量池、class常量池和运行时常量池)

java字符串常量池、class常量池和运行时常量池

最主流最常见的就是从垃圾回收的角度对堆内存进行划分