目录
目标
讲述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的接口方法)从操作数栈中取出数据,进行操作,然后将计算结果放入操作数栈中。 - 用于准备[调用方法时需要传递的参数],以及存储方法的返回结果。
- 针对JVM指令,
- 每个帧,都具有一个后进先出LIFO的操作数栈,操作数栈的最大深度在编译期就已经确定。
- 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 年老代
- New 年轻代
- 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均保证被安全构建的成员且仅保证初始化的数据的可见性,即一旦在初始化后被修改,即无可见性。所以要保证可见性和安全性,还是需要通过锁来控制对其的内存操作。