Java内存模型

1 Java内存模型

1.1 JVM虚拟内存分布图

  • Java语言中,采用共享内存来实现多线程之间的信息交换和数据同步。说Java内存模型之前,先看一下Java内存结构,也就是运行时数据区域。
  • 如下图所示,Java虚拟机在执行Java程序的过程中,会把它管理的内存划分为几个不同的数据区域,每个区域有各自的用途、创建时间和销毁时间。
    JVM虚拟内存分布图

1.2 内存区域详解

1.2.1 寄存器/程序计数器
  • 程序计数器严格演说是一个数据结构,用来存储当前正在执行的程序的内存地址。
  • Java支持多线程,所以程序不可能一直线性执行。当多个线程交叉执行时,被中断的线程锁执行到的内容地址必然要保存下来,以便于该线程恢复执行时再按照被中断时的指令继续执行下去。
  • 为了线程切换时能恢复到正确的位置,每个线程都有独立的程序计数器。每个线程之间的计数器互不影响,独立存储,这部分内存区域也被称为“线程私有”的内存,线程安全。
1.2.2 Java栈(Java Stack)
  • Java栈总是与线程关联的,每创建一个线程,JVM虚拟机就会为其创建相应的Java栈。
  • Java栈中包含多个栈帧,每运行一个方法,就创建一个栈帧,栈帧中包含一些局部变量、操作栈、方法返回值等信息。
  • 每执行完一个方法时,就会弹出对应栈帧的元素作为方法的返回值,并清除该栈帧。
  • Java栈的栈顶栈帧就是当前的活动栈,也是当前正在执行的方法,程序计数器也会指向该栈帧。
  • 由于Java栈与线程对应,数据不是线程共有的,因此线程安全,不存在同步锁问题。
  • 栈的大小可以设置,其大小决定了函数调用可达深度。
1.2.3 堆(Heap)
  • 堆是JVM所管理的内存中最大的一块,被所有线程共享,不是线程安全的,在JVN启动时创建。
  • Java堆也是GC管理的主要区域。
1.2.4 方法区(Method Area)
  • 方法区中存放了要加载类的信息(类名,访问修饰符等)、类中的静态变量、final变量、Field信息、方法信息。
  • 程序中通过Class的getName.isInterface等方法来获取信息时,数据都来源于方法区。
  • 方法区是堆中的一部分,线程共享,但不像其他部分一样会被GC频繁回收,存储的信息相对稳定。
1.2.5 本地方法栈(Native Method Stack)
  • 本地方法栈与Java栈作用类似,只是Java栈为JVM执行Java方法提供服务,本地方法栈为JVM执行Native方法提供服务。

1.3 Java常量池

1.3.1 概念

Java常量池分为:静态常量池和运行时常量池:
- 静态常量池:即*.class文件的常量池,不只包含字符串(数字)常量,还包括方法、类的信息等。

  • 运行时常量池:JVM在完成类的装载后,会将class文件中的常量载入到内存中,并保存在方法区,通常所指的为运行时常量池。
1.3.2 结论
  • 对Java常量池的理解,应关注编译器行为。
  • 运行时常量池中的常量,来源于各个class文件中的常量池。
  • 程序运行时,除非手动像Java常量池中添加常量(如调用intern方法),JVM不会自动添加。
  • 另有整型常量池,只不过数值型常量池不可以手动添加,程序启动时常量池中的常量已经确定了,如整型的范围是-128~127,在这个范围内的数值可以用到常量池。
1.3.2 优点
  • 常量池是为了避免频繁的创建和销毁对象而影响系统性能,实现了对象共享。
  • 例如在编译期将所有字符串都放入常量池:
    • 节省内存空间:所有相同的字符串常量被合并,只占用一个空间。
    • 节省运行空间:比较字符串时,==比equals快,对于引用变量,只用==判断其引用是否相等,也就可以判断其实际值是否相等(说明:看String的equals方法源码,是用==优先判断内存地址是否相等,相等时直接返回true,不等时才会比较字符串的具体内容)。

1.4 并发编程

1.4.1 主内存和工作内存
  • Java内存模型的主要目标是定义程序中各个变量的访问规则,即JVM将变量存储到内存和从内存取出这样的底层细节。
  • JVM规定所有变量都存储在主内存,每个线程都有自己的工作内存。线程的工作内存中保存了线程该线程使用到的变量主内存的副本拷贝,线程对变量的所有操作(读写等)都只能在工作内存中进行,不能直接读写主内存的变量。
  • 不同线程之间也无法直接访问其工作内存中的变量,线程之间值的传递都需要通过主内存完成。
  • 线程1和线程2进行数据交换步骤:
    • 线程1把工作内存1中修改过的变量刷新到主内存中。
    • 线程2读取主内存中线程1更新过的变量,然后拷贝一份到线程2的工作内存2中。
