Java内存模型

Java内存模型

JVM-Java内存模型

准备 Java 面试绕不过去 JVM,Java内存模型经常问到。于是写一篇Java 内存模型的相关笔记来加深记忆!

在这里插入图片描述

Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有的用途以及创建销毁的时机,其中灰色区域内的绿色部分代表的是所有线程共享的数据区域,而橙色部分代表的是每个线程的私有数据区域

Java 运行时数据区

方法区(Method Area)

方法区属于线程共享的内存区域。GC 回收的一块区域(回收频率低,只有在 Full GC 时回收)。主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。值得注意的是在方法区中存在一个叫运行时常量池(Runtime Constant Pool)的区域,它主要用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后存放到运行时常量池中,以便后续使用。

JDK 1.7 之前,HotSpot 方法区的实现是永久代运行时常量池包含了:类元信息、运行时常量池、JIT 代码缓存。

JDK 1.7 时,字符串常量、静态变量从永久代移动到了堆中。

JDK 1.8 时,永久代被废弃,转而使用元空间实现,元空间使用的本地内存实现的。

Java 内存中有相当大一块空间是用于存储字符串的,而很多字符串(toString() 产生的)都是只用一次,每次的字符串大概率不相同;如果字符串常量放在方法区中,很难及时回收。因此,放到堆中后,内存回收性能会更好。

方法区存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和接口初始化以及实例初始化中使用的特殊方法。

  • 运行时常量池
    Java 虚拟机为每个类和接口维护一个运行时常量池,这种数据结构和传统编程语言实现的符号表的目的基本一致。运行时常量池中有两种条目:符号引用(稍后可能会被解析),以及不需要进一步处理的静态常量
    • 符号引用
      运行时常量池中的符号引用是根据constant_pool表中每个条目的结构派生出来的。
    • 静态常量
      运行时常量池中的静态常量也根据每个条目的结构从constant_pool表中的条目派生出来。包含了字符串常量、数字常量。
JVM堆(Java Heap):

Java堆是线程共享的内存区域。堆是 Java 最大的一块内存,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,是 GC 回收的主要区域(另一块区域是方法区)。因此很多时候也被称做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

堆被划分为了 年轻代、老年代;年轻代又被分为了 Eden区Survivor0区Survivor1区(一般比例为8:1:1)。将堆内存这么细划分,好处在于:可以针对不同分区内存采用不同的内存分配策略和GC回收策略,进行更细粒度的控制,提升内存分配、回收性能。

属于线程私有的数据区域。栈分为虚拟机栈本地方法栈

  • 虚拟机栈与线程同时创建,总数与线程关联,代表Java方法执行的内存模型。进入一个新方法时,会在栈上划分一块内存作为该方法的栈帧;栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。从一个方法中退出时(不管是正常 return 还是异常退出),该栈帧都会被释放掉。栈不需要 GC 进行回收。每个方法从调用直到结束就对应一个栈桢在虚拟机栈中的入栈和出栈过程,如下图所示:

    在这里插入图片描述

tip: C/C++ 进行方法调用时,call 指令会将 SP 寄存器保存在栈中;从方法返回时,return 指令会将 SP 指针从栈中弹出并赋值给 SP。这两条指令保证了栈帧的释放。

call method -> push SP
return -> pop SP
一个栈帧包含了:局部变量表、操作数栈(临时存储运算结果、变量值的地方)、动态链接(符号引用->方法区中方法的直接引用)、方法出口(方法调用的下一条指令的地址)。

image.png

Java虚拟机栈会出现的错误:

StackOverFlowErrorjvm规定了虚拟机栈的最大深度,当执行当前线程时栈帧压入的深度大于了规定的深度,就会抛出该错误,一般是由于递归导致的无限嵌套调用递归方法。
OutOfMemoryErrorJVM的内存大小可以动态扩展,如果虚拟机在扩展栈时无法申请到足够的内存空间,就会抛出该错误。

虚拟机栈是为 Java 方法服务的,本地方法栈就是为 native 方法服务的。

  • 本地方法栈属于线程私有的数据区域,这部分主要与虚拟机用到的 Native 方法相关,本地方法栈和java虚拟机栈十分相似,差别不过是java虚拟机栈是为了java虚拟机执行字节码所服务,而本地方法栈则是为了执行native方法所服务的,所以本地方法也是一个私有的内存区域,也是后进先出栈,作用是支撑native方法的调用。一般情况下,我们无需关心此区域。
PC-程序计数器

属于线程私有的数据区域,是一小块内存空间。

用于保存线程当前正在执行的 Java 虚拟机指令的地址。注意,只能保存执行 Java 虚拟机指令的地址,不能保存 native 指令的地址。字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。JVM 是用内存模拟了一个 PC。在看完以上的内容,我们画张图看下整体的结构。

image.png

Java 线程与主内存的关系

Java 内存模型(JMM) 抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。
在 JDK1.2 之前,Java 的内存模型实现总是从 主存 (即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存 本地内存 (比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

什么是主内存?什么是本地内存?
  • 主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
  • 本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。

Java 内存模型其实是一种规范,定义了很多东西:

  • 所有的变量都存储在主内存(Main Memory)中。
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
  • 不同的线程之间无法直接访问对方本地内存中的变量。

线程间通信

线程间的通信一般有两种方式进行,一是通过消息传递,二是共享内存。Java 线程间的通信采用的是共享内存方式,JMM 为共享变量提供了线程间的保障。如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 规范下会有一系列的操作。

img

在多线程的情况下,对主内存中的共享变量进行操作可能发生线程安全问题,比如:线程 1 和线程 2 同时对同一个共享变量进行操作,执行+1操作,线程 1 、线程2 读取的共享变量是否是彼此修改前还是修改后的值呢,这个是无法确定的,这种情况和CPU的高速缓存与内存之间的问题非常相似

如何实现主内存与工作内存的变量同步,为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:

  • lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。工作内存即本地内存
  • use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

Java并发的三大特性

原子性

原子性:即一个或者多个操作作为一个整体,要么全部执行,要么都不执行,并且操作在执行过程中不会被线程调度机制打断;而且这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换(context switch)

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

部分知识来源:小牛呼噜噜

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

部分知识来源:小牛呼噜噜

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值