JVM(一)初步
1. jvm生命周期


2.内存布局:

3. 类的加载过程(生命周期):
加载,链接(验证、准备、解析),初始化,使用,卸载。链接结束后,类已经加载到内存中了,初始化表示类装载到系统中了。
3.1 加载:







方法区和堆区的对象,相互关联(引用)

方法区的是有cpp编写的,java程序员一般不能直接操作,而堆区的可以

java.lang.Class对象可供反射使用。

- PS:基本数据类型由虚拟机预先定义,不需要加载
数组的加载
- 数组类由JVM 在运行时根据需要动态生成的。JVM使用指定的元素类型和数组维度来创建新的数组类。
- 当 JVM 需要创建一个数组类时,它会检查数组元素的类型:
- 如果数组元素是基本数据类型(如 int、char 等),则 JVM 会将该数组类标记为与引导类加载器(Bootstrap ClassLoader)关联,不需要通过常规的类加载器加载;
- 如果数组元素是引用类型(如 String、Object 等),JVM 会递归地使用类加载器去加载该元素类型。例如,对于 String[] 数组,JVM 会首先确保 java.lang.String 类已经被加载。
3.2 验证
3.3 准备
3.3.1

- 静态变量位于堆区的Class对象中
3.3.2

- boolean实际是int,false是0,true是1
3.3.3

- final修饰的类变量,右边是常量,在编译阶段已确定好值,在准备阶段直接赋值。
- static final int a = 1
3.4 解析
符号引号:常量池里的编号,就是一组符号用来描述所引用的目标,解析,即将常量池内的符号引用转换为直接引用,使用内存中的地址。
public class Main {
public static void main(String[] args) {
// 触发String类的解析
String str = new String("Hello");
System.out.println(str);
}
}
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String Hello
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
13: aload_1
14: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
17: return
#2 = Class #23 // java/lang/String
#23 = Utf8 java/lang/String
- 符号引用:#2 是常量池中对 java/lang/String 类的符号引用
- 类加载检查:JVM 检查 String 类是否已加载
- 类加载触发:如果未加载,触发完整的类加载过程(加载、验证、准备、解析、初始化)
- 符号引用转换:将 #2 转换为指向 String 类元数据的直接指针
- 内存分配:new 指令在堆中为 String 对象分配内存
3.5 初始化
即执行clinit方法,由类变量赋值语句和静态代码块中的语句合并而成,方法中的语句按代码顺序执行。子类会先执行父类的clinit方法。多线程下,一个类的clinit方法会加锁执行,这保证了,虚拟机加载类,只会执行一次clinit方法。
3.5.1 示例
代码:
public class Demo01 {
public static final int num ;
public static String name = "Tom";
static {
num=1;
System.out.println(num);
System.out.println(name);
}
}
字节码:

3.5.2 以下的几种方式会导致类的初始化:

3.5.2.1 初始化示例:
public class Demo02 {
public static void main(String[] args) {
System.out.println("A");
new Demo02();
new Demo02();
}
public Demo02(){
System.out.println("B");
}
{
System.out.println("C");
}
static {
System.out.println("D");
}
}
输出如下:构造代码块先于构造方法
D
A
C
B
C
B
3.5.3 不会有clinit指令的情况

3.5.4 可能会出现非法的前向引用
public class Demo01 {
static {
System.out.println(num);// 报错:非法的前向引用
}
public static final int num =1;
}
public class Demo01 {
static {
num=1;
System.out.println(num);// 报错:非法的前向引用
}
public static final int num ;
}
3.5.5初始化的面试题
3.5.5.1访问父类的变量,不会初始化子类
public class Demo3 {
public static void main(String[] args) {
//输出3
System.out.println(B.a);
}
}
class A{
static int a=3;
}
class B extends A{
static {
a=4;
}
}
3.5.5.2下面的代码表示子类变量a覆盖了父类,访问a即表示访问子类,会初始化子类
public class Demo3 {
public static void main(String[] args) {
//输出4
System.out.println(B.a);
}
}
class A{
static int a=3;
}
class B extends A{
static int a=6;
static {
a=4;
}
}
3.5.5.3类的使用
类的主动使用和被动使用,即会不会执行类的初始化
3.5.5.3.1以下是主动使用:



3.5.5.3.1以下是被动使用:

3.6 准备阶段对比初始化阶段
public class Demo5 {
//准备阶段赋值,并且进入常量池
public static final int a =1;
public static final String PATTERN="DUAL";
//初始化阶段赋值,不会进入常量池
public static final int b =new Random().nextInt();
public static final String CAR_COLOR=new String("RED");
}
- 凡是以字面量形式赋值的常量,准备阶段赋值;需要调用方法指令(new、invoke)的都是初始化阶段赋值。
4. 运行时数据区
4.1 总览:


