您的位置:首页 > 编程语言 > Java开发

Java内存区域与内存模型

2013-05-10 00:29 148 查看

Part 1 Java内存区域

在Java内存分配中,java将内存分为:方法区虚拟机栈本地方法栈程序计数器。其中方法区和堆对于所有线程共享,而虚拟机栈和本地方法栈还有程序计数器对于线程隔离的。每个区域都有各自的创建和销毁时间。

程序计数器:

作用是当前线程所执行的字节吗的行号指示器。Java的多线程是通过线程轮流切换并分配处理器执行时间方式来实现的。因此,每个线程为了能在切换后能恢复到正确的位置,每个线程需要独立的程序计数器。

Java 虚拟机栈:

每个放在被执行的时候都会同时创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。虚拟内存栈就是我们经常讲的“栈”。其中局部变量表所需内存是在编译期完成分配。

本地方法栈:

与虚拟机栈类似,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用Native方法服务。

Java 堆:

被所有程序共享,并且在虚拟机启动时创建。此内存区域作用是存放对象实例。根据Java虚拟机规定,Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可。

方法区:

与堆相同,在各个线程间共享。作用是存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

运行时常量池:

是方法区的一部分。作用是存储编译期生成的各种字面量和符号引用。

Part 2 Java内存模型

本节向您介绍Java内存模型的概念,在C或C++中, 利用不同操作平台下的内存模型来编写并发程序;Java利用了自身虚拟机的优势, 使内存模型不束缚于具体的处理器架构,真正实现了跨平台。

什么是内存规范

在jsr-133中是这么定义的

A memory model describes, given a program and an execution trace of that program, whether

the execution trace is a legal execution of the program. For the Java programming language, the

memory model works by examining each read in an execution trace and checking that the write

observed by that read is valid according to certain rules.


也就是说一个内存模型描述了一个给定的程序和和它的执行路径是否一个合法的执行路径。对于java序言来说,内存模型通过考察在程序执行路径中每一个读操作,根据特定的规则,检查写操作对应的读操作是否能是有效的。

java内存模型只是定义了一个规范,具体的实现可以是根据实际情况自由实现的。但是实现要满足java内存模型定义的规范。内存模型就是规定了一个规则,处理器如何同主内存同步数据的一个规则。

内存模型 (memory model)

内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的底层细节.

不同平台间的处理器架构将直接影响内存模型的结构.

在C或C++中, 可以利用不同操作平台下的内存模型来编写并发程序. 但是, 这带给开发人员的是, 更高的学习成本.相比之下, Java利用了自身虚拟机的优势, 使内存模型不束缚于具体的处理器架构, 通过Java内存模型真正实现了跨平台.(针对hotspot jvm, jrockit等不同的jvm, 内存模型也会不相同)

内存模型的特征:

a, Visibility 可视性 (多核,多线程间数据的共享)

b, Ordering 有序性 (对内存进行的操作应该是有序的)

Java内存模型 ( java memory model )

根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。

每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。




其中, 工作内存里的变量, 在多核处理器下, 将大部分储存于处理器高速缓存中, 高速缓存在不经过内存时, 也是不可见的.

jmm怎么体现可视性(Visibility) ?

在jmm中, 通过并发线程修改变量值, 必须将线程变量同步回主存后, 其他线程才能访问到.

jmm怎么体现有序性(Ordering) ?

通过Java提供的同步机制或volatile关键字, 来保证内存的访问顺序.

