JVM基础之内存模型

3,381 阅读5分钟

简介

本篇内容

  • 进程内存与java运行时数据区的关系
  • java内存模型(运行时数据区)
  • 模拟程序执行
  • 模拟对象创建过程
  • jvm堆

进程的内存

进程

我们先来简单的说一下进程。

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位

我们电脑上安装的,qq,微信,360等软件,运行起来后,都是以一个个的进程运行在操作系统上,操作系统会把计算机的内存分配给每个进程。

image.png

进程的内存

我们把放大镜放到进程中的内存上,其实每个进程内部又把内存分了几块区域,大致分为,代码段,栈段,堆段

image.png

java进程

我们的java程序,也是一个进程。这个进程会加载,解析class文件,并在内存中存储、处理。存储的位置就在进程的堆中

image.png

本篇文章的重点就是介绍运行时数据区的样子

运行时数据区

image.png

运行时数据区分为,方法区,虚拟机栈。本地方法栈。堆。程序计数器。下面依次介绍。

方法区

作用范围: 方法区是所有线程共享的

作用: 方法区用来存储被加载过的类信息,常量,变量等数据

永久代与元空间

方法区是java虚拟机规范给出的“规范(理解为程序中的接口)”,永久代与元空间是不同时期对方法区的实现(1.8后是元空间,之前是永久代)。

还记得我们在基础一中讲的klass模型嘛(klass是java类在c++中的映射)。c++中klass对象其实就是存储在方法区(元空间).

image.png

虚拟机栈

虚拟栈是每个线程私有的区域

image.png

看这张图想一个问题:虚拟栈的个数有几个?答案显而易见,有几个线程就有多少个。

结构

再看一眼虚拟机栈里面的样子

非常简单,其实就是栈。栈就一定有栈帧。

image.png

那这里的栈帧的是什么呢?

每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧

例如:

public class Test {
    public static void main(String[] args) {
        add();
    }
    private static void add(){
        
    }
}

对应的虚拟栈就是这样。

image.png

我们来分析下这个图。

  1. 程序运行后,main方法先执行,在虚拟机栈里创建main方法的栈帧并压栈。main方法中调用add方法后,为add创建栈帧并压栈。所以说main在下add在上。
  2. 有多少栈帧?经我们第一点的分析后,很容易得出结论:调用了多少方法就有多少栈帧(注意不是有多少方法就有多少个,因为同一个方法可能被调用多次)

栈帧结构

栈帧包括:局部变量表,操作数栈,动态链接,返回地址等信息。

image.png

操作数栈与局部变量表

大家来看这个例子就明白了(对字节码不熟悉的请看上篇文章)

public static void main(String[] args) {
    int num = 1;
    int num2 = 2;
    int res = num + num2;
}

操作码

image.png

我们模拟一下这段程序的执行过程

  1. 此时局部变量表的内容

image.png

重点记住这几个变量的顺序。

  1. 常量 1 压入操作数栈

image.png

  1. 栈顶元素弹出赋值给局部变量表的第二个变量

image.png

  1. 常量1 压入操作数栈

image.png

  1. 栈顶元素弹出赋值给局部变量表第三个变量

image.png

6.局部变量表第一个int类型的元素压栈

image.png

  1. 局部变量表第二个int类型元素压栈 image.png

  2. 栈顶两个变量相加,结果放入栈顶

image.png

  1. 栈顶元素弹出并赋值给局部变量表的第四个变量

image.png

看完这个例子,相信大家对操作数栈跟局部变量表有了一定的认识,我们接下来再看下,new 一个对象时,都做了什么事情。

new 对象的过程

public static void main(String[] args) {
   Test test = new Test();
}

操作码

image.png image.png

我们逐一解释下

  1. new #2 <com/haozi/Test>

image.png

这个操作符干两件事

第一件:在堆上申请一块空间

第二件:操作数栈,压入这块空间的地址

注意: 此时只在堆上申请了这个对象的空间,并没有执行该对象的构造函数也就是未初始化(对类加载过程不了解的请看,基础三)

  1. dup

复制栈顶元素,并压入栈顶

image.png

为什么要复制一份呢?

因为要弹出一个元素给this指针赋值,如果不复制一份this指针赋值后,栈里就没有地址了。

  1. invokespecial

执行构造方法

  1. astore_1

赋值给局部变量表第二个元素

DCL与volatile

为什么懒汉模式下单例对象要加volatile? (不是本篇文章的重点)

我们的cpu执行时为提高效率会乱序执行,上一个例子的第三步与第四步有可能颠倒,那么理论上就有可能拿到还未初始化的对象。

默认大小

最小是物理内存的 1/64 最大是物理内存的 1/4

堆中划分

image.png

堆分为老年代,新生代(比例为2:1),新声代分为 eden区,from区,to区(比例为8:1:1)

为什么老年代的空间大?

概括:老年代要存储的对象多.

详细说:(不懂没关系,垃圾回收还会聊)

老年代要负责存储以下情况的对象

  1. 两年代要存储gc大于15的对象

from区to区来回倒腾15次对象还没被回收旧进入老年代)

  1. 为新生代进行空间担保 新生代发生gc,且放不下时,这个对象直接进入老年代

例:对象占20兆,ygc后,没被回收,from区只有5兆,此时直接进老年代

  1. 大对象,对象超过eden区的1/2直接进老年代