前面我们讲了从java源文件到class文件,在从class文件到JVM。那么今天继续聊JVM是如何布局的。

JVM运行时数据区有几个?看看官网是就知道了

https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

91129fbe6ec4e23c30cf1cb70eb86f6b.png

分为六块:

1. The pc Register 程序计数器/寄存器

2. Java Virtual Machine Stacks Java虚拟机栈

3. Heap 堆

4. Method Area 方法区

5. Run-Time Constant Pool  运行时常量池

6. Native Method Stacks 本地方法栈

为了更好的理解,下面画了一张图作为辅助:

981ea63a5311ac433491219a2f8c5a16.png



Method Area

方法区是用于存储类结构信息的地方,线程共享,包括常量池、静态变量、构造函数等类型信息,类型信息是由类加载器在类加载时从类.class文件中提取出来的。

官网的介绍;

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1

e94d475f862831e964e53337cb8551f2.png

从上面的介绍中,我们大致可以得出以下结论:

  1. 方法区是各个线程共享的内存区域在虚拟机启动时创建,生命周期和JVM生命周期一样。

  2. 用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据。

  3. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。

  4. 当方法区无法满足内存分配需求时,将抛出OOM=OutOfMemoryError异常。

用一段代码来加深印象:

 1/**
2 * @author 老田
3 * @version 1.0
4 * @date 2020/11/5 12:55
5 */

6public class User {
7    private static String a = "";
8    private static final int b = 10;
9
10}

User.class类信息,以及静态变量a,常量b这些都是存放在方法区的。

5386d4250bc24e56d5e3c99931e6aa13.png



The pc Register

37581b2cfdb7c6ba6d19d125ac962272.png


39239aae9e0433b919a2e2539f7e59d1.png

也有的翻译为pc寄存器。下面是官网对寄存器的解释,做了一个简要的翻译。

 1The Java Virtual Machine can support many threads of execution at once (JLS §17). 
