JVM--解析Java内存区域及数据的内存分配与线程安全之间的一些联系 JVM--解析Java内存区域及数据的内存分配与线程安全之间的一些联系

JVM--解析Java内存区域及数据的内存分配与线程安全之间的一些联系

转载:https://blog.csdn.net/championhengyi/article/details/76857401

最近一直在看《Java多线程编程核心技术》的第二章,主要讲的是线程共享变量与线程私有变量以及如何写出线程安全的代码。看这部分一开始没太注意,只是记住了一条规则,“类中的成员变量,也叫实例变量,也叫全局变量,它是非线程安全,是所有线程共享的变量,定义在方法中的私有变量是线程安全的,是每个线程私有的”。很好理解不是吗,然后一帆风顺的看到了关于volatile这部分的知识,看过之后我陷入了凌乱。。。关于这部分我之后进行总结,而现在我觉得你如果真的想写出线程安全的代码,那么Java的内存分配以及布局就是我们需要掌握的基础。为此,我粗略的看了一下《深入理解Java虚拟机》这本书的第二章,并且查阅了一些资料,现在汇总整理如下。

注:学习这部分内容之前如果你对进程的内存映像或数据在内存中的分配有大概的了解,建议你先忘记它们,因为这是讲Java虚拟机运行时的数据区,和之前的知识并不相同,所以学习的时候不要拿自己以前所了解的知识进行比较与衡量。


Java虚拟机运行时的数据区

先来看一张图片: 
Java虚拟机运行时的数据区

在这里我们只需要关注线程共享区中的堆,以及线程独占区中的虚拟机栈,就是我们平时说的栈,只是这种说法在JVM中并不严谨,正确的应该说是虚拟机栈中局部变量表部分。(被static关键字声明的东西就是存储在方法区中)

其次,我在查阅资料的时候,看到网上很多资料都说栈中的数据共享而堆中的数据不共享,起初我还以为是博主写错了,了解之后发现我们考虑问题的立场不同,我们今天要讨论的是堆或栈内的数据对多线程是否共享,而他们所说的栈内数据共享则是在创建新变量时为节省内存空间而采取的一种措施,被称为Slot复用,两个完全是不同的东西。具体的区别在后面给大家说明。


虚拟机栈与堆

看了上面的图片,对JVM的数据区也有了个大概的认识,我们来详细说一下虚拟机栈和堆中到底都存储的是哪些数据。

下面这段话摘自网上他人博客:

基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。

最后需要补充的是所有对象的引用也都存在于栈中,而实际的对象本身是存储在堆中的,我们这时候倒可以将引用理解为一个指针,它指向了我们在堆中创建的对象。

再来说明前面说的栈内数据共享是什么东西,还是摘自他人博客:

另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义int a = 3; int b = 3; 编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。 特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。

至于堆中存储的数据,只要记住一句话,所有new出来的变量都存储在堆中。但是在这里我们还要考虑String类型的特殊性和自动拆箱与装箱的相关概念,好吧,概念实在太多了。我们之后再说这些问题。我们先来看两个问题:(下面讲述的问题只针对同一个对象,在多个不同的对象中,堆中的数据也是不共享的)

1.基本数据类型的成员变量放在jvm的哪块内存区域里?看了上面的概念之后,我们知道基本数据类型应该是存放在虚拟机栈中的。如果你也认为是虚拟机栈中,那么根据上面的图片,它应该属于线程独占区啊,怎么会属于共享变量呢?

2.与上面的问题对应,方法中新建的非基本数据类型放在jvm的哪块内存区域里?如果我们在方法中new了一个对象,按道理来说,new 出来的对象都是存放在堆上的,而根据上图我们又发现… …Java堆是属于线程共享区的。这是怎么一回事呢?

首先回答第一个问题:

class {
    private int i;
}

基本数据类型放在栈中,这一概念的确没有错,但是这个说法又不是很准确,如上面的代码,基本数据类型的全局变量i,它是存放在java堆中。因为它不是静态的变量,不会独立于类的实例而存在,而该类实例化之后,放在堆中,当然也包含了它的属性i。因此成员变量就算是基本数据类型也共享。

