Java内存模型笔记

参考博文:https://www.cnblogs.com/lewis0077/p/5143268.html

Java运行时数据区分为以下几个内存区域:

    PC寄存器/程序计数器:

    用于保存当前正在执行的程序的内存地址,为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,各个线程之间计数器互不影响,独立存储;

  Java栈:

  每当创建一个线程,JVM就会为该线程创建对应的Java栈,在Java栈中又会包含多个栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量,操作栈和方法返回等信息。每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新栈帧被创建,并被放置到Java栈的栈顶,变成当前的活动栈。

  由于Java栈是与线程对应起来的,Java栈数据不是线程共有的,所以不需要关系其数据一致性,也不会存在同步锁的问题。如果线程的递归深度过深,或者创建线程过多,导致栈深度大于虚拟机允许的深度,将会抛出StackOverflowError异常;如果虚拟机可以动态扩展,扩展到无法申请到足够的内存,就会抛出OutOfMemoryError异常。(可以使用-Xss参数设置栈的大小,栈的大小直接决定了函数调用的可达深度)

  堆 Heap

  堆是JVM所管理的内存最大的一块,是被所有Java线程锁共享的,不是线程安全的,在JVM启动创建。堆是存储Java对象的地方(所有的对象以及数组都要在堆上分配)。Java堆是GC管理的主要区域,从内存回收的角度来看,Java堆可以分为:新生代和老年代。

  方法区

  方法去存放了要加载的类的信息、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。方法区时被Java线程锁共享的,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

  方法区也是堆中的一部分,就是我们通常所说的Java堆中的永久区Permanet Generation,大小可以通过参数设置,-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

  常量池

  常量池属于方法区中的一个数据结构。常量池中存储了如字符串,final变量值,类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。一般分为两类:字面量和引用量。

  字面量就是字符串、final变量等。

  引用量包含:类和接口的权限定名,字段的名称和描述符,方法的名称和描述符。类名和方法名属于引用量。引用量最常见的是在调用方法  时候,根据方法名找到方法的引用,并以此定位到函数体进行函数代码的执行。

  本地方法栈

  本地方法栈和Java栈相似,区别是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。

 

主内存和工作内存:

  Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。JMM规定了所有的变量都存储在主存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用的变量的主内存的副本拷贝,线程对变量的所有操作(读/写)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法之间访问对方工作内存中的变量,线程之间值得传递都需要通过主内存来完成。

线程1和线程2要想进行数据的交换一般需要经历下面步骤:

1)    线程1把工作内存1中的更新过的共享变量刷新到主内存中去;

2)    线程2到主内存中读取线程1刷新过的共享变量,然后copy一份到工作内存2中去

Java内存模型是围绕着并发并发编程中原子性、可见性、有序性这三个特征来建立的:

      原子性:一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似数据库的实务操作。

  大部分的基本数据类型都是原子操作,long和double类型的变量是64位,但是在32位JVM中,32位的JVM会将64位数据的读写操作分为2次32位的读写操作来进行,这就导致了long、double类型的变量在32位虚拟机中是非原子操作,数据有可能会被破坏,也就是在多线程并发访问的时候是非线程安全的。要想保证64位数据的原子性,需要使用synchronize保证同步性。

      可见性:一个线程对共享变量做了修改后,其他线程立即能够感知到变量的修改。

  Java内存模型是通过将在工作内存中的变量修改后的值同步主内存,在读取变量前从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的。

  无论是普通变量还是volatile变量都是如此,区别在于:volatile的特殊规则保证了volatile变量值修改后的新值立刻同步到主内存,每次使用volatile变量立即从主内存中刷新,因此volatile保证了多线程之间的操作变量的可见性,而普通变量则不能保证这一点。除了volatile关键字可以实现可见性之外,还有synchronized,Lock,final也是可以实现的。

  使用synchronized关键字,在同步方法/同步代码块开始时,使用共享变量时会从主内存中刷新值到工作内存(线程私有工作内存),在同步方法/同步代码块结束,会将工作内存中的变量值同步到主内存中(即将线程私有的工作内存中的值写入到主内存进行同步)

  使用Lock接口最常用的实现ReetrantLock重入锁来实现可见性:即lock.lock()方法,这个和synchronized开始位置有相同的语义,即使用共享变量时会从主内存中刷新变量值到工作内存中,在方法的最后finally块执行lock.unlock()方法,同样将工作内存中的值同步到主内存中去。

       有序性:对于一个线程的代码而言,不一定是从前往后执行,在单线程中,可以确保执行顺序,但是在多线程并发时,程序有可能出现乱序。即在本线程内而言,操作是有序的;如果在一个线程观察另外一个线程,所有操作都是无序的。(前半句指线程内串行语义,后半句指令重排和工作内存和主内存同步延迟现象)Java提供了两个关键字volatile和synchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令重排,而synchronized关键字通过一个变量在同一时间只允许一个线程对其进行加锁的规则来实现,在单线程中,不会发生指令重排和工作内存和主内存同步延迟现象,只会出现在多线程并发程序中。

衡量并发安全问题一切必须以happen-before原则为准,即如果一个操作happens-before另一个操作,那么第一个操作的执行结果对第二操作可见,并且第一个操作的执行顺序排在第二个操作之前。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值