2Java虚拟机支持多线程并发
3Each Java Virtual Machine thread has its own pc (program counter) register
4每个Java虚拟机线程都拥有一个寄存器
5At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method (§2.6for that thread. 
6在任何时候,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法
7If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. 
8如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
9If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. 
10如果正在执行的是Native方法,则这个计数器为空。
11The Java Virtual Machine'
s pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.
12Java虚拟机的pc寄存器足够宽,可以容纳特定平台上的返回地址或本机指针。

实际上,程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。

我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。

假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?

这就是需要在线程中维护一个变量,记录线程执行到的位置,记录本次已经执行到哪一行代码了,当CPU切换回来时候,再从这里继续执行。

04a14fdd2a50d2e9dc2368a20b97c22c.png



heap

堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。Java对象实例以及数组都在堆上分配。官网介绍:

1The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. 
2线程共享
3The heap is the run-time data area from which memory for all class instances and arrays is allocated.
4所有的Java对象实例以及数组都在堆上分配。
5The heap is created on virtual machine start-up
6在虚拟机启动时创建

在前面类加载阶段我们已经聊过了,在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

堆在JDK1.7和JDK1.8的变化

e4df2359c5d764e67e35773d9db000a4.png

大家都知道,JVM 在运行时,会从操作系统申请大块的堆内内存,进行数据的存储。但是,堆外内存也就是申请后操作系统剩余的内存,也会有部分受到 JVM 的控制。比较典型的就是一些 native 关键词修饰的方法,以及对内存的申请和处理。

bef4400cbc05de35bf684369d9db10ed.gif

因为堆想讲完整,篇幅量会很大,这里大家知道有这么个东西,他是干嘛的就行了,后面会有专门讲解,敬请期待!!

Java Virtual Machine Stacks

5386d4250bc24e56d5e3c99931e6aa13.png

Java虚拟机栈,是线程私有

每一个线程拥有一个虚拟机栈,每一个栈包含n个栈帧,每个栈帧对应一次一个放调用,

每个栈帧里包含:局部变量表、操作数栈、动态链接、方法出口。

官网介绍

1Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread.
2一个线程的创建也就同事创建一个java虚拟机栈
3A Java Virtual Machine stack stores frames (§2.6). 
4Java虚拟机堆栈存储帧
5The memory for a Java Virtual Machine stack does not need to be contiguous.
6Java虚拟机堆栈的内存不需要是连续的。

看一段代码

 1public class JavaStackDemo {
2
3    private void checkParam(String passWd, String userName) {
4        // TODO: 2020/11/6 用户名和密码校验 
5    }
6
7    private void getUserName(String passWd, String userName) {
8        checkParam(passWd, userName);
9    }
10
11    private void login(String passWd, String userName) {
12        getUserName(passWd, userName);
13    }
14
15    public static void main(String[] args) {
16        //这里是演示代码,希望大家能结合自己平时写的代码理解,那样会更爽
17        //你就不再死记硬背了
18        JavaStackDemo javaStackDemo = new JavaStackDemo();
19        javaStackDemo.login("老田""111111");
20    }
21}

启动main方法就是启动了一个线程,JVM中会对应给这个线程创建一个栈。

0957023ed4806d85b088d16b81cfff17.png

从这个调用过程很容易发现是个先进后出的结构,刚好栈的结构就是这样的。java虚拟机栈就是这么设计的

a4de727229c9546d982353ef39929ce1.png

每个栈帧表示一个方法的调用。

多线程的话就是这样了

8b6927449ffc4a8380578e1de4812123.png

从上面这个图大家会不会觉得这个栈有问题?其实也是有问题的,比如说看下面这段代码

 1/**
2 * TODO
3 *
4 * @author 田维常
5 * @version 1.0
6 * @date 2020/11/6 9:05
7 */

8public class JavaStackDemo {
9
10    public static void main(String[] args) {
11        JavaStackDemo javaStackDemo = new JavaStackDemo();
12        javaStackDemo.test();
13    }
14    //循环调用test方法
15    private void test(){
16        test();
17    }
18}

调用过程如下图:

b1284ba8744ac61398585f7c0f15179e.png

是不是觉得很无语,调用方法就往栈里加入一个栈帧,这么下去,这个栈得需要多深才能放下,死循环和无限递归呢,岂不是栈里需要无限深度吗?

Java虚拟机栈大小(深度)肯定是有限的,所以就会导致一个大家都听说过的栈溢出

运行上面的代码:

1456a4041efd6b71ecec02ae5e84107e.png

如何设置Java虚拟机栈的大小呢?

我们可以使用虚拟机参数-Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度;

-Xss size

设置线程堆栈大小(以字节为单位)。附加字母kK表示KB,mM表示MB,和gG表示GB。默认值取决于平台:

  • Linux / x64(64位):1024 KB

  • macOS(64位):1024 KB

  • Oracle Solaris / x64(64位):1024 KB

  • Windows:默认值取决于虚拟内存

下面的示例以不同的单位将线程堆栈大小设置为1024 KB:

1-Xss1m (1mb)
2-Xss1024k  (1024kb)
3-Xss1048576

回到上面关于栈中栈帧的话题。

什么是栈帧?

上面提到过,调用方法就生成一个栈帧,然后入栈。

看一段代码

 1public class JavaStackDemo {
2
3    public static void main(String[] args) {
4        JavaStackDemo javaStackDemo = new JavaStackDemo();
5        javaStackDemo.getUserType(21);
6    }
7
8    public String getUserType(int age) {
9        int temp = 18;
10        if (age < temp) {
11            return "未成年人";
12        }
13        //动态链接
14        //userService.xx();
15        return "成年人";
16    } 
17}

既然是和方法有关,那么就可以联想到方法里都有些什么

官网介绍

1Each frame has its own array of local variables , its own operand stack (§2.6.2), and a reference to the run-time constant pool  of the class of the current method.

每个栈帧拥有自己的本地变量。比如上面代码里的

1 int age、int temp

这些都是本地变量。

每个栈帧都有自己的操作数栈

通过javac编译好JavaStackDemo,然后使用

1javap -v JavaStackDemo.class >log.txt

将字节码导入到log.txt中,打开

46d31022da7b641100c35638e964ec1b.png

getUserType方法里面的字节码做一个解释。有时候本地变量通过javap看不到,可以再javac的时候添加一个参数

javac -g:vars XXX.class这样就可以把本地变量表给输出来了。

1指令bipush 18  将18压入操作数栈
2istore_2    将栈顶int型数值存入第三个本地变量
3iload_1    将第二个int型本地变量推送至栈顶
4iload_2    将第三个int型本地变量推送至栈顶
5if_icmpge    比较栈顶两int型数值大小, 当结果大于等于0时跳转
6ldc    将int,float或String型常量值从常量池中推送至栈顶
7areturn    从当前方法返回对象引用

官网

https://docs.oracle.com/javase/specs/jvms/se8/html/

f7aeda0e7f1ea41eb6d3e294fa60dd4b.png

这些都是字节码指令。

LocalVariableTable

本地变量表

1        Start  Length  Slot  Name   Signature
2            0      14     0  this   Lcom/tian/demo/test/JavaStackDemo;
3            0      14     1   age   I
4            3      11     2  temp   I

自己this算一个本地变量,入参age算一个本地变量,方法中的临时变量temp也算一个本地变量。

方法出口

return。如果方法不需要返回void的时候,其实方法里是默认会为其加上一个return;

另外方法的返回分两种:

正常代码执行完毕然后return。

遇到异常结束

栈帧总结

方法出口:return或者程序异常

局部变量表:保存局部变量

操作数栈:保存每次赋值、运算等信息

动态链接:相对于C/C++的静态连接而言,静态连接是将所有类加载,不论是否使用到。而动态链接是要用到某各类的时候在加载到内存里。静态连接速度快,动态链接灵活性更高。

Java虚拟机栈总结

用图来总结一下Java虚拟机栈的结构

7b977d18e2018c70ed33afbf2703662f.png

最后大总结

5386d4250bc24e56d5e3c99931e6aa13.png



Native Method Stacks

c3d78dc0cdd7a23a6828dff84ba6f2c4.png

翻译过来就是本地方法栈,与Java虚拟机栈一样,但这里的栈是针对native修饰的方法的,比如System、Unsafe、Object类中的相关native方法。

 1public class Object {
2    //native修饰的方法
3    private static native void registerNatives();
4    public final native Class<?> getClass();
5    public native int hashCode();
6    protected native Object clone() throws CloneNotSupportedException;
7    public final native void notify();
8    //.......
9}    
10public final class System {
11    //native修饰的方法
12    private static native void registerNatives();
13    static {
14        registerNatives();
15    }
16    public static native long currentTimeMillis();
17    private static native void setIn0(InputStream in);
18    private static native void setOut0(PrintStream out);
19    private static native void setErr0(PrintStream err);
20    //.....
21}
22public final class Unsafe {
23    //native修饰的方法
24    private static native void registerNatives();
25    public native int getInt(Object var1, long var2);
26    public native void putInt(Object var1, long var2, int var4);
27    public native Object getObject(Object var1, long var2);
28    public native void putObject(Object var1, long var2, Object var4);
29    public native boolean getBoolean(Object var1, long var2);
30    //...
31}   

面试常问:JVM运行时区那些和线程有直接的关系和间接的关系,哪些区会发生OOM?

每个区域是否为线程共享,是否会发生OOM

336f5bb977d6d0e334266b1edbcf5cb9.png