写点什么

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

用户头像
田维常
关注
发布于: 2020 年 11 月 10 日

关注“Java 后端技术全栈”


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


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


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



分为六块:


  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/javas...



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


  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; 910}
复制代码


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-up6在虚拟机启动时创建
复制代码


在前面类加载阶段我们已经聊过了,在 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    }1011    private void login(String passWd, String userName) {12        getUserName(passWd, userName);13    }1415    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 { 910    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/javas...



这些都是字节码指令。


LocalVariableTable


本地变量表


1        Start  Length  Slot  Name   Signature2            0      14     0  this   Lcom/tian/demo/test/JavaStackDemo;3            0      14     1   age   I4            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



发布于: 2020 年 11 月 10 日阅读数: 43
用户头像

田维常

关注

关注公众号:Java后端技术全栈,领500G资料 2020.10.24 加入

关注公众号:Java后端技术全栈,领500G资料

评论

发布
暂无评论
JVM真香系列:轻松掌握JVM运行时数据区