JDK体系结构
JDK:JDK(Java Development Kit) 是 Java 语言的软件开发工具包(SDK)。
JDK :1.命令工具 (java/javac/jar)
2.JRE:(JavaRuntimeEnvironment) 指Java运环境, 包括各种Libraies(类库,表现形式jar包),比如:lang and util Base Libraries,Other Base Libraries 等,最重要的是包含JVM Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言的跨平台特性
Jvm 整体结构及JMM内存模型
1.JVM由三部分组成:
- 类装载子系统
- 运行时数据区(内存模型)
- 字节码执行引擎
其中类装载子系统是C++实现的,他把类加载进来,放入到虚拟机中。这一块就是之前分析过的类加载器加载类,采用双亲委派机制,把类加载进来放入到JVM虚拟机中。
然后,字节码执行引擎去虚拟机中读取数据。字节码执行引擎也是C++实现的。我们重点研究运行时的数据区。
2.运行时数据区的构成
运行时数据区主要由5个部分构成:堆,栈,本地方法栈,方法区,程序计数器。
3.JVM三部分密切配合工作
下面我们来看看一个程序运行的时候,类装载子系统,运行时数据区,字节码执行引擎是如何密切配合工作的? 我们举个列子说明一下:
package com.jason;
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
当我们在执行main方法的时候,都做了什么事情呢? 第一步: 类加载子系统加载Math.class类, 然后将其丢到内存区域, 这个就是前面博客研究的部分,类加载的过程, 我们看源码也发现,里面好多代码都是native本地的, 是c++实现的
第二步: 在内存中处理字节码文件, 这一部分内容较多, 也是我们研究的重点, 后面会对每一个部分详细说
第三步: 由字节码执行引擎执行java虚拟机中的内存代码, 而字节码执行引擎也是由c++实现的
这里最核心的部分是第二部分运行时数据区(内存模型), 我们后面的调优, 都是针对这个区域来进行的.
下面详细来说内存区域
四. 栈
栈是用来存放变量的
4.1. 栈空间
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
开始运行main方法时,会创建一个线程,创建线程的时候,就会在大块的栈空间分配一块小空间,用来存放当前要运行的线程的变量,这里math这个局部变量就会保存在分配的小空间里,在这里我们运行了math.compute()方法,这里面的a,b,c这样的局部变量也放在分配的小空间里面
如果多个线程,会再次在栈空间分配一块小的空间,用来存放新的线程内部变量
同样是变量, main方法中的变量和compute()方法中的变量放在一起么?他们是怎么放得呢?这就涉及到栈帧的概念。
4.2. 栈帧
1.什么是栈帧呢?
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
以上面代码为例 :当我们启动一个线程执行main 方法的时候,会先在栈空间分配一小块空间。然后在这一小块空间中分配一块区域给main()方法,这个区域就叫做栈帧空间。当程序运行到compute()方法的时候,就会去调用compute方法,这时候会在分配一个栈帧空间,给compute()方法使用。
注:一个方法对应一个栈帧内存区域
2.为什么要将一个线程中的不同方法放在不同的栈帧空间里面呢?
一方面: 我们不同方法里的局部变量是不能相互访问的. 比如compute的a,b,c在main里不能被访问到。使用栈帧做了很好的隔离作用。
另一方面: 方便垃圾回收, 一个方法用完了, 值也返回了, 那他里面的变量就是垃圾了, 后面直接回收这个栈帧就好了.
3.java内存模型中的栈算法
先进后出
4.3 栈帧的内部构成
栈帧内部有很多部分,我们主要关注下面四个部分: 1.局部变量表 2.操作数栈 3.动态链接 4.方法出口
4.3.1 局部变量表
存放局部表量
4.3.2操作数栈
那么操作数栈,动态链接, 方法出口他们是干什么的呢? 我们用例子来说明操作数栈
那么这四个部分是如何工作的呢?
我们用代码的执行过程来对照分析.
我们要看的是jvm反编译后的字节码文件, 使用javap命令生成反编译字节码文件.
javap命令是干什么用的呢? 我们可以查看javap的帮助文档
主要使用javap -c和javap -v
javap -c: 对代码进行反编译
javap -v: 输出附加信息, 他比javap -c会输出更多的内容
下面使用命令生成一个Math.class的字节码文件. 我们将其生成到文件
javap -c Math.class > Math.txt
打开Math.txt文件, 如下. 这就是对java字节码反编译成jvm汇编语言.
Compiled from "Math.java"
public class com.jason.Math {
public static int initData;
public static com.jason.User user;
public com.jason.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/jason/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: return
static {};
Code:
0: sipush 666
3: putstatic #5 // Field initData:I
6: new #6 // class com/jason/User
9: dup
10: invokespecial #7 // Method com/jason/User."<init>":()V
13: putstatic #8 // Field user:Lcom/jason/User;
16: return
}
这就是jvm生成的反编译字节码文件.
我们以compute()方法为例来说说这个方法是如何在在栈中处理的
源代码 public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
反编译后的jvm指令
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
jvm的反编译代码是什么意思呢? 我们对照着查询手册
0: iconst_1 将int类型常量1压入操作数栈, 这句话的意思就是先把int a=1;中的1先压入操作数栈
1: istore_1 将int类型值存入局部变量1-->意思是将int a=1; 中的a放入局部变量表的第二个位置, 然后让操作数栈中的1出栈, 赋值给a
注意: 这里的1不是变量的值, 他指的是局部变量的一个下标. 我们看手册上有局部变量0,1,2,3 0表示的是this, 1表示将变量放入局部变量的第二个位置, 2表示放入第三个位置.
对应到compute()方法,0表示的是this, 1表示的局部变量a, 2表示局部变量b,3表示局部变量c
2: iconst_2 将int类型常量2压入栈-->意思是将int b=2;中的常量2 压入操作数栈
3: istore_2 将int类型值存入局部变量2 -->意思是将int b=2;中的变量b存入局部变量表中第三个位置, 然后让操作数栈中的数字2出栈, 给局部变量表中的b赋值为2
4: iload_1 加载局部变量第1个变量压入操作数栈
要想更好的理解iload_1,我们要先来研究程序计数器。
程序计数器
在JVM虚拟机中,程序计数器是其中的一个组成部分。 重点:程序计数器是每个线程独有的,他用来存放马上要执行的哪行代码的内存位置,也可以叫行号。我们看到jvm反编译代码里,都会有 0 1 2 3 这样的位置(如下图),我们可以将其认为是一个标识。而程序计数器简单理解为是记录这些数字的。而实际上这些数字对应的是内存里的地址
当字节码执行引擎执行到第4行的时候,将执行到4: iload_1,我们可以简单理解为程序计数器记录代码位置是4,我们的方法Math.class 是放在方法区的,字节码执行引擎执行,每次执行完一行代码,字节码执行引擎都会修改程序计数器的位置,让其向下移动一位
java虚拟机为什么要设计程序计数器呢?
因为多线程。当一个线程正在执行, 被另一个线程抢占了cpu, 这时之前的线程就要挂起, 当线程2执行完以后, 再执行线程1. 那么线程1之前执行到哪里了呢? 程序计数器帮我们记录了. 4: iload_1 从局部变量1中装载int类型值--> 意思是从局部变量表的第二个位置取出int类型的变量值, 将其放入到操作数栈中.此时程序计数器指向的是4
4.3.3 动态链接
在之前说过什么是动态链接: 参考文章: juejin.cn/post/705087…
搜索:动态链接
在程序运行过程中,把符号引用转换变为直接引用
静态链接是在程序加载的时候一同被加载进来的. 通常用静态常量, 静态方法等, 因为他们在内存地址中只有一份, 所以, 为了性能, 就直接被加载进来了
而动态链接, 是使用的时候才会被加载进来的链接, 比如compute方法. 只要在执行到math.compute()方法的时候才会真的进行加载.
4.3.4 方法出口
当我们运行完compute()方法以后, 还要返回到main方法的math.comput()方法的位置, 那么他怎么返回回来呢?返回回来以后该执行哪一句代码了呢?在进入compute()方法之前,就在方法出口里记录好了, 我应该如何返回,返回到哪里. 方法出口就是记录一些方法的信息的.
五. 堆和栈的关系
上面研究了compute()方法的栈帧空间,再来看一下main方法的栈帧空间。整体来说,都是一样的,但有一块需要说明一下,那就是局部变量表。来看看下面的代码
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
main方法的局部变量和compute()有什么区别呢? main方法中的math是一个对象. 我们知道通常对象是被创建在堆里面的. 而math是在局部变量表中, 记录的是堆中new Math对象的地址。
说的明白一些,math里存放的不是具体的内容,而是实例对象的地址。
那么堆和栈的关系就出来了,如果栈中有很多new 对象,这些对象是创建在堆里面的,栈里面存的是这些创建的对象的内存地址。
六. 方法区
我们可以通过javap -v Math.class > Math.txt命令, 打印更详细的jvm反编译后的代码
这次生成的代码,和使用javap -c生成的代码的区别是多了Constant pool常量池。这些常量池是放在哪里的呢?放在方法区。这里看到的常量池叫做运行时常量池。还有很多其他的常量池,比如:八大数据类型的对象常量池,字符串常量池等。
这里主要理解运行时常量池。运行时常量池放在方法区里。
方法区主要有哪些元素呢?
常量 + 静态变量 + 类元信息(就是类的代码信息)+运行时常量池
在Math.class类中, 就有常量(常量和变量都属于变量,只不过常量是赋过值后不能再改变的变量,而普通的变量可以再进行赋值操作)和静态变量
public static final int initData = 666; //常量
public static User user = new User();//静态变量
他们就放在方法区里面. 这里面 new User()是放在堆里面的, 在堆中分配了一个内存地址,而user对象是放在方法区里面的. 方法区中user对象指向了在堆中分配的内存空间。
堆和方法区的关系是: 方法区中对象引用的是堆中new出来的对象的地址
类元信息: Math.class整个类中定义的内容就是类元信息, 也放在方法区。
七. 本地方法栈
本地方法栈是有c++代码实现的方法. 方法名带有native的代码.
比如:
new Thread().start();
这里的start()调用的就是本地方法
这就是本地方法
本地方法栈: 运行的时候也需要有内存空间去储存, 这些内存空间就是本地方法栈提供的
每一个线程都会分配一个栈空间,本地方法栈和程序计数器。如上图main线程:包含线程栈,本地方法栈,程序计数器。