JVM系列之内存结构运行时数据区

布朗大学(美国)校训:“我们信赖上帝。”


转眼已进入深秋,在书房里听听歌写写文章,蛮舒服惬意的事情!

继续聊jvm系列,上两节聊了class文件结构和类加载机制,接下来聊聊面试的高频点,jvm的内存结构模型,及实战中对运行时数据区的理解,闲话不多说,先来一张图吧,请阅:
内存结构规范.png
这张图基本把前面讲的也一起串起来了,先是静态编译把源码编译成class字节码文件,再通过加载器加载进内存,接下来就进入了运行时数据区域了,这块即是jvm内存结构规范,把此区域划分为堆/方法区/栈区(本地方法栈/虚拟机栈/程序计数器),其中栈区为线程私有,堆和方法区为线程共享。接下来就重点讲解下这几个区域:

一 Java堆(Heap)

这个大家估计耳熟能详,随口便说是放置new对象的地方,没错,主要是存储对象实例,几乎所有的对象实例都在这个区域分配内存。看到这里估计有朋友问了,那是不是有的对象实例不是在堆上分配的,jvm还真提供了,是否听说过栈上分配和TLAB,栈上分配是为了减少gc的压力,因为随栈弹出而释放并不会出发gc,TLAB则是优化对象实例在堆中并发分配的效率,具体详细可自行百度了解,这里放一张对象分配图,摘自实战Java虚拟机一书中:
对象分配.png

堆是jvm管理内存最大的一块,是线程共享的一块区域,在jvm启动时创建;可能有些同学经常遇到OOM(OutOfMemoryError)异常,这里考大家一题,如何模拟OOM?
很简单,首先设置下jvm参数(-Xms 1m -Xmx 2m),然后在main函数初始化一个大的数组即可浮现。

二 方法区(Method Area)

与堆一样,也是被线程共享的区域,同样会出现OOM(OutOfMemoryError)异常,此区域主要存储如下信息:
1/已被虚拟机加载的类信息,即元数据;
2/静态变量和常量,这里维护一个运行时常量池;
3/JIT(即时编译)后的代码,这个上节有提到过;
这里,分享一个常考的面试题:

/**
     * @author 阿伦故事
     * @Description:
     *  小试牛刀
     * */
    public static void main(String[] args) {
        String s = "1"+"2"+"3";// 1 这里创建了几个对象?
         
        String s1 = "allen story";// 2 
        String s2 = new String("allen story");// 3 这里创建了几个对象?
    }
    /**
     * 分析如下:
     * 1处只创建了一个对象,即“123”在运行时常量池,栈中引用s直接指向常量池
     * 3处也只创建一个对象,即在堆中new了一个对象实例,“allen story”在2处已
     * 经在运行时创建,堆中对象直接存储该指向即可。
     * */

三 程序计数器(Program Counter Register)

了解jvm内存结构规范的朋友们,肯定脱口而出,pc用于记录所属线程所执行的字节码的执行行号,没错,有多少朋友理解这个意思?如果了解寄存器的话,那就容易很多,cpu执行的指令就是从指令寄存器取的,而pc就是用于存储下一条指令的偏移地址。当然,pc是线程私有的,存储的自然所属线程的指令地址。
唯一一个在JVM规范中没有规定任何OutOfMemoryError的区域。
如何理解?
很简单,pc只存储下一条指令的地址,并不会随着指令量级的增长而增长。

四 栈(Stack)

栈分为本地方法栈和虚拟机栈,顾名思义,一个是用于native方法的,栈的特性是先进后出,这也是递归调用的逻辑。栈是线程私有的,每启动一个新线程时,jvm都会为它分配一个Java栈,以栈帧为单位保存线程的运行状态。
虚拟机只会对Java栈执行两种操作:以栈帧为单位的入栈或者出栈。
这里也容易出现一个异常,StackOverflowError,如何模拟?
理解了就很容易,超过了栈深度不就异常了嘛?
只需要写个递归调用,无返回不退出,一直调用无退出条件,那不就一直在入栈而不弹栈,那不栈溢出了嘛?另可通过测试得知jvm为每个线程分配的内存默认是1M,具体可测试下,即jvm参数(-Xss 1m)。
栈在jvm内存结构中非常重要,我们有必要详细了解它的组成部分,由基本类型变量区、执行环境上下文、操作指令区(存放操作指令)组成,只是保存基本类型数据和自定义对象的引用,即堆中实例对象的地址或偏移地址。

五 小结

介绍了jvm内存结构划分,稍稍总结下重点区域,堆/方法区/栈之间的关系(如图),以一个例子串下类的加载/分配与执行。


内存结构关系.png

Sample:

/**
     * @author 阿伦故事
     * @Description:
     *  jvm执行流程
     * */
    public static void main(String[] args) {
        Person person = new Person();
         person.sayHello();
    }
    /**
     * 流程如下:
     * 上述很简单,就是main主线程创建一个person对象实例,调用其sayHello方法
     * 1/先去方法区寻找Person类的元数据信息;
     * 2/如果找不到,Classloader加载Person类信息存储内存方法区;
     * 3/在堆中创建Person对象,并持有方法区中Person的元信息的引用;
     * 4/把引用变量person添加到栈中,指向堆中的内存对象Person实例;
     * 5/执行person.sayHello()时,JVM根据person定位到堆空间的Person实例;
     * 6/根据Person实例持有的方法区引用,获取Person元信息与sayHello方法执行字节码。
     * */
执行流程.png

特此声明:
分享文章有完整的知识架构图,将从以下几个方面系统展开:
1 基础(Linux/Spring boot/并发)
2 性能调优(jvm/tomcat/mysql)
3 高并发分布式
4 微服务体系
如果您觉得文章不错,请关注阿伦故事,您的支持是我坚持的莫大动力,在此受小弟一拜!

你可能感兴趣的:(JVM系列之内存结构运行时数据区)