JVM真香系列:轻松掌握JVM运行时数据区

回复“000”获取大量电子书

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

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

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

分为六块:

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

2. Java Virtual Machine Stacks Java虚拟机栈

3. Heap 堆

4. Method Area 方法区

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

6. Native Method Stacks 本地方法栈

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

Method Area

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

官网的介绍;

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

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

  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这些都是存放在方法区的。

The pc Register

也有的翻译为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.6) for 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切换回来时候,再从这里继续执行。

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的变化

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

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

Java Virtual Machine Stacks

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中会对应给这个线程创建一个栈。

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

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

多线程的话就是这样了

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

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}

调用过程如下图:

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

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

运行上面的代码:

如何设置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中,打开

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/

这些都是字节码指令。

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虚拟机栈的结构

最后大总结

Native Method Stacks

翻译过来就是本地方法栈,与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

关注公众号“Java后端技术全栈”

免费获取500G最新学习资料

(0)

相关推荐

  • JVM性能优化简介

    JVM性能优化简介

  • JVM入门看着一篇就够了

    JVM入门看着一篇就够了

  • 终于搞懂了Java 8 的内存结构,再也不纠结方法区和常量池了!!

    java8内存结构介绍 java8内存结构图 虚拟机内存与本地内存的区别 java运行时数据区域 直接内存 常见问题 java8内存结构介绍 java虚拟机在jdk8改变了许多,网络上各种解释都有,在 ...

  • JVM真香系列:轻松理解class文件到虚拟机(下)

    回复"000"获取大量电子书 类加载器 类加载器是很多人认为很硬的骨头.其实也没那么可怕,请听老田慢慢道来. 在装载(Load)阶段,通过类的全限定名获取其定义的二进制字节流,需要 ...

  • JVM真香系列:轻松理解class文件到虚拟机(上)

    回复"000"获取大量电子书 JVM初探 class文件到JVM中,就相当于我们吃饭,食物吃进了肚子里,不同的营养成分被身体不同的器官吸收. 查找class文件并导入到JVM中 ( ...

  • JVM真香系列:图解垃圾回收器

    回复"000"获取大量电子书 不知不觉,JVM系列已经到回收算法的实现了. 本文主要内容 先普及三个概念: 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态. 并 ...

  • JVM真香系列:如何判断对象是否可被回收?

    回复"000"获取大量电子书 在JVM中程序寄存器.Java虚拟机栈.本地方法栈,这三个区是随着线程的创建而创建,随着线程结束而销毁. 其实就是这三个的生命周期和线程的生命周期一样 ...

  • JVM真香系列:堆内存详解

    回复"000"获取大量电子书 前面的文章中已经有所提到过堆,只是大致介绍了一下.本文就来详细聊聊JVM中的堆. 在 JVM中,堆被划分成两个不同的区域:新生代 ( Young ). ...

  • JVM真香系列:方法区、堆、栈之间到底有什么关系

    回复"000"获取大量电子书 栈指向堆 如果在栈帧中有一个变量,类型为引用类型,比如: package com.tian.my_code.test; public class Jv ...

  • JVM真香系列:.java文件到.class文件

    回复"000"获取大量电子书 认识JVM 什么是JVM JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机.它能识别 .class后缀的 ...

  • 【jvm】运行时数据区笔记

    运行时数据区包含5个部分: 程序计数器:可以理解为存放当前线程执行的字节码的行号. 虚拟机栈:在每个方法被调用时,都会在虚拟机栈里存放一个栈帧,里边存放了局部变量表.操作.方法出口等内容. - 本地方 ...

  • Java 虚拟机运行时数据区详解

    本文摘自深入理解 Java 虚拟机第三版 概述 Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟 ...