JVM内存模型

jvm模型图

在这里插入图片描述

常量池:

class文件中的常量池包含两部分:字面值和符号引用。

  • 字面值:理解为java中定义的字符串常量、final常量等;

    String常量表并不是一成不变的,程序运行时可以动态添加字符串常量,使用String的intern()可以动态的添加String常量。但

    jvm 确保两个在值上完全相等的字符串字面值(即其中包含的字符序列是相同的,使用equals()来判断)指向同一个 String 实例。

  • 符号引用:指的是一些字符串,这些字符串表示当前类引用的外部类、方法、变量等的引用地址的抽象表示形式,在类被jvm装载并第一次使用这些符号引用时,这些符号引用将会解析为直接引用。符号常量包含:

    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类(Double、Float)则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。

Integer itg1 = new Integer(1);	 ------->	Integer integer = new Integer(1);
Integer itg2 = new Integer(1);	 ------->	Integer integer1 = new Integer(1);
Integer i1 = 1;					------->	Integer integer2 = Integer.valueOf(1);
Integer i2 = 1;					------->	Integer integer3 = Integer.valueOf(1);

Double d1 = 1.0;				------->	Double double1 = Double.valueOf(1.0D);
Double d2 = 1.0;				------->	Double double2 = Double.valueOf(1.0D);

Integer类中的valueOf源码:
	public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
Double类中的valueOf源码:
	public static Double valueOf(double d) {
    	return new Double(d);
	}

String也实现了常量池技术

String类的构造函数源码
	public String(String original) {
		this.value = original.value;
		this.hash = original.hash;
	}
使用	String newString = new String("Hello");时,本身就包含了装箱操作,这里注意接收的参数类型也是String对象类型

jmm模型图

在这里插入图片描述

Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存(working memory),线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

基于此种内存模型,便产生了多线程编程中的数据“脏读”等问题。

举个简单的例子:在java中,执行下面这个语句:

i = 10++;

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

比如同时有2个线程执行这段代码,假如初始时i的值为10,那么我们希望两个线程执行完之后i的值变为12。但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的工作内存当中,然后线程1进行加1操作,然后把i的最新值11写入到内存。此时线程2的工作内存当中i的值还是10,进行加1操作之后,i的值为11,然后线程2把i的值写入内存。

最终结果i的值是11,而不是12。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

那么如何确保共享变量在多线程访问时能够正确输出结果呢?

在解决这个问题之前,我们要先了解并发编程的三大概念:原子性,有序性,可见性

原子性
volatile无法保证操作的原子性
volatile的使用场景
单例模式中的double check
class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}
为什么要使用volatile 修饰instance?
主要在于instance = new Singleton()这句,这并非是一个原子操作	
拓展
对象的创建

Java是一门面向对象的编程语言,Java 程序运行过程中无时无刻都有对象被创建出来,在语言层面上,创建对象(例如克隆,反序列化)通常仅仅是一个new关键字而已,例如下面的语句。

Object obj = new Object();

其实在虚拟机中,当遇到上述语句时,其执行过程大致要经历下面几个阶段。

  • 类加载检查

    虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。

  • 为新生对象分配内存

    在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

  • 初始化内存空间

    内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  • 对对象进行必要的设置

    接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

    http://blog.csdn.net/ylyg050518/article/details/52319003#comments

可见性

有序性
  • 指令重排序:

    指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

Java内存模型具备一些先天的“有序性”(happens-before 原则)

​ 即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作

  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作

  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

    happen-before俗解

对象的访问定位

建立对象是为了使用对象。我们已经知道,对象的引用保存在Java 虚拟机栈中,而具体的对象实在堆中的。
由于reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄池和直接使用指针。

  • 使用句柄池访问的方式
    在这里插入图片描述

  • 直接使用指针
    在这里插入图片描述
    同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄池和直接使用指针。

    目前Java默认使用的HotSpot虚拟机采用的便是第二种方式进行对象访问的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值