前言
- 前边的 JVM知识体系学习 1-4讲的是
class loader (类加载)
、类对象
等知识。那这里讲的就是 类加载之后运行时 的数据区域,也就是java 运行时数据区
(java runtime data area),如下图所示: - JVM 文档 是 JDK 13版本
- 本博客记录了JVM运行时区域的内容
- 线程私有:JVM栈、本地方法栈、PC(程序计数器)
- 线程公有:Java 堆(在第六节里详细讲解)、方法区(7之前永久代实现,7之后元空间实现)
- 还记录了 栈中指令涉及局部变量表、操作数栈的实现过程。举了多个例子。
零、问题引入
很简单的小程序,为什么执行第六行代码是8,执行第7行代码是9呢。引入问题,后面解答。
看了答案之后的思维发散:说明 i++ 还是 ++i,都不是原子操作,在字节码层面是由三个指令的,(题外话,也就是JVM知识体系三中的会可能发生指令重排的情况。)。
package com.mashibing.jvm.c4_RuntimeDataAreaAndInstructionSet;
public class TestIPulsPlus {
public static void main(String[] args) {
int i = 8;
i = i++;
// i = ++i;
System.out.println(i);
}
}
一、java Runtime Data Area
0、概述
- 数据运行时内存包含如下
DM 是JDK1.4之后出现的,为NIO部分。 - 对应的文档在哪呢
- https://docs.oracle.com/javase/specs/index.html
- 上面是java language,下面是 JVM ,点击PDF即可。
- 位置如下:
- https://docs.oracle.com/javase/specs/index.html
1、Program Counter 程序计数器(线程私有)
存放指令位置
虚拟机的运行,类似于这样的循环:
while( not end ) {
取PC中的位置,找到对应位置的指令;
执行该指令;
PC ++;
}
- JVM是这样说的:
- Each Java Virtual Machine thread has its
own
pc (program counter) register. - At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method for that thread.
- If that method is not native , the pc register contains the address of the Java Virtual Machine instruction currently being executed.
- Each Java Virtual Machine thread has its
2、JVM stacks(重点)(线程私有)
JVM 是这样说的:
- Each Java Virtual Machine
thread
has aprivate
Java Virtual
Machine stack, created at the same time as the thread. - A Java Virtual Machine stack stores
frames
- Each Java Virtual Machine
- 每一个线程对应一个栈,每个方法对应一个栈针。JVM 虚拟机 所管理的
3、Native Method Stacks本地方法栈(线程私有)
JVM 是这样说的:
- An implementation of the Java Virtual Machine may use
conventional stacks called native method stacks
- An implementation of the Java Virtual Machine may use
- c 和 c++。java 调用了 JNC ,调用了 c 和 c++时就会调用 ,没法调试和调优
4、Direct Memory
JDK 1.4 版本之后 ,增加了一个新的 Direct Memory 即直接内存,NIO的内容
。 所有的内存都归java 虚拟机(JVM)直接管理,为了增加IO的效率,在JDK1.4之后增加了 DM(直接内存),从java虚拟机内部 可以访问操作系统管理的内存的。用户空间可以访问内核空间的内存。
5、Method Area 方法区(重点)(线程公有)
a、MA
JVM 是这样说的:
- The Java Virtual Machine has a method area that is
shared
among all Java Virtual Machine threads. - It stores per-class structures
- The Java Virtual Machine has a method area that is
存储各种常量池,包含 :(JVM知识体系学习一中的常量池就是第一个 Class常量池)
Class文件中的常量池
(存放存放两大常量:字面量(Literal)和符号引用(Symbolic References)。
(下面小节专门讲解常量池。很详细)运行时常量池
(run-time constant pool,运行之后Class文件中的常量池加载到内存即运行时常量池)字符串常量池
(1.7 和静态变量放到堆上)、基本数据类型包装类常量池(6个)
(Float 和Double 没有)、静态变量
上面这 6 种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。
JVM 版本的 方法区
Perm Space
永久代 (<=JDK1.6版本)
Class常量池、字符串常量池、运行时常量池、静态变量、基本数据类型包装类常量池 位于PermSpace
不会触发FGC清理
大小启动的时候指定,不能变Perm Space
永久代 (=JDK1.7版本,开始去永久代)
字符串常量池、静态变量 转移到堆上,其他还在永久代
不会触发FGC清理Meta Space
元空间 (>=JDK1.8版本)
字符串常量池 位于堆
会触发FGC清理
不设定的话,最大就是物理内存
b、常量池
常量池的资料
java中的常量池的分类:https://cloud.tencent.com/developer/article/1450501#
JAVA常量池,一篇文章就足够入门了。(含图解)https://blog.csdn.net/qq_41376740/article/details/80338158
JVM常量池主要分为
Class文件常量池
、运行时常量池
,全局字符串常量池
,以及基本类型包装类对象常量池(Double和Folat这两种浮点数类型的包装类没有实现常量池技术)
。运行时常量池 中的常量是 运行之后 Class 文件中的常量池加载到MA中的。
c、JDK1.6、JDK1.7、JDK1.8 内存模型演变
- JDK1.6、JDK1.7、JDK1.8 内存模型演变:https://www.cnblogs.com/xiaofuge/p/14244755.html
- JDK 1.6:有永久代,静态变量存放在永久代上。
- JDK 1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。开始去永久代。逐渐的减少永久代的使用。
- JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的元空间。但字符串常量池仍然存放在堆上。
6、Run-Time Constant Pool(属于MA)
- JVM 是这样说的:
- A run-time constant pool is a per-class or per-interface run-time
representation of the constant_pool table in a class file
- A run-time constant pool is a per-class or per-interface run-time
运行时常量池
位于方法区中
7、Heap 堆(线程共有)
- JVM 是这样说的:
- The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads.
- The heap is the run-time data area from which memory for
all class instances and arrays is allocated.
GC时好好聊:JVM知识体系学习六第三章: https://blog.csdn.net/qq_40036754/article/details/128651806
8、总体图
从下图看出:
- PC、JVM stacks、native method stack 是线程独有的
- heap、method area 是线程共享的
二、java stack 中的 栈针Frame
- A frame is used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions. (框架用于存储数据和部分结果,以及执行动态链接、方法的返回值和分派异常)
- 每一个线程是一个栈针,每个方法对应一个栈帧,比如main()方法应该第一个进入栈中,调用的方法依次入栈(作为栈针)。
1、栈针的四部分
- 每一个栈针包括 以下四部分:
Local Variable Table
:本地变量表Operand Stack
:操作数栈- 对于long的处理(store and load),多数虚拟机的实现都是原子的
jls文档里 17.7,没必要加volatile
Dynamic Linking
:动态链接- 文档详解:https://blog.csdn.net/qq_41813060/article/details/88379473
jvms文档的 2.6.3
- 就是class文件里的常量池 constant pool(常量池)里的
符号链接
,看有没有解析,如果没有解析就动态解析;如果解析了则直接使用,所以Dynamic Linking
就是这个东西。 - 比如:有a()方法和b()方法,a()方法中调用了b()方法,那么执行a()方法中的b()方法时,则需要去常量池中去找b()方法,这个找的过程就是动态链接。
Dynamic Linking
return address
:返回地址。- a() 调用了 b(),方法a调用了方法b, b方法的返回值放在什么地方,这就是 return address。
2、Local Variable Table 局部变量表
a、问题引入的代码
package com.mashibing.jvm.c4_RuntimeDataAreaAndInstructionSet;
public class TestIPulsPlus {
public static void main(String[] args) {
int i = 8;
i = i++;
// i = ++i;
System.out.println(i);
}
}
b、查看字节码中的本地变量表
如上图所示:
LineNumberTable
:行号LocalVariableTable
:本地变量表,包括两个,一个是main()
方法的形参args
,一个是 变量i
。- 右边绿色的
cp info #15
:cp,是 constant pool 常量池(在MA中);所以是常量池第15块地址存储的,可以点进去查看。
3、Operand Stack(解释i++和++i的问题)
a、解释指令集:i++
先看下图的指令
指令一
bipush 8
:(byte int push)就是 讲byte 转成 int 型 push 到栈中。即 将8 压入栈中。- 点击指令,可以直接进入到指令的解释网页
- 下图
指令二
istore_1
:通过下面指令解释可以看出,将操作栈中的数出栈,赋值到局部变量表(二.2)中 下标为2的变量中。即将8出栈赋值到i上。- 指令解释如下
- 指令解释如下
到了这儿,
i=8
这条语句就执行完了,就是通过 1和 2这两条指令完成的。指令三
iload_1
:取出局部变量表下标为1的值,然后入操作栈;即 i=1取出,入操作栈。- 解释如下
- 解释如下
指令四
iinc 1 by 1
:局部变量表下标为1 的值 自加1。此时 局部变量表下标为1 的 i 值 变成了 9。- 解释
- 解释
指令五
istore_1
:同指令2,将操作栈中的值出栈,然后赋值到局部变量表下标为1的变量 i 上。所以将8赋值到局部变量表 i=9上,则为8。此时,到这里,完美的解释了
i++
的操作。i++
的指令一共有三步,也就是第三、第四、第五个指令。
b、解释指令集:++i
执行的字节码如下图:唯一的不同,第三行和第四行交换了。 ++i
的指令也是第三、第四、第五个指令,共三个。
解释如下:
bipush 8
:将8压栈istore_1
:8出栈,赋值到 局部变量表 下标为1 的i
上,完成i=8
。iinc 1 by 1
:局部变量表下标为1 的i变量的值,自加1,完成++i
,i = 9。iload_1
:局部变量表下标为1 的i 变量 的值 9 取出,入操作栈。istore_1
:操作栈 出栈,赋值 到 局部变量表 下标为1 的i
上,为9。
三、栈的执行流程
1、案例 1_sipush 指令
a、代码
b、字节码指令集
c、指令解说
和上面的案例相比,这里第一个指令是 sipush
(原来是 bipush
), 原因是: 200>127
,所以不再是 byte
类型,而是 short
类型 转成 int 类型。
2、案例 2_局部变量表中的this
a、代码
b、字节码指令集
c、指令解说
- 第一个指令是
sipush
是正常的,因为300>127。 - 但是为什么第二个指令的下标变成2了呢,原因是: 只要是
非 static
方法都有一个this
变量在局部变量表中,然后形参k
是第二个,则i
是第三个,所以这里的指令下标是2
。
前边的main 方法没有 this,是因为 main 方法有static。
- 要点:非static的局部变量表的第一个是this。
3、案例 3_加法指令_iadd
a、代码
b、字节码指令集
c、指令解说
- 先说下局部变量表,有
this,a,b,c
共四个。 - 指令描述如下:
- 指令
iload_1
iload_2
分别按顺序入操作栈 ; - 指令
iadd
则是将int
类型的两个数出栈 相加 在入栈,此时栈中只有7; - 指令
istore_3
则将操作栈 栈顶元素7
出栈 赋值到局部变量表下标为3
的c
中
- 指令
4、案例 4_创建对象指令
a、代码
b、字节码 指令集
m1()方法
main()方法
c、指令解说
- 从下图中也可以看出,线程栈中有两个栈针,一个
main()
方法,一个m1()
方法。 - 局部变量表:
main()
中:args
、h
。m1()
中:this
、i
。
- 执行顺序如下:
new
指令先 创建一个对象(load、linking、initializing等初始化),dup
指令,复制操作数栈栈顶的值,再入操作数栈。此时操作数栈中有两个一样的Hello_02
对象,都指向对象的地址。invokespecial
指令,是执行 默认的构造函数init
。到了这儿,对象构造才算完毕,然后弹出栈就没啦,此时操作数栈中只有一个对象了。(invoke 指令有五种,后面在讲)astore_1
指令(注意这里是a
开头),是将操作数栈中的对象出栈赋值
到h
上。aload_1
指令,从局部变量表中在取出 入操作数栈。invokevirtual
指令,执行m1()
栈针,然后出栈
。- 然后执行
sipush 200
、istore_1
和return
指令。然后返回地址
- 然后执行
- 然后再执行,
return
执行完即可。
- 注意的点:
dup
指令:就是将操作栈中最上面的值复制一份,再压入操作栈中。半初始化状态
,就是new
和invokevirtual
中间有个dup
指令。
(上面main方法的栈针中的局部变量表中的this 错了,应该是args)
- 可以想到 栈溢出(over stack)
- 问题:DCL(双重检查,在JVM知识体系学习三 中有详细说明) 为什么要使用 volatile
- 因为指令之间可能会
指令重排
- 因为指令之间可能会
5、案例 5
a、案例5_1
i、代码
ii、字节码指令集
- m1方法:
- main方法
iii、指令解说
m1() 方法
bipush
:看文档;就是将 byte 转成 int 类型的值,压入操作数栈ireturn
:返回int 值,并出栈
main() 方法
- 其余指令在上面案例中都讲过,我这里就稍微描述下
new
需要仔细看文档 。开辟对象地址,并不是完整的创建对象,然后将对象引用入操作数栈。dup
复制对象引用入操作数栈。invokespecial
:调用构造函数,完成对象初始化,并出栈,此时占中还有一个对象引用。astore_1
:出栈,存储到局部变量表中下标为1
的h
中。aload_1
:取出下标为1
的局部变量边的位置的值,入操作数栈。invokevirtual
:调用m1
方法- 走
m1
的字节码指令集。
- 走
pop
函数,因为是返回,直接出栈即可。
- 注意是main 方法 指令中倒数第二个指令,与下个案例做对比。
b、案例5_2
i、代码
ii、字节码指令集
m1方法(和上个案例一样)
main方法
iii、指令解说
- 这里的指令与上一个案例只有一个地方不一样,就是main方法倒数第二个指令
- 这里是将操作数栈中的值取出,然后赋值给局部变量表中下标为
2
的变量i
中。
6、案例 6 (递归指令集解说)
a、代码
b、字节码指令集:
m方法:
main方法
c、指令解说
- 有些难度,递归指令的描述。
四、常用指令
目前还没面试官问过,增长见识和知识点就OK。
1、常用命令
- store
- load
- pop
- mul
- sub
- invoke
2、invoke-共5个指令
InvokeStatic
:调用静态方法InvokeVirtual
:自带多态,执行类的成员方法。InvokeInterface
:调用接口方法。InovkeSpecial
:- 可以直接定位,不需要多态的方法
- private 方法 , 构造方法
InvokeDynamic
:- 1.7版本添加的指令,这时 java 开始支持动态语言。
- JVM最难的指令
lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令
package com.mashibing.jvm.c4_RuntimeDataAreaAndInstructionSet;
public class T05_InvokeDynamic {
public static void main(String[] args) {
I i = C::n;
I i2 = C::n;
I i3 = C::n;
I i4 = () -> {
C.n();
};
System.out.println(i.getClass());
System.out.println(i2.getClass());
System.out.println(i3.getClass());
//for(;;) {I j = C::n;} //MethodArea <1.8 Perm Space (FGC不回收)
}
@FunctionalInterface
public interface I {
void m();
}
public static class C {
static void n() {
System.out.println("hello");
}
}
}
3、JDK1.8之前的一个BUG
for(;;) {I j = C::n;}
- JDK 在小于1.8之前叫
Perm Space(永久代)
,这时上面的代码会产生大量的class类,会存放在MethodArea(方法区)
,这时在1.8之前MethodArea
会产生一个巨大的bug
:FGC(Full GC)不回收、不清理
。 - 所以在JDK1.8之前,会产生OOM,内存溢出,但是在1.8之后,如果清除不掉,也会产生OOM。
- JDK 在小于1.8之前叫