JVM类加载机制
Java类生命周期
- 这张图表现了一个类的生命周期,完整一点的话,可以在最开始加上javac编译阶段。而==“类加载”只包括加载、连接、初始化==这三个过程。
- 需要区分==“类加载”与“加载”==,加载只是类加载的第一个环节。
- 解析部分是灵活的,它可以在初始化环节之后再进行,实现所谓的“后期绑定”,其他环节的顺序不可改变。
加载
加载是一个读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的 java.lang.Class类型的对象的过程。
连接:验证
- 文件格式验证(在加载阶段基本就完成)
- 元数据、字节码验证
- 符号引用验证(解析阶段)
简单概括来说就是对 class静态结构进行语法和语义上的分析,保证其不会产生危害虚拟机的行为。
验证包含很多个步骤,分散在各个不同的阶段内。
连接:准备
Class A {
private int a = 1;
static private int b; // b=0 此处的是静态变量为赋0,并非成员变量
static {
b = 1;
}
}
方法区为抽象概念,元空间是实现方式。
在JDK8之前,类的元信息、常量池、静态变量等都存储在永久代这种具体实现中。
子JDK8及以后,常量池、静态变量被移除“方法区”,转移到堆中,元信息等依然保留在方法区中,具体存储方式改为了元空间。
连接:解析
将符号引用替换为直接引用
静态解析:A调用的B是一个具体的实现类,解析的目标类很明确。
动态解析:Java上层使用了多态,B为抽象类或者接口,可能有两个具体的实现类,具体实现并不明确。
当然也就不知道使用哪个具体类的直接引用来进行替换,知道运用过程中发生了调用,此时虚拟机调用栈中将会得到具体的类型信息,再进行解析,就会有明确的直接引用来替换符号引用。
初始化
主动的资源初始化动作:不是指的构造函数,而是 class层面的,比如说成员变量的赋值动作、静态变量的赋值动作,以及静态代码块的逻辑。
显式的调用new指令:会调用构造函数进行对象实例化。
JVM主导 | 用户主导 | |
---|---|---|
加载 | 剩余部分 | 读取二进制流 |
连接 | JVM主导 | |
初始化 | 整个初始化部分 |
只有加载步骤中的读取二进制流与初始化部分,能够被上层开发者,也就是大部分的Java程序员控制,而剩下的所有步骤,都是由JVM掌控,其中细节由JVM的开发人员处理,对上层开发者来说是个黑盒。
面向对象SOLID原则:单一功能、开闭、里氏替换、接口隔离、依赖反转。
类加载器
类加载器的分类
双亲委派
类加载的命名空间
JⅥM规范:每个类加载器都有属于自己的命名空间。
需求:默认情况下,一个限定名的类只会被一个类加载器加载并解析使用,这样在程序中,它就是唯一的,不会产生歧义。
上面非继承关系,而是组合而成的。
先由父亲加载器加载,无法加载,再交给儿子加载器加载。
- 不同的类加载器,除了读取二进制流的动作和范围不一样,后续的加载逻辑是否也不一样?
除了
Bootstrap ClassLoader
,所有的非 Bootstrap ClassLoader都继承了java.lang.ClassLoader
,都由这个类的defineClass
进行后续处理。
2.遇到限定名一样的类,这么多类加载器会不会产生混乱?
越核心的类库被越上层的类加载器加载,而某限定名的类一旦被加载过了,被动情况下,就不会再加载相同限定名的类。这样,就能够有效避免混乱。
破坏双亲委派
重写 loadClass
方法
破坏双亲委派—第一次
破坏双亲委派—第二次
JDBC
破坏双亲委派—第三次
模块化
思考题:能不能自己写一个限定名为 java.lang.String的类,并在程序中调用它?
如果自定义一个java.lang.String类 那么根据双亲委派原则,会先从AppClassLoader依次向上查找是否已经加载过这个类,最后会查找到BootStrapClassLoader中已经加载过,所以自定义的String无法被加载调用
JVM内存分区
- 线程共享:方法区、堆
- 线程隔离:虚拟机栈、程序计数器、本地方法栈
程序计数器
硬件层面:一种寄存器,存储指令地址提供给处理器执行
JVM软件层面:类似,存储字节码指令地址
虚拟机栈
也称Java方法栈
:程序执行的过程对应方法的调用,而方法的调用对应栈帧的入栈出栈。
public void funcA() {
int a = 1;
funcB(1);
// do something
}
public void funcB(int arg) {
// do something
}
先调用A方法,将A方法封装成“栈帧”入栈,由于A方法调用了B方法,B方法封装成“栈帧”入栈,
先执行B中的逻辑,B栈帧出栈,执行A方法,A栈帧出栈
1.栈帧
,2.栈帧的生成时机
,3.栈帧的构成
超出方法栈的最大深度 - StackOverFlow
栈帧
栈帧构成
- 局部变量表(Local Variable)
- 主要存储方法的参数、定义在方法內的局部变量、包括基本数据类型(8大),对象的引用地址,返回值地址。
- 局部变量表中存储的基本单元为变量槽(Slot),32位(4字节)以内的数据类型占一个slot,64位(long,double)的占两个slot。
- 局部变量表是一个数字数组,byte、short、char都会被转化为int,boolean类型也会被转化为int,0代表false、非0代表true。
- 局部变量表的大小是在编译期间决定下来的,所以在运行时它的大小是不会变的。
- 局部变量表中含有直接或者间接指向的引用类型变量时,不会被垃圾回收处理。
- 操作数栈(Operand Stack)
- 用来存储操作数的栈
- 操作数大部分就是方法内的变量
- 栈帧-操作数栈
- 两个栈帧时
- 动态链接(Dynamic Linking)
- OOP的主要特性之一就是多态
- Java中的多态,就是通过栈帧中的动态链接来实现
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接( Dynamic Linking)。
- 返回地址(Return Address)
- 其他附加信息
本地方法栈
JVM内存区域
方法区
JDK8以前,Hotspot的开发者将面向堆的分代设计复用在了方法区上,使用永久代作为方法区的实现,
JDK8以后,开始借鉴验了JRockit的设计思路使用了元空间来代替永久代作为新的实现方式。
方法区是抽象概念,永久代和元空间是实现方式。
使用“永久代”实现“方法区”的缺点:
- 可能引起内存溢出。
- 永久代本身的复杂设计并不是方法区需要的,并可能带来未知异常。
所以使用基手直接内存的元空间来代替永久代。
方法区-类型信息
常量池
在虚拟机加载字节码的时候,首先加载的是些静态的“符号引用”。然后在类加载的“链接”或者说程序运行时
将符号引用转化为直接引用。上面说到的符号引用既然是从字节码中加载进来的,那么在字节中怎么体现呢?
Constant Pool(常量池)内的数据体现了符号引用写一些其他的静态引用,这个常量池更像链接表。
运行时常量池(Runtime)
Constant Pool)
- 编译期间产生的,主要字节码中定义的静态信息,比如:
- 由字节码生成的Class对象(Constant Pool 包含在Class对象内)
- 由字节码生成的字面量
- 运行行期间产生的,这部分比较灵活,虚拟机开发者可以将必要的数据都放进去,比如:
- 运行时会将一部分符号引用转换为直接引用,那么这些直接引用可以存储进来
- 常见的字符串常量池
- …
Java中的常量池(字符串常量池、class常量池和运行时常量池)
堆
最主流最常见的就是从垃圾回收的角度对堆内存进行划分