1.前端编译器将java文件编译为class字节码,后端编译器将字节码指令编译为机器指令,jit编译器将热点代码提前编译为机器代码存储在方法区中
2.Java编译器输入指令基本上是基于栈的指令集架构,另外的一种指令集架构是基于寄存器的指令集架构。
两种架构的区别:
- 基于栈式的架构的特点:
设计实现简单不占资源,使用零地址指令,执行过程依赖于操作数栈,指令更小,不需要硬件支持,可移植性更好 - 寄存器架构特点
性能优秀,花费更少指令完成操作,指令集往往都是一地址指令,二地址指令,三地址指 令(三地址指令就是含有三个地址和一个操作数)
- 常量池&运行时常量池
Constant pool:
#1 = Methodref #3.#21 // java/lang/Object."<init>":()V
#2 = Class #22 // com/wan_classmate/jvm/Test1
#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 Lcom/wan_classmate/jvm/Test1;
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 args
#14 = Utf8 [Ljava/lang/String;
#15 = Utf8 i
#16 = Utf8 I
#17 = Utf8 j
#18 = Utf8 k
#19 = Utf8 SourceFile
#20 = Utf8 Test1.java
#21 = NameAndType #4:#5 // "<init>":()V
#22 = Utf8 com/wan_classmate/jvm/Test1
#23 = Utf8 java/lang/Object
程序在运行的时候将常量池加载到内存里,就是运行时常量池
类加载准备阶段
为类变量分配内存并且设置该类变量初始值,即零值。而被final修饰的static,因为final在编译的时候就会分配了,准备阶段就会显示初始化-
启动类加载器
- 启动类加载器(BootStrap ClassLoader)是c/c++实现,嵌套在JVM内部。
- 用来加载核心类库,并不继承ClassLoader类。
- 扩展类加载器和应用类加载器都是由其加载。
- 启动类加载器只加载java、javax、sun开头的类。
-
扩展类加载器
- java语言编写,派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库,如果用户创建的jar包放在此目录下也会被加载。
-
应用程序加载器(AppClassLoader)
- 它负责加载环境变量classpath或属性 java.class.path 指定路径下的类库
- 一般来说java应用的类都是由其加载完成,通过ClassLoader.getClassLoader()方法可以获得。
JVM程序计数器
JVM的程序计数器寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存指令相关的现场信息,CPU只有把指令装载到寄存器才能够运行。JVM的程序计数器并不是广义上指的物理寄存器。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。CPU的指令寄存器存放下一条指令地址,JVM的程序计数器从软件层面实现该功能。堆和方法区有垃圾回收,栈和程序计数器没有垃圾回收。程序计数器不存在OOM内存溢出
虚拟机栈
- 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。当然最后编译成机器指令还是需要走CPU的寄存器
- 一个线程对应一个栈,一个方法对应一个栈帧
- 存放局部变量(八种基本数据类型,引用类型对象的引用地址)
- 栈帧的内部结构
- 局部变量表
大小在编译期的时候就已经确定,并且不会被更改 - 操作数栈
操作数栈,主要用于保存中间的结算结果,同时作为计算过程中变量的存储空间
栈的深度在编译期就已经确定好了,保存在方法的Code属性中
public static void main(String[] args) {
int i = 6;
int j = 6;
int k = i + j;
}
0: bipush 6 //将常量池中的 6 存入栈
2: istore_1 //弹出栈中数据放到 局部变量表 1 的位置
3: bipush 6 //将常量池中的 6 存入栈
5: istore_2 //弹出栈中数据放到 局部变量表 2 的位置
6: iload_1 //将局部变量表 1的位置加载到 栈中
7: iload_2 //将局部变量表 2的位置加载到 栈中
8: iadd //将栈中的数据弹出,交给cpu运算将结果存入栈中
9: istore_3 //将栈中数据弹出放到局部变量表3的位置
10: return
栈顶缓存技术,将栈顶元素全部缓存在CPU的寄存器当中,一次降低对内存的读写次数,提升执行引擎的执行效率。
-
动态链接
每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
所有变量和方法引用都作为符号引用保存在class文件的常量池中,比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
image.png -
方法返回地址
存放调用该方法的pc计数器的值。
一个方法的结束,有两种方式:- 正常执行完成
- 出现未处理的异常,非正常退出
A方法调用了B方法,程序计数器现在记录的是B方法的字节码指令了,B方法执行结束要返回到A方法继续执行剩下的字节码指令,但是现在不知道应该从什么地方继续执行A方法剩下的指令,所以在B方法的栈帧中增加一个方法返回地址,用来存储*调用B方法时候的PC程序计数器的值,然后字节码解释器就可以根据这个值获得下一条字节码指令的地址,并且修改程序计数器的值为这个值。
-
方法的调用
在JVM中,将符号引用转为调用方法的直接引用与方法的绑定机制相关。- 静态链接
当一个字节码被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接 - 动态链接
如果被调用的方法在编译期无法确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就称为动态链接。
虚方法与非虚方法
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行期时是不可变的。这样的方法称为非虚方法
- 静态方法、私有方法、final方法、实力构造器、父类方法都是非虚方法。
- 其他称为虚方法
- 静态链接
堆
Java堆区在JVM启动时即被创建,其空间大小就确定了,虚拟机规范规定,堆可以在物理上不连续,但在逻辑上应该被视为连续
方法结束弹出栈帧,局部变量表也就跟着失去了作用,对象引用也就没有了。堆中的对象变成了没有引用的对象,等待垃圾回收只能。Minor GC/Young GC
只有Eden区满的时候才会触发Minor GC同时也会清理 survior区。-
TLAB
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
- 所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
- 每个TLAB 只占Eden区1%
栈上分配
如果逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无须在堆上分配,也无须进行垃圾回收。这也是常见的堆外存储技术。元空间
元空间和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。相比于永久代不光改变名字,里面的结构也改变了。-
方法区
它用于存储已被虚拟机加载的类型信息、常量(JDK 7 及以后 字符串常量移到堆)、静态变量(JDK 7 及以后存放在堆中跟随Class对象)、即时编译器编译后的代码缓存等。
image.png
字符串常量池放在堆空间中,因为永久代回收率很低,在full gc的时候才会触发。而full gc是老年代、永久代不足时才会触发。这就导致字符串常量池回收效率不高。而我们开发的时候会有大量的字符创被创建,回收率低,导致永久代内存不足。放到堆里能及时回收。
从《Java虚拟机规范》所定义的概念模型来看,所有Class相关的信息都应该存在方法区中,但方法区该如何实现,《Java虚拟机规范》并未作出规定,这就允许不同虚拟机自己灵活把握的事情 。JDK7及其以后版本的HotSpot 虚拟机选择把静态变量与类型在java语言一端的映射Class对象存放在一起,存储在Java堆中。
Class对象是存放在堆区的,不是方法区,静态变量和Class对象在一起,这点很多人容易犯错。类的元数据(元数据并不是类的Class对象!Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的
常量池
执行引擎
- 当对字符串进行重新赋值时,需要重新制定内存区域赋值,不能使用原有的value进行赋值
- 当对现有字符进行操作时,也需要重新制定内存区域赋值,不能使用原有的value进行赋值
- 当调用String的replace() 方法修改指定字符或字符串时,也需要重新指定内存区域进行赋值,不能使用原有的value进行赋值
- GC Roots包括以下几类元素
- 虚拟机栈中引用的对象
比如各个线程被调用的的方法中使用的参数、局部变量等。 - 本地方法栈内JNI(通常说本地方法)引用的对象
- 静态属性引用的变量
- 方法区中常量引用对象
比如:字符串常量池里面的引用 - 所有被同步 Synchronized锁持有的对象
- Java虚拟机内部的引用
基本数据类型对应的Class对象,一些常驻的异常对象 ,系统类加载器
System.gc()
提醒jvm进行垃圾回收(full gc),并不一定会立马进行垃圾回收内存溢出
- 1 Java虚拟机内存设置大小不够 通过-Xms 和- Xmx 来这是对空间大小
- 2 代码中创建了大量的大对象,并且长时间不能被垃圾收集器回收(存在被引用)
30 Java中的引用
31 垃圾回收器
32 垃圾回收日志分析
33 符号引用&&直接引用
虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件不会保存各个方法和字段的内存信息,因此,这些字段和方法的符号引用不经过转换是无法被虚拟机直接使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类加载的过程中的解析阶段将其替换为直接引用,并翻译到具体的内存中。
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不可虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引目,那说明引用的目标必定已经存在于内存之中了
34 操作数栈
执行每条指令之前,Java虚拟机要求该指令操作数已经被压入操作数栈中。在执行指令时,Java虚拟机会将该指令所需要的操作数弹出,并且将指令的结果重新压入栈中。
35 指令
load 局部变量表数据入栈
const,push,ldc 常量入栈,表示的范围越来越大
store 出栈装入局部变量表
36 类的加载过程