1.4.2 原子性
  • 原子性是指一个操作不允许被打断,要么全部执行完毕,要么不执行。类似事务操作,要么执行成功,要么回退到操作执行前的状态。
  • 使用synchronized关键字,同步方法,可以保证数据操作的原子性。
1.4.3 可见性
  • 可见性指当一个线程对共享变量做了修改,其他线程可以立刻知道该变量的变化。
  • volatile、synchronized、Lock、final可以实现可见性。
    • volatile关键字特殊规则保证了变量值被修改后立刻同步到主内存中,每次使用volatile变量前先从主内存中刷新该变量的值。volatile禁止指令重排序,不保证原子性。
    • synchronized和Lock:在同步方法/同步代码块、Lock.lock()方法开始时,会将主内存的变量刷新到工作内存中,在同步方法/同步代码块结束、Lock.unlock()方法执行结束时,会将工作内存中变量值同步到主内存中。
    • 被final修饰的变量,构造函数一初始化完成,并且在构造函数中没有把“this”传递出去,那么其他线程就可以看到变量的值。(“this”引用逃逸很危险,其他线程很可能通过该实例访问到只“初始化一半”的变量)
1.4.4 有序性
  • 有序性是指在本线程内观察程序的执行操作是有序的(因为线程内表现为串行语义),在一个线程中观察另一个线程的所有操作是无序的(“指令重排”和“工作内存与主内存同步延迟”现象,只有多线程中才会出现)。
  • Java提供volatile和synchronized来保证多线程之间操作的有序性:
    • volatile关键字本身通过加入内存屏障来禁止指令的重排序。
    • synchronized关键字通过一个变量在同一时间只允许有一个线程对其加锁的规则来实现。
1.4.5 指令重排序
  • 指令重排序是编译器或运行时环境为了优化程序性能而采用对指令进行重新排序的一种手段。虚拟机会按照自己的一些规则将程序编写顺序打乱,尽可能充分的利用CPU。
  • 编译器和处理器在重排序时,会遵循数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。
  • 数据依赖性:两个操作同时访问一个变量且有写操作,则这两个操作存在数据依赖性。
  • 单线程中指令执行的最终结果与其顺序执行一致,叫做as-if-serial语义。
1.4.6 happends-before原则

重排序在多线程环境下出现的概率还是挺高的,在关键字上有volatile和synchronized可以禁用重排序,除此之外还有一些规则,也正是这些规则,使得我们在平时的编程工作中没有感受到重排序的坏处:
- 程序次序规则(Program Order Rule):在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是代码顺序,因为要考虑分支、循环等结构。
- 监视器锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个对象锁的lock操作。这里强调的是同一个锁,而“后面”指的是时间上的先后顺序,如发生在其他线程中的lock操作
- volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先发生于后面对这个变量的读操作,这里的“后面”也指的是时间上的先后顺序。
- 线程启动规则(Thread Start Rule):Thread独享的start()方法先行于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule):线程中的每个操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值检测到线程已经终止执行。
- 线程中断规则(Thread Interruption Rule):对线程interrupte()方法的调用优先于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否已中断。
- 对象终结原则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
- 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

正是以上这些规则保障了happen-before的顺序,如果不符合以上规则,那么在多线程环境下就不能保证执行顺序等同于代码顺序,也就是“如果在本线程中观察,所有的操作都是有序的;如果在一个线程中观察另外一个线程,则不符合以上规则的都是无序的”,因此,如果我们的多线程程序依赖于代码书写顺序,那么就要考虑是否符合以上规则,如果不符合就要通过一些机制使其符合,最常用的就是synchronized、Lock以及volatile修饰符。

1.5 StackOverflowError和OutofMemoryError

1.5.1 StackOverflowError
  • 如果线程所请求的栈的深度大于JVM所允许的深度,将抛出StackOverflowError。
  • Java栈和本地方法栈都有可能抛出该异常。
1.5.2 OutofMemoryError
  • 如果虚拟机是支持动态扩展的,扩展时无法申请到足够的内存,抛出OutofMemoryError,Java栈和本地方法栈都有可能抛出该异常。
  • 当方法区使用的内存超出其允许的大小时,抛出OutofMemoryError。


参考链接(Java内存模型):https://www.cnblogs.com/lewis0077/p/5143268.html
参考链接(Java常量池):https://www.cnblogs.com/dreamroute/p/5946272.html
参考链接(Java常量池):http://blog.csdn.net/gcw1024/article/details/51026840
参考链接(指令重排序):http://blog.csdn.net/beiyetengqing/article/details/49580559
参考链接(指令重排序):http://blog.csdn.net/wxwzy738/article/details/43238089
参考链接(happends-before):http://ifeve.com/easy-happens-before/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值