缓存一致性(cache coherency

什么是缓存一致性?

它是一种管理多处理器系统的高速缓存区结构,其可以保证数据在高速缓存区到内存的传输中不会丢失或重复。(来自wikipedia)

举例理解:

假如有一个处理器有一个更新了的变量值位于其缓存中,但还没有被写入主内存,这样别的处理器就可能会看不到这个更新的值.

解决缓存一致性的方法?

a, 顺序一致性模型:

要求某处理器对所改变的变量值立即进行传播, 并确保该值被所有处理器接受后, 才能继续执行其他指令.这个模型定义了程序执行的顺序和代码执行的顺序是一致的。也就是说 如果两个线程,一个线程T1对共享变量A进行写操作,另外一个线程T2对A进行读操作。如果线程T1在时间上先于T2执行,那么T2就可以看见T1修改之后的值。这个模型定义比较严格,在多处理器并发执行程序的时候,会严重的影响程序的性能。因为每次对共享变量的修改都要立刻同步会主内存,不能把变量保存到处理器寄存器里面或者处理器缓存里面。导致频繁的读写内存影响性能。

b, 释放一致性模型: (类似jmm cache coherency)

允许处理器将改变的变量值延迟到释放锁时才进行传播.

Java内存模型的缓存一致性模型 - "happens-before ordering(先行发生排序)"

一般情况下的示例程序:

x = 0;
y = 0;
i = 0;
j = 0;

// thread A
y = 1;
x = 1;

// thread B
i = x;
j = y;
在如上程序中, 如果线程A,B在无保障情况下运行, 那么i,j各会是什么值呢?

答案是, 不确定. (00,01,10,11都有可能出现),这里没有使用Java同步机制, 所以Java内存模型有序性和可视性都无法得到保障. happens-before ordering( 先行发生排序) 如何避免这种情况? 排序原则已经做到:

a, 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前.

b, 对象监视器的解锁发生在等待获取对象锁的线程之前.

c, 对volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前.

d, 对一个线程的 Thread.start() 调用 发生在启动的线程中的所有操作之前.

e, 线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前.

规则a应该比较好理解,因为比较适合人正常的思维。比如在同一个线程t里面,代码的顺序如下:

因为以上是在同一个线程里面,按照规则1 也就是按照代码顺序,A = 1 先行发生 r1 =A ,那么r1 = 1

再看规则b,下面是jsr133的例子



按照规则b,由于unlock操作先于发生于lock操作,所以X=1对线程2里面就是可见的,所以r2 = 1



在分析以下,看这个例子,由于unlock操作先于lock操作,所以线程x=1对于线程2不一定是可见(不一定是现行发生的),所以r2的值不一定是1,有可能是x赋值为1之前的那个状态值(假设x初始值为0,那么此时r2的值可能为0)

对于规则c,我们可以稍微修改一下我们说明的第一个例子

-------------------------------------------------------

-------------------------------------------------------

  因为对于volatile类型的变量B,线程1对B的更新马上线程2就是可见的,所以r1的值就是确定的。由于A是非valotile类型的,所以值不确定。

规则d,e,f这里就不解释了,知道规则就可以了。

可以从以上的看出,先行发生的规则有很大的灵活性,编译器可以对指令进行重新排序,以便满足处理器性能的需要。只要重新排序之后的结果,在单一线程里面执行结果是可见的(也就是在同一个线程里面满足先行发生原则1就可以了)。

为了实现 happends-before ordering原则, Java及JDK提供的工具:

a, synchronized关键字

b, volatile关键字

c, final变量

d, java.util.concurrent.locks包(since jdk 1.5)

e, java.util.concurrent.atmoic包(since jdk 1.5)

使用了happens-before ordering的例子:




(1) 获取对象监视器的锁(lock)

(2) 清空工作内存数据, 从主存复制变量到当前工作内存, 即同步数据 (read and load)

(3) 执行代码,改变共享变量值 (use and assign)

(4) 将工作内存数据刷回主存 (store and write)

(5) 释放对象监视器的锁 (unlock)

注意: 其中4,5两步是同时进行的.

这边最核心的就是第二步, 他同步了主内存,即前一个线程对变量改动的结果,可以被当前线程获知!(利用了happens-before ordering原则)



对比之前的例子

如果多个线程同时执行一段未经锁保护的代码段,很有可能某条线程已经改动了变量的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。

部分摘自:http://developer.51cto.com/art/200906/131393.htm

更多关于jsr133规范:/article/4878224.html(文中有笔误,但不影响阅读。最后关于final和volatile语义的注解也很值得学习)

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: