JVM
所有图片来源于网络 如有侵权,立删
总体概念:
JVM图:
引导类加载器:BootStrapClassLoader
扩展类加载器:ExtensionClassLoader
系统类加载器:ApplicationClassLoader
PC寄存器又叫程序计数器
方法区存放类加载器加载的类信息存放在方法区(还有运行时的常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件常量池部分的内存映射))
堆占最大的内存空间所有的数据共享,创建java对象
虚拟机栈一个线程对应一个栈,里面的一个又一个结构叫做栈帧,里面包含了局部变量表、操作数栈、动态链接、方法返回地址
javap -v反编译命令
一、类加载器
1.类加载过程:
过程:加载、链接(验证、准备、解析)、初始化
简图
详情图
类加载器ClassLoader只负责class文件的加载(class文件在文件开头都有特定的文件表示CA FE BA BE见下图),至于它是否能运行,是由执行引擎ExecutionEngine来决定的,好比过年催婚,亲戚(类加载器)介绍对象给你,把别人给你介绍过来,能不能成就看你自己(执行引擎)。
class文件的标识:
2.加载Loading阶段
- 通过类的全限定类名获取定义此类的二进制文件流
- 讲这个二进制文件流所代表的静态储存结构转化为方法区的运行时数据结构(元空间)
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
3.链接
1. 验证Verification阶段
目的在于确保Class文件的字节流中包含的信息符合当前虚拟机要求,确保加载类的正确性,不会对虚拟机有危害,如果class文件不合法,会报错VerifyError
主要包含四种验证方法
1. 文件格式验证
2. 元数据验证
3. 字节码验证
4. 符号引用验证
2. 准备Preparation阶段
为类变量分配内存,并且设置该类变量的默认初始值,即零值
但是准备阶段不会为被final修饰的static赋值,因为final在编译的时候就分配了,准备阶段只会显式初始化,同时也不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆当中
例如:
在类中定义了变量
private static int a = 2;
a在准备阶段,会被赋值为0(零值),在初始化阶段才会被赋值为2
常见零值:
int:0
float、double:0.0
char:/u0000
boolean:false
引用类型:null
3. 解析Resolution阶段
将常量池中的符号引用转化为直接引用的过程,解析主要针对类、接口、字段、类方法、接口方法、方法类型等,在常量池中对应为:CONSTANT_Class_info、CONSTANT_Filedref_info、CONSTANT_Methodref_info等
4.初始化Initialization阶段
初始化阶段就是执行类构造器方法()的过程,注意:这个方法不是我们定义的方法也不需要我们定义,是javac编译器自动收集类中的所有类的变量的赋值动作和静态代码块中的语句合并而来的,构造器方法中的指令按照语句在源文件中出现的的顺序执行,()方法不同于我们写的类构造器方法,()方法,同样满足先执行父类的()方法,同时一个类的()会在多线程下被同步加锁。要是没有静态代码块或者静态变量之类的就没有()方法
二、类加载器
在JVM中加载器只分类两类,一类是引导类加载器(BootstrapClassLoader,用C写的)和用户自定义加载器(用java写的),结合上图也就是说,扩展类加载器(ExtensionClassLoader)和系统类加载器(SystemClassLoader)以及用户自定义加载器都属于是用户自定义类加载器。本质上定义的就是所有直接或间接的继承自ClassLoader类的类加载器都属于是用户自定义类加载器。
public class ClassLoaderTest {
public static void main(String[] args){
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层,扩展类加载器
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);//sun.misc.Launcher$ExtClassLoader@1b6d3586
//获取引导类加载器
ClassLoader bootstrapClassLoader = parent.getParent();
System.out.println(bootstrapClassLoader);//null
//获取用户自定义类加载器
ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(appClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取String的类加载器:String类使用的类加载器是引导类加载器,java的核心类库都是使用引导类加载器进行加载的
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader);//null
}
}
对几种类加载器的解释:
1.引导类加载器(BootstrapClassLoader):
- BootstrapClassLoader使用的是C/C++编写的,嵌套在JVM的内部
- BootstrapClassLoader负责加载java的核心库(JAVA_HOME/jre/rt.jar、resource.jar或者sun.boot.class.path路径下的内容)
- BootstrapClassLoader是单独的类,没有继承java.lang.ClassLoader
- 加载扩展类和应用程序类加载器,BootstrapClassLoader是他们的父加载器
- BootstrapClassLoader只加载包名为java、javax、sun开头的类
- 获取引导类加载器加载类库的位置
//获取引导类加载器加载的位置
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urLs){
System.out.println(url);
}
// file:/D:/jdk/jre/lib/resources.jar
// file:/D:/jdk/jre/lib/rt.jar
// file:/D:/jdk/jre/lib/sunrsasign.jar
// file:/D:/jdk/jre/lib/jsse.jar
// file:/D:/jdk/jre/lib/jce.jar
// file:/D:/jdk/jre/lib/charsets.jar
// file:/D:/jdk/jre/lib/jfr.jar
// file:/D:/jdk/jre/classes
2.扩展类加载器(ExtensionClassLoader):
- 由java编写,由sun.misc.Launcher$ExtClassLoader实现
- 继承自ClassLoader
- 父加载器为BootstrapClassLoader
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录下的jre/lib/ext子目录下加载类库。如果用户创建的jar放在此目录下,也会由扩展类加载器进行加载
四、双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作原理
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器(BootstrapClassLoader);
3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
了解一下沙箱安全机制
扩展:
-
JVM中判断两个class对象是否是同一个类
1.类的完整类名必须一样
2.加载这个类的类加载器也必须一样 -
Java程序对类的使用分为主动使用和被动使用
(1)主动使用:
1.·创建类的实例
2.访问某个类或接口的静态变量,或者对该静态变量赋值
3.调用类的静态方法
4.反射(比如:class.forName ( “com.zjj. Test”) )
5.初始化一个类的子类
6.Java虚拟机启动时被标明为启动类的类
7.JDK 7开始提供的动态语言支持:
java . lang .invoke.MethodHandle实例的解析结果
REF_getstatic、REF_putstatic、REF_invokestatic句柄对应的类没有初始化,则初始化
(2)被动使用:
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
五、运行时数据区学习
运行时数据区:
每个线程独立的占有各自的程序计数器(PC寄存器)、虚拟机栈、本地方法栈;线程之间共享堆(heap)、堆外内存(方法区(Method area和永久代、jdk8之后叫做元空间、元数据区)、代码缓存)
1.程序计数器(PC寄存器)
- 介绍:用来储存指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令
- PC寄存器是线程私有的,,每个寄存器的生命周期和线程的生命周期一致。程序计数器会储存当前线程正在执行的Java方法的JVM指令地址;如果执行的是native方法,则是未指定值(undefined),不会出现OutOfMemoryError(内存溢出)的情况
- 程序
public class PCtest {
public static void main(String[] args) {
int a = 10;
int b = 20;
int c = a + b;
}
}
//反编译过后
D:\IDEA\JVM\out\production\JVM>javap -v PCtest.class
Classfile /D:/IDEA/JVM/out/production/JVM/PCtest.class
Last modified 2020-11-14; size 429 bytes
MD5 checksum bfe52a4c6ffd3375382821ed3168267f
Compiled from "PCtest.java"
public class PCtest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#21 // java/lang/Object."<init>":()V
#2 = Class #22 // PCtest
#3 = Class #23 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 LPCtest;
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 args
#14 = Utf8 [Ljava/lang/String;
#15 = Utf8 a
#16 = Utf8 I
#17 = Utf8 b
#18 = Utf8 c
#19 = Utf8 SourceFile
#20 = Utf8 PCtest.java
#21 = NameAndType #4:#5 // "<init>":()V
#22 = Utf8 PCtest
#23 = Utf8 java/lang/Object
{
public PCtest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LPCtest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
//下面左边的数字0,2,3,5,6,7,8,9,10就是PC寄存器当中的指令地址
//右边的就是命令bipush
0: bipush 10 //取出10
2: istore_1 //保存10 索引为1
3: bipush 20 //取出20
5: istore_2 //保存20 索引为2
6: iload_1 //取出索引为1的值
7: iload_2 //取出索引为2的值
8: iadd //相加
9: istore_3 //将结果保存到索引为3的位置上
10: return //结束
LineNumberTable:
line 3: 0
line 4: 3
line 5: 6
line 6: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
3 8 1 a I
6 5 2 b I
10 1 3 c I
}
SourceFile: "PCtest.java"
4.面试中的两个问题
(1)使用PC寄存器储存字节码指令地址有什么用?或者问为什么使用PC寄存器记录当前线程的执行地址?
因为CPU需要不停地切换各个线程,这时候切换回来以后,需要知道接着从哪里开始继续执行,JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
2.虚拟机栈(JVM Stacks)
1.特点
1.粗略地来讲栈储存的是执行的顺序,堆是数据存放的地方。即栈是顺序,堆是储存空间
2.JVM Satcks是线程私有的,生命周期和对应的线程一致,主要保存方法的局部变量、部分结果,并参与方法的调用和返回(与栈帧相对应,即对应的局部变量保存在对应的栈帧中)
3.虚拟机栈内部保存了一个个栈帧,一个栈帧就代表一个java中的方法调用
4.栈本身就有进栈和出栈的方法,方法执行完了,栈就空了,因此栈不存在垃圾回收
5.栈的大小可以固定不变也可以动态改变,栈的大小决定了方法执行的深度,但是都有可能出现StackOverFlowError,可以通过修改***-Xss***来设置线程的最大虚拟机栈内存,单位是K(k)、M(m),不写单位代表是Byte(IDEA:Run——>Edit Configuration中的VM options进行设置)
2.栈的储存结构和运行原理
1.每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在,在该线程上运行的每个方法都是一个栈帧,栈帧是一个内存区块,是一个数据集
2.在一个活动的线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧成为当前栈帧,与当前栈帧对应的方法成为当前方法,调用当前方法的类叫做当前类,执行引擎运行的所有字节码指令只针对当前栈帧进行操作。如果该方法中调用了其他方法,对应的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧
3.java中有两种返回函数的方式。一种是正常的的函数返回,即return;还有一种就是抛出异常。不管使用哪种方式,都会导致栈帧的弹出。
4.不同线程中所包含的栈帧是不允许互相引用的,即一个线程一个虚拟机栈,即栈帧的私有性。
3.栈帧的内部结构
1.每个栈帧中储存着局部变量表(Local Variables)、操作栈数(Operand Stack)(或表达式栈)、动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)、方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)、和其他的一些附加信息
2.名词解释:
(1)局部变量表:
- 局部变量表又称为局部变量数组或者本地变量表。
- 其内部定义为一个数字数组,主要用于储存方法参数和定义在方法体内部的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference)、以及ReturnAddress类型。
- 因为局部变量表储存在栈帧内部是线程私有的,所以不存在数据的安全问题。
- 局部变量表所需的容量大小在编译器就被确定下来了,并保存在方法的Code属性的Maximum local variables数据相中。在方法运行期间是不会改变局部变量表的大小的。
(2)操作数栈
(3)动态链接
(4)方法返回地址
(5)附加信息