是不是觉得有点绕?然而事实的确是这样。

再来看第二个问题,对象的确是存放在堆中,但我们也说了线程不安全只针对单例模式的成员变量,而此时如果对象被定义在了方法之中,那么当每个线程调用一次方法都会新创建一个对象,这些对象都属于每个线程所私有,所以虽然对象本身存在于堆中,但也并不共享。至于基本类型由于每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。

最后结合Java虚拟机运行时的数据区总结一下,就是对于同一对象(单例模式),成员变量共享,局部变量不共享。


运行时常量池

运行时常量池存在于方法区中,常量池里面主要存储字符串常量和基本类型常量(public static final)。

对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。

如以下代码:

String s1 = "china"; 
String s2 = "china";
String s3 = "china"; 
String ss1 = new String("china"); 
String ss2 = new String("china"); 
String ss3 = new String("china");   
  • 一下,对于通过 new 产生一个字符串(假设为 ”china” )时,会先去常量池中查找是否已经有了 ”china” 对象,如果没有则在常量池中创建一个此字符串对象,然后堆中再创建一个常量池中此 ”china” 对象的拷贝对象。

也就是有道面试题: String s = new String(“xyz”); 产生几个对象?

一个或两个。如果常量池中原来没有 ”xyz”, 就是两个。如果原来的常量池中存在“xyz”时,就是一个。


对于基础类型的变量和常量:变量存储在栈中,常量存储在常量池中。

如以下代码:

int i1 = 9; 
int i2 = 9; 
int i3 = 9;  
public static final int INT1 = 9; 
public static final int INT2 = 9; 
public static final int INT3 = 9;


自动拆箱与自动装箱

public class JVM {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;

        Long g = 3L;

        out.println(c == d);
        out.println(e == f);
        out.println(c == (a+b));
        out.println(c.equals(a+b));
        out.println(g == (a+b));
        out.println(g.equals(a+b));
    }
}

来看一下程序的运行结果是否符合你的预期:

true
false
true
true
true
false

首先我们先来了解一下包装类数据:

包装类数据,如Integer, String, Double,Long等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中。

然后我们在对自动装箱与拆箱做个了结:

1.包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,equals()方法不处理数据转型的关系。

2.在自动装箱时,把int变成Integer的时候,是有规则的,当你的int的值在-128-IntegerCache.high(127) 时,返回的不是一个新new出来的Integer对象,而是一个已经缓存在堆中的Integer对象,(我们可以这样理解,系统已经把-128到127之 间的Integer缓存到一个Integer数组中去了,如果你要把一个int变成一个Integer对象,首先去缓存中找,找到的话直接返回引用给你就 行了,不必再新new一个),如果不在-128-IntegerCache.high(127) 时会返回一个新new出来的Integer对象。

对于第二点,不仅Integer有这种特性,其它包装类数据也具有,我们可以看一下Long的源码:

private static class LongCache {
    private LongCache(){}

    static final Long cache[] = new Long[-(-128) + 127 + 1];

    static {
        for(int i = 0; i < cache.length; i++)
        cache[i] = new Long(i - 128);
    }
}

在了解了上面的概念之后,我相信你已经基本能够正确解释代码运行的结果了。

最后我来解释一下最后两个的运行结果为什么是true与false,先来看一下后两句话在进行编译之后在.class文件中的样子吧:

System.out.println(g.longValue() == (long)(a.intValue() + b.intValue()));
System.out.println(g.equals(Integer.valueOf(a.intValue() + b.intValue())));

我们可以看到编译器对out.println(g == (a+b));进行编译的时候,进行了拆箱与向上转型的操作,所以此时比较的仅仅是两个变量的字面值,与基本数据类型的比较是一样的,所以是true,而最后仍然比较的是对象中的数据并且对a没有进行向上转型,Long中存在的数据肯定就和Integer中存在的数据不等了,所以为false。

再说一点,我们能将字面值直接赋给Integer类是因为Java语法糖的存在,实际上Integer a = 1在经过编译之后是这样的:Integer a = new Integer(1),语法糖帮助我们简化了语法。


猜你喜欢

转载自blog.csdn.net/xiao__jia__jia/article/details/81054161
今日推荐