【Java】多线程内存模型

事先说明

本文主要参考 《深入理解Java虚拟机 第二版》和
Jakob Jenkov
所写的博文,用Java虚拟机中所提到的概念诠释博文中的多线程内存模型。如有不妥之处,还希望各位老哥不惜指正。

概念讲解

简单来说,Java虚拟机将内存划分为两大类,一类是每个线程私有的内存区:JVM 栈(JVM Stack)、本地方法栈(Native method Stack)和程序计算器,第二类是所有线程能够共用的:堆(Heap)和方法区(method area)。

在多线程变量内存模型中,所要关注的概念就是JVM栈(JVM Stack)和 堆(Heap)。
JVM栈是一个较大的范围,它由一个个的栈帧所组成,每一个对一个类的方法调用都将产生一个栈帧,将栈帧细分还能得出局部变量比表、动态链接和操作栈等,但对于这一次的主题而言,到局部变量表就足够了。

实际操作

我们可以用两行简单的代码来解释JVM栈和堆具体起到什么作用。

 int i = 1;

我们都知道 int 代表了一个基础类型,他并不是对象,我们在执行这一行代码的时候,会首先将其编译为字节码,而这个字节码的行数就其实并不是一行了。通过javap这个命令可以将一个编译好的.class文件打印成字节码。
用中文解释的话能分为两个步骤,1)将1这个数组压入操作栈 2)在局部变量表中创建出 i 这个变量,并把操作栈中的1弹出,赋值给i。

TestClass testClass = new TestClass();

TestClass是一个我新建的Java 类,用来区分一个非基础类型的对象是怎么在内存中创建出来的。同样我们也可以通过javap的命令来将这个编译好的.class文件打印成字节码,同样这一份的字节码也不会是一行。这次的过程会首先将new TestClass()这部分代码在堆(Heap)申请好对应的内存,并调用它这个类的初始化函数。接着,再将这一个引用(reference)赋值给TestClass testClass这一个局部变量。

从上面的例子之中我们可以看出,基础类型的 1 或者 true 或者 ‘c’ 而言,他的内存并没有储存在堆(Heap)上,而是由一个线程私有的栈(Stack)来管辖;相对而言,一个我们自定义的类就全部保存在Java 的堆(Heap)上,通过引用(reference)的方式赋值给了我们定义的局部变量上。有意思的是基础类型和引用都会保存在JVM栈(Stack)上的局部变量表中。大家可以想一想,如果我们上面提到的TestClass中顶一个int i = 1的成员变量这个数据在内存中又是如何保存的,答案会在下一节中分享。

复杂一些

上面我们所提到的仅仅是一个Main函数中的一行代码,当事情的边界逐渐拓展到多线程上,内存模型也变得复杂了起来。
来看看并不是一行的代码,

public class MyRunnable implements Runnable() {

    public void run() {
        methodOne();
    }

    public void methodOne() {
        int localVariable1 = 45;

        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;

        //... do more with local variables.

        methodTwo();
    }

    public void methodTwo() {
        Integer localVariable1 = new Integer(99);

        //... do more with local variable.
    }
}
public class MySharedObject {

    //static variable pointing to instance of MySharedObject

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    //member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member2 = 67890;
}

将上面的MyRunnable类加载到两个Thread中,一块启动,这两个线程各自的JVM栈(Stack)和他们共用的堆(Heap)中的内存使用情况就如下图所示。

在这里插入图片描述
看左边的Thread1的示意图,可以看出Thread线程启动时会调用run()方法,所以run()方法会新生成一个栈帧。methodOne()和methodTwo()方法同理会各自产生一个栈帧。在methodOne()栈帧中,会在局部变量表中生成一个int 45的数据;接着会将一个已经在堆(Heap)中的静态实例的引用赋值给localVariable2。method2的栈帧会在自己的方法中因为Integer,这个int类型的包装类的原因,会在堆(Heap)中创建一个值为99的Integer对象,然后在局部变量表中生成一个这个对象的引用。

再看右边的Thread2的示意图,因为局部变量的原因,它methodOne()中生成的localVariable1的int 没有和Thread1的JVM 栈(Stack)产生交集,methodTwo()中的包装类也没有和堆(Heap)中已经有的数据发生交集,而是重新生成了一个。唯一发生交际的地方就是localVariable2这个静态变量,Thread1的localVariable2和Thread2中的localVariable2各自有一个它的引用。

最后,我们回到上一节提到问题,如果在一个类中的成员变量已经被初始化好起始值之后,他的值并不是保存在调用它构建函数的线程独占的JVM栈中,而是被保存在所有线程公用的堆中。

值得争议的点

关于类中成员变量的内存保存位置,Jakob Jenkov 作者给出的实例较为粗犷,并没有把堆这个概念更加细分为堆(Heap)和方法区(method area),我在国内搜索相关文档的时候发现有人提出了一个有意思的问题,如果这个成员变量是对象的话,应该被保存在方法区之中。但我在搜索的时候,并没有找到相关文档有探讨这件事,所以需要记为一个存疑点,希望之后能够查漏补缺。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值