JMM-java内存模型

目录

目标

物理内存模型

栈 Stack(线程独享)

维度

帧frame

为什么要设计栈

栈溢出StackOverflowError

栈引起OOM

配置与影响

PC计数器(线程独享)

本地方法栈(线程独享)

堆 Heap

what

heap管理

人为配置

导致OOM原因

方法区(before 1.8)

存储什么

元空间(since 1.8)

内存操作模型

背景

解决方式

执行乱序问题的解决方式

Happens-before规则

借助同步

共享变量不可见问题的解决方式

非安全发布

static与final发布时的可见性保证

发布后可见性


目标

讲述Java物理内存模型和内存操作模型。

物理内存模型

栈 Stack(线程独享)

维度

栈是线程维度,与线程是同一个生命周期。线程创建时,jvm会分配栈;线程退出,栈被销毁。

栈由帧组成。帧是方法维度,方法被调用时,向栈中插入帧;方法完成时,帧被弹出。

除了压入和弹出帧之外,栈不会进行其他操作,所以frame帧可以由heap堆来分配空间,栈的内存空间不需要是连续的

帧frame

保存线程中方法调用的状态,如方法参数/局部变量/运算的中间结果/返回值

  • local variables 本地变量数组
    • 由local variable组成的index-based-0数组,存储的数据包括:方法所属对象的引用,方法入参,方法内声明并赋值的局部变量。(通过查看class文件,可清楚看到astore指令)
    • 长度定义:在编译期,方法帧中使用的本地变量数组长度已经确定。
    • 存储类型:
      一个local variable可存储byte/short/int/char/boolean/float/reference/returnAddress;
      两个local variable可存储long/double.
    • 针对类方法,连续存储方法参数;
    • 针对对象方法,下标0存储对象的引用,之后连续存储方法参数。
  • operand stack 操作数栈
    • 每个帧,都具有一个后进先出LIFO的操作数栈,操作数栈的最大深度在编译期就已经确定。
      long/double占用两个unit,其他类型占用一个unit。
    • 暂时存放JVM指令需要的操作数与返回结果。
      • 针对JVM指令,
        A:将constant常量(icoust_1 常量int 1,aconst_null 常量null, ldc #xx 加载常量池中xx位置的常量)/local variable本地变量(aload)/field域(getField #xx 加载位置xx的field)数据加载到操作数栈中;
        B:指令(invokevirtual #xx,执行位置xx的普通方法;invokeinterface #xx 执行位置xx的接口方法)从操作数栈中取出数据,进行操作,然后将计算结果放入操作数栈中。
      • 用于准备[调用方法时需要传递的参数],以及存储方法的返回结果。
  • dynamic Linking 动态链接
    • 每个帧frame都会持有一个[当前方法类型对应的常量池]的引用,通过这个引用可以方便的使用常量池来支持方法的动态链接。
    • 由于方法的class file code会通过 符号引用的方式 指向 被调用的方法以及被访问的变量,所以动态链接的作用为:
      1-将符号方法引用 翻译为 具体的方法引用,必要时进行类加载,解决未定义的语法。
      2-将符号变量访问 翻译为 (与变量在运行时的位置相关的)存储结构的适当偏移量。

为什么要设计栈

其实存储操作数,有两种方式,一是使用栈,一是使用cpu的寄存器,而且使用cpu的寄存器会更快。那为什么要用栈呢?

原因
平台无关性:如果使用cpu寄存器存储操作数,则依赖具体的硬件。
calss文件紧凑性,便于网络传输:编译时就会计算使用多少栈,对常量池进行合并,减少数据量。

栈溢出StackOverflowError

原因:一个线程的JVM stack 所需要的栈大小超过了栈的允许大小上限

举例

  • 方法嵌套调用深度很大,例如递归调用,导致不断的新建方法帧,且不断的加入到线程栈中,最终栈空间不够用。
  • 方法帧具有很大的local varables(很多的入参,很多的局部变量),导致线程栈空间不够用

栈引起OOM

原因:已存在的线程栈的大小动态扩增时/为新线程创建栈时,内存不够用。

配置与影响

JVM支持配置栈大小,-Xss=xx,默认值1M。相同物理内存情况下,栈大小影响线程数量。

 

PC计数器(线程独享)

每个线程 独立拥有 一个PC计数器

存储什么?

当前方法不是native本地方法,PC计数器存储当前被执行的JVM指令的地址

 

本地方法栈(线程独享)

线程维度,生命周期与对应线程相同

导致StackOverflowError的原因:线程需要的本地方法栈大小 超过 本地方法栈的上限大小

导致OOM的原因:线程对应的本地方法栈自动扩展时需要的空间 大于 内存剩余空间;新建本地方法栈所需的大小 大于 内存剩余空间。

 

堆 Heap

what

heap是run-time内存空间,用于为所有class instance 和 array分配空间, 对JVM中所有线程 是共享的。

由gc对heap进行存储空间的整理。可以是固定大小或动态扩展的,不连续的内存空间。

heap管理

  • 分代
    • New 年轻代
      • Eden
      • from survivor
      • to survivor
    • Old 年老代
  • GC(详见内存管理)

人为配置

  • -Xms 堆初始大小 
  • -Xmx 堆最大小
  • -Xmn -XX:NewSize -XX:MaxNewSize 年轻代大小
  • -XX:NewRatio -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
    Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
  • -XX:SurvivorRatio 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

导致OOM原因

gc后,eden或old的剩余空间不满足需要的空间。

 

方法区(before 1.8)

JVM中所有线程共享。heap的一个逻辑区域

存储什么

为[每个]class存储这些数据

  • run-time 常量池
    • 定义:run-time contant pool是class文件中CONSTANT_POOL table的run-time表现形式。
    • 存储最初的符号表示,符号引用都与class中的数据结构关联。
    • 创建时机:class/interface被JVM加载loading的时候
    • 导致OOM的原因:创建run-time常量池所需要的空间 大于 方法区的剩余空间
  • field data 域数据
  • method data 方法数据
  • code of methods and constructor 方法/构造器的代码

 

元空间(since 1.8)

取代方法区,不占用jvm分配的内存,而是使用堆外内存。可用启动参数进行配置。

内存操作模型

背景

平台内存模型:多处理器 + 处理器缓存 + 定期与主存同步,弱一致性保证
重排序:编译后的代码顺序可能与程序顺序不一致,乱序或并行的执行指令

导致的问题:执行顺序乱序,共享变量不可见等问题。

解决方式

  • 为多种内存操作规定了Happens-before的偏序关系,且这些关系是基于内存操作和同步操作等级别来定义的,
  • 规定了一个线程的内存操作在哪些情况下是对其他线程可见的,保证内存一致性和可见性。

执行乱序问题的解决方式

Happens-before规则

  • 程序执行顺序规则
    • 程序中操作A在操作B之前,则在线程中操作A也必须先于操作B执行。
  • 监视器锁规则
    • 针对同一个监视器锁,解锁操作必须先于加锁操作执行。
  • volatile变量规则
    • 写操作 必须先于 读操作 被执行。
  • 线程启动规则
    • Thread.start操作 必须先于 线程其他任何操作。
  • 线程结束规则
    • 线程A的任何操作 必须先于 其他线程感知到线程A已结束,或者ThreadA.join成功返回,或者ThreadA.isalive返回false。
  • 中断规则
    • 线程A调用线程B的interruput 必须先于 线程B监测到interrupt(中断异常或检查中断状态)
  • 终结器规则
    • 对象的构造函数的执行 必须先于 对象的终结器执行。
  • 传递性规则
    • 若操作A先于操作B,操作B先于操作C,则操作A先于操作C。

借助同步

what

happens-before的程序顺序规则 与 其他顺序规则(锁或volatile) 进行结合,从而达到 对 没有用锁保护的变量的 访问操作的排序。换言之:通过程序顺序规则+其他顺序规则,达到对共享内存不加锁也可保证线程安全的目的。

when

当ReentrantLock无法满足性能要求时,才应该使用。

例子

  • FutureTask的内部同步器实现,就使用借助同步技术。达到的效果:set执行结果 先于 release;acquire 先于 get执行结果。

  • 一个线程将数据A放到线程安全集合中  先于  另一个线程从该集合中获取数据A
  • CountDownLancher的倒数操作 先于 线程从condition的await返回 
  • Future代表任务的所有操作 先于 Future.get返回结果
  • 向Executor中添加任务 先于 任务被执行

 

共享变量不可见问题的解决方式

非安全发布

发布一个共享对象  与  一个线程访问该共享对象 之间没有形成happens-before的顺序关系。
后果:访问到的共享对象还没有初始化完成。

static与final发布时的可见性保证

安全发布模式 涉及static的内存模型,指的就是static field的初始化时机

  • 延迟初始化
    • 缺点:每次调用都有同步开销。
  • 静态成员初始化器
    • 初始化器(静态代码块)采用特殊方式处理静态成员:在类被加载后与类被使用之前,会进行静态初始化,期间jvm会获取一个锁,并且每个线程至少获取一次这个锁,所以保证静态成员的内存写入对所有线程均可见。
    • 这个规则仅使用于初始化阶段,如果静态成员是可变的,那么仍然需要在使用时添加锁,以保证可见性和数据安全性。
  • 提前初始化
    • 类加载 happens before 进行静态初始化 before 类被使用
  • 延长初始化站位模式
  • 双重检查加锁已经[被废弃],需要使用volatile以保证可见性。

初始化过程的安全性 final内存模型
final确保初始化过程的安全性,所以在初始化过程中无需加锁。
只要能确保安全的初始化final成员,则该final成员与通过该final成员可达到的任意变量都具有可见性。但是如果可到达的变量在之后被修改,则无可见性。

发布后可见性

static和final均保证被安全构建的成员且仅保证初始化的数据的可见性,即一旦在初始化后被修改,即无可见性。所以要保证可见性和安全性,还是需要通过锁来控制对其的内存操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值