RunTime说明,一个JVM对应一个RunTime实例


4.2 PC寄存器
在加载阶段,虚拟机将字节码文件中的指令读取到内存后,会将偏移量转换为内存地址,每条字节码指令都拥有一个内存地址,程序计数器存放下一条指令的地址,执行引擎读取之,执行相应的操作,线程私有。



4.3 虚拟机栈


虚拟机栈可能出现的异常。方法递归,可能会导致StackOverFlowError

设置栈大小:示例,-Xss256K
4.3.1 栈帧:一个一个的方法

栈运行的原理:

栈运行的原理示意图:

- 方法正常退出或者发生异常,会导致栈帧弹出。
4.3.2 栈帧内部的结构:

4.3.2.1 局部变量表
栈帧大小主要由局部变量表影响,表的前面存有形参。实例方法的局部变量表首位存了this。



- 访问变量q,那么访问的索引为4(不是5)。
局部变量表里的变量是要声明的,比如下图的info变量,删除后,局部变量表就没有了。
删除前:

删除后:

slot槽位可重复利用

注意下图中局部变量表的长度为3,b的index是2,c的index也是2,变量c复用了变量b的位置

局部变量需要显示赋值,没有默认值

局部变量表涉及到GC

局部变量表里的“length”,指作用域范围,pc加length等于code 长度,code长度就是指令长度。



4.3.2.2 操作数栈


4.3.2.3 动态链接
动态链接,指向运行时常量池的方法引用。当前类的字节码指令引用了其他类的属性或方法时,需要将符号引用转换为对应的内存地址,动态链接保存了符号引用到运行时常量池的内存地址的映射关系。
比如下图,运行时常量池指的是System类的,第9行获取System类的静态变量out,在11行接着调用out的方法println,JVM虚拟机在执行该指令前,需要将符号引用#10、#16的内存地址保存在动态链接中。

4.3.2.4 方法返回地址:
在方法正常返回时,返回地址是调用该方法的指令的下一条指令的地址。
public class Example {
public static void main(String[] args) {
int result = add(1, 2); // 调用 add() 方法,记录下一条指令地址(即 println)
System.out.println(result); // 这行指令的地址就是返回地址
}
public static int add(int a, int b) {
int sum = a + b;
return sum; // 执行 ireturn 指令,返回地址用于跳转回 main 方法的 println
}
}
4.4 异常表
4.5 本地方法栈

本地方法栈抛异常示例
public class Demo6 {
public static void main(String[] args) throws IOException {
FileOutputStream fileOutputStream =new FileOutputStream("E:\\123.txt");
fileOutputStream.write(2);
}
}
Exception in thread "main" java.io.FileNotFoundException: E:\123.txt (系统找不到指定的路径。)
at java.io.FileOutputStream.open0(Native Method)
at java.io.FileOutputStream.open(FileOutputStream.java:270)
at java.io.FileOutputStream.<init>(FileOutputStream.java:213)
at java.io.FileOutputStream.<init>(FileOutputStream.java:101)
at org.jvm.study.java6.Demo6.main(Demo6.java:9)
- 电脑里没有E盘
4.6变量的位置
局部变量
- 在虚拟机栈的栈帧内
- 基本类型(如 int)的变量名和值均存于栈帧的局部变量表。
- 引用类型(如 Object)的变量名存于栈,其指向的对象实例存于堆。
- 生命周期:随方法调用开始,方法结束栈帧弹出时销毁。
实例变量
- 存储位置:堆内存(Heap)
- 对象实例化时,实例变量随对象分配在堆中,生命周期与对象一致。
- 若变量为基本类型(如 int),值直接存于堆内;若为引用类型(如 String),变量存储对象在堆中的地址。
类变量


5 方法调用
原理:



示例:



5.2 方法调用指令

5.3 动态绑定



类加载补充:
自己写个String类
包的名不同就可以重写
package com.alibaba.dme.bcp.ump.biz.activities.constants;
public class String {
public static void main(java.lang.String[] args) {
System.out.println(String.class.getClassLoader());
java.lang.String str = "123";
System.out.println(str.getClass().getClassLoader());
}
}
类初始化和对象实例化
https://www.cnblogs.com/chanshuyi/p/jvm_serial_07_jvm_class_loader_mechanism.html
浙公网安备 33010602011771号