平时我们使用的电脑主机、服务器,都是传统意义上的物理机,通过具体的CPU,内存等硬件实现。
虚拟机的定义:“虚拟机指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。"
虚拟机分为系统虚拟机和程序虚拟机。
系统虚拟机是对物理机的仿真,它可以提供一个可运行完整操作系统的软件平台。例如常用的VMWare,我们在使用VM创建一个linux系统时,常常要我们为系统分配内存,CPU。由此可见系统虚拟机也和物理硬件挂钩。
程序虚拟机则是专门执行计算机程序的平台。
二、什么是JVM虚拟机
JVM虚拟机就是一台程序虚拟机。
在我们将写好的Java程序的源代码编译成字节码文件(.class后缀)后,JVM虚拟机会读取字节码文件,经过一系列的步骤后,最终将我们的程序运行起来。
JVM虚拟机不受限于操作平台,具备可移植性,在windows、linux平台都可以运行编译好的字节码文件。
JVM虚拟机还有一个特性,跨语言性。很多刚学Java的人以为JVM虚拟机只有Java语言能够使用,实际上JVM虚拟机可以运行多种语言,例如Kotlin、Groovy、Jython,JRuby、JavaScript等。
三、JVM虚拟机的整体结构
粗略图
细化图
四、类加载器
我们写好Java程序后,通过编译器将XXX.java文件编译成了JVM虚拟机能够识别的XXX.class文件,也就是字节码文件。而我们在程序中写的自定义类,使用的基本数据类型,对象引用,方法等就存在于字节码文件当中,此时字节码里存储的数据只是静态数据,他们并没有被使用。类加载器的作用就是读取字节码的信息,经过一系列的步骤,将字节码里的静态数据最终加载到运行时数据区。
类加载的工作步骤主要分为三步,装载------->链接------->初始化
(一)装载(Loading)1.通过一个类的全限定名获取定义此类的二进制字节流。
2.将该字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在堆内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
举例:例如有一个实体类Animals所在的包叫做com.xxx.entity,类加载器用com.xxx.entity.Animals这个全限定名找到class文件,将class文件中的二进制数据加载到内存中,然后在运行时数据区中的堆内存创建一个代表该字节流的java.lang.Class对象,用来封装类在方法区内的数据结构。java.lang.Class对象就是Animals这个类的元对象,这个对象由类的变量,方法等各种信息。
补充:加载class文件的常见途径
1.直接本地磁盘上加载
2.通过网络传输
3.jar、war包
4.动态代理技术
(二)链接(Linking)1.验证(Verify)
验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。
主要包括文件格式验证、元数据验证、字节码验证、符号引用验证。
2.准备 (prepare)
准备阶段主要为类变量分配内存并设置初始值。也就是我们写的静态变量。
3.解析(resolve)
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。解析和初始化不一定是顺序的。
举例:
代码如下
public class Dog {
//类变量
static int name = 1;
//常量
final int name2 = 2;
public static void main(String[] args) {
}
}
通过javap -v反编译Dog.class文件后,得到的信息如下
Classfile /D:/JAVA基础/test/target/classes/com/jimmy/Dog.class Last modified 2022年5月19日; size 519 bytes SHA-256 checksum 338f56de0ce4231aaaf16be5207555b7976ebbdbbf7651474768395da6640b35 Compiled from "Dog.java" public class com.jimmy.Dog minor version: 0 major version: 59 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #8 // com/jimmy/Dog super_class: #2 // java/lang/Object interfaces: 0, fields: 2, methods: 3, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // " ":()V #4 = Utf8 java/lang/Object #5 = Utf8 #6 = Utf8 ()V #7 = Fieldref #8.#9 // com/jimmy/Dog.name2:I #8 = Class #10 // com/jimmy/Dog #9 = NameAndType #11:#12 // name2:I #10 = Utf8 com/jimmy/Dog #11 = Utf8 name2 #12 = Utf8 I #13 = Fieldref #8.#14 // com/jimmy/Dog.name:I #14 = NameAndType #15:#12 // name:I #15 = Utf8 name #16 = Utf8 ConstantValue #17 = Integer 2 #18 = Utf8 Code #19 = Utf8 LineNumberTable #20 = Utf8 LocalVariableTable #21 = Utf8 this #22 = Utf8 Lcom/jimmy/Dog; #23 = Utf8 main #24 = Utf8 ([Ljava/lang/String;)V #25 = Utf8 args #26 = Utf8 [Ljava/lang/String; #27 = Utf8 #28 = Utf8 SourceFile #29 = Utf8 Dog.java { static int name; descriptor: I flags: (0x0008) ACC_STATIC final int name2; descriptor: I flags: (0x0010) ACC_FINAL ConstantValue: int 2 public com.jimmy.Dog(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: aload_0 5: iconst_2 6: putfield #7 // Field name2:I 9: return LineNumberTable: line 3: 0 line 5: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/jimmy/Dog; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 9: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 args [Ljava/lang/String; static {}; descriptor: ()V flags: (0x0008) ACC_STATIC Code: stack=1, locals=0, args_size=0 0: iconst_1 1: putstatic #13 // Field name:I 4: return LineNumberTable: line 4: 0 } SourceFile: "Dog.java"
挨个分析
//由final修饰的常量在编译阶段就已经赋值为2
final int name2;
descriptor: I
flags: (0x0010) ACC_FINAL
ConstantValue: int 2
//静态变量在类加载链接中的准备阶段会开辟内存空间,给int基本类型赋默认值为0
//如果是引用类型则是null
//注意 静态变量在jdk1.8中内存位置在堆中的class对象身上
static int name;
descriptor: I
flags: (0x0008) ACC_STATIC
//Class常量池 //当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池 //常量池中主要存放两类数据,一是字面量、二是符号引用。 //字面量:比如String类型的字符串值或者定义为final类型的常量的值。 //可以通过#17,发现常量name2的值 2 //符号引用: // # 就是符号引用 //类或接口的全限定名(包括他的父类和所实现的接口) //变量或方法的名称 //变量或方法的描述信息 //方法的描述:参数个数、参数类型、方法返回类型等等 //变量的描述信息:变量的返回值 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // " ":()V #4 = Utf8 java/lang/Object #5 = Utf8 #6 = Utf8 ()V #7 = Fieldref #8.#9 // com/jimmy/Dog.name2:I #8 = Class #10 // com/jimmy/Dog #9 = NameAndType #11:#12 // name2:I #10 = Utf8 com/jimmy/Dog #11 = Utf8 name2 #12 = Utf8 I #13 = Fieldref #8.#14 // com/jimmy/Dog.name:I #14 = NameAndType #15:#12 // name:I #15 = Utf8 name #16 = Utf8 ConstantValue #17 = Integer 2 #18 = Utf8 Code #19 = Utf8 LineNumberTable #20 = Utf8 LocalVariableTable #21 = Utf8 this #22 = Utf8 Lcom/jimmy/Dog; #23 = Utf8 main #24 = Utf8 ([Ljava/lang/String;)V #25 = Utf8 args #26 = Utf8 [Ljava/lang/String; #27 = Utf8 #28 = Utf8 SourceFile #29 = Utf8 Dog.java
//字符串常量池 JDK1.8 内存位置 堆 //全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例 //然后将该字符串对象实例的引用值存到string pool中 //string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的 // 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串 //运行时常量池 JDK1.8 内存位置 元空间(本地内存) //而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中 //由此可知,运行时常量池也是每个类都有一个 //class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值 //而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池 //运行时常量池和java.lang.Class对象 //装载阶段在堆内存中实例化出代表这个类的java.lang.Class对象 //这个对象将作为程序访问方法区中的类型数据的外部接口。(三)初始化(Initialization)
1、初始化阶段就是执行类构造器方法
2、此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来的。
3、构造器方法中指令按语句在源文件中出现的顺序执行。
4、
5、若该类具有父类,JVM会保证子类的
6、虚拟机必须保证一个类的
举例:
public class Dog {
static int name = 1;
static String str;
static {
name = 2;
str = "Jimmy";
}
public static void main(String[] args) {
System.out.println(Dog.name);
}
}
JVM把需要赋值的变量收集起来,整合一个clinit方法,对这些类变量集体赋值。如果没有静态变量或者方法,是不会有clinit方法的。
(四)不同的类加载器程序中常见的三种类加载器:
主要分为引导类加载器(BootStrap ClassLoader)、扩展类加载器(Extension ClassLoader)、系统类加载器(System ClassLoader)。
引导类加载器:
引导类加载器由c/c++实现,扩展类加载器和系统类加载器由引导类加载器加载,不继承ClassLoader类。主要负责java.lang等核心类库。
获取引导类加载器:ClassLoader bootStrapClassLoader = extensionClassLoader.getParent();
扩展类加载器:
Java语言编写,派生ClassLoader,父加载器为启动类加载器。
从java.ext.dirs中系统属性所指定的目录中加载类库,或从jdk安装目录jre/lib/ext子目录(扩展目录)下加载类库。用户创建的jar放在此目录下,也会由扩展类加载器自动加载。
获取扩展类加载器:ClassLoader extensionClassLoader = systemClassLoader.getParent();
系统类加载器:
java语言编写,派生ClassLoader,父加载器为扩展类加载器。
负责加载环境变量ClassPath或者系统属性java.class.path路径下的类库,该类加载器是程序中默认的类加载器。
获取系统类加载器:ClassLoader systemClassLoader = ClassLoader.getSysteamClassLoader();
(五)双亲委派机制我们自己定义了一个java.lang.String类,这个是JDK提供的工具包中的String是相同的包名,而String是最顶级的类加载器------------引导类加载器负责的。
此处不放图了,反正并没有输出哈哈哈哈。
原理:我们自己定义的这个String类,编译成class文件后,开始被类加载器加载,首先从系统加载器开始,系统类加载器会检查自己加载过这个类没有,如果没有就继续往上一级的扩展类加载器检查,如果没有加载就继续往上,到最终的引导类加载器,引导类发现自己负责加载的模块中有String内,它就会加载JDK提供的核心类库中的String类。引导类加载器加载完成后,就不需要下两个类加载器加载了,说到底加载的String还是JDK的String,而不是我们自定义的。
双亲委派机制的作用大致就是为了保证系统文件不会被串改,导致程序不可用。
五、程序计数器(PC寄存器)
程序计数器是每个线程私有的,通过自增的方式,程序计数器存储着指向下一条字节码指令的地址。
如果程序计数器是共享的,那么会出现的问题就是在多线程的情况下,多个线程来回切换,无法知道自己这个线程执行到了哪个指定位置。
六、虚拟机栈
整体结构
1.局部变量表(Local Variables)局部变量变,也叫局部变量表或本地变量表,由一个数组来定义的。
局部变量表主要存储方法参数和定义在方法体内的局部变量,例如对象引用,基本数据类型,returnAddress(返回给pc寄存器方法结束后的下一条指令地址)。
局部变量表的内存大小在编译器就确认好了。
举例:
public class Dog {
public static void main(String[] args) {
test(1,1L);
Dog dog = new Dog();
dog.test2(1,1L);
}
public static void test(int a,long b){
long c = (long)a + b;
}
public void test2(int a,long b){
long c = (long)a + b;
}
}
test静态方法
start:方法开始的字节码指令的地址。
length:整个方法的字节码指令长度。
slot:可以看到基本数据类型int占一个槽位,long类型占两个槽位。
局部变量表的大小,例子中的两个long占四个位置,int占一个,总共五。
test2方法,注意,test2是一个对象方法
可以看到对象方法在局部变量表中多了一个变量,就是指向对象的this,默认占槽位开始的位置。
2.操作数栈(Operate Stack) 与局部变量表一样,均以字长为单位的数组。不过局部变量表用的是索引,操作数栈是弹栈/压栈来访问。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。
存储的数据与局部变量表一致含int、long、float、double、reference、returnType,操作数栈中byte、short、char压栈前(bipush)会被转为int。
数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。
值得注意的一点,如果代码中出现 int a = 1 + 1;那么运算的结果值在编译阶段就已经确定为2,不会再被操作数栈进行运算。
3.动态链接 (Dynamic Linking)每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
例子:
public class Dog {
public static void main(String[] args) {
Dog dog = new Dog();
dog.test1();
}
public void test1(){
System.out.println("开始调用test2");
test2();
}
public void test2(){
System.out.println("哈哈哈哈");
}
}
public void test1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #19 // String 开始调用test2
5: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
// Method test2:()V 此处调用了test2方法 #27代表符号引用
9: invokevirtual #27
12: return
LineNumberTable:
line 10: 0
line 11: 8
line 12: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/jimmy/Dog;
Constant pool: #1 = Methodref #2.#3 // java/lang/Object."":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // " ":()V #4 = Utf8 java/lang/Object #5 = Utf8 #6 = Utf8 ()V #7 = Class #8 // com/jimmy/Dog #8 = Utf8 com/jimmy/Dog #9 = Methodref #7.#3 // com/jimmy/Dog." ":()V #10 = Methodref #7.#11 // com/jimmy/Dog.test1:()V #11 = NameAndType #12:#6 // test1:()V #12 = Utf8 test1 #13 = Fieldref #14.#15 // java/lang/System.out:Ljava/io/PrintStream; #14 = Class #16 // java/lang/System #15 = NameAndType #17:#18 // out:Ljava/io/PrintStream; #16 = Utf8 java/lang/System #17 = Utf8 out #18 = Utf8 Ljava/io/PrintStream; #19 = String #20 // 开始调用test2 #20 = Utf8 开始调用test2 #21 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V #22 = Class #24 // java/io/PrintStream #23 = NameAndType #25:#26 // println:(Ljava/lang/String;)V #24 = Utf8 java/io/PrintStream #25 = Utf8 println #26 = Utf8 (Ljava/lang/String;)V #27 = Methodref #7.#28 // com/jimmy/Dog.test2:()V #28 = NameAndType #29:#6 // test2:()V #29 = Utf8 test2 #30 = String #31 // 哈哈哈哈 #31 = Utf8 哈哈哈哈 #32 = Utf8 Code #33 = Utf8 LineNumberTable #34 = Utf8 LocalVariableTable #35 = Utf8 this #36 = Utf8 Lcom/jimmy/Dog; #37 = Utf8 main #38 = Utf8 ([Ljava/lang/String;)V #39 = Utf8 args #40 = Utf8 [Ljava/lang/String; #41 = Utf8 dog #42 = Utf8 SourceFile #43 = Utf8 Dog.java
补充:
静态链接:如果被调用的目标方法在编译器可知,并且在运行期保持不变,这时候JVM可以直接讲方法的符号引用转化为直接引用。
动态链接:被调用的方法在编译器无法被确定,只能动态的转化。



