讲述Java内存模型
Java体系
Java内存模型
1. Run-Time Data Areas 运行时数据区
JAVA虚拟机定义了各种用于程序执行时的运行时数据区:
a) 一些数据区是与虚拟机的生命周期一致的:创建于虚拟机开始运行时,销毁于虚拟机退出时
b) 一些数据区是与线程生命周期一致的:创建于线程创建,毁于线程推出
在数据区中:
线程私有:Program Counter Register、JVM Stacks、Native Method Stacks(无GC)
线程共享:Heap、Method Area(有GC)
1.1 Program Counter Register 程序计数器
程序计数器是指CPU中的寄存器
执行java方法时,程序计数器是有值的,存储的是当前执行的字节码指令的地址(保存下一条指令的所在存储单元的地址)CPU根据保存的指令地址找到指令,然后在执行。目的是防止多线程执行时出现干扰,单线程的话可以没有PC
执行native本地方法时,程序计数器的值为空(Undefined):因为native方法是java直接调用本地C/C++库进行实现,那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
程序计数器PC有足够的空间存储返回地址信息,占用的内存很小,所以PC是唯一一个JVM中没有OutOfMemoryError的区域。
1.2 Native Method Stacks 本地方法栈
作用场景:在程序调用或JVM调用本地方法(非java语言编写,不受JVM管理)接口(Native)时候启用,即本地方法栈将会在每个线程创建的是将被分配内存
存储的是本地方法的数据
如上图的线程的start()方法创建时会调用一个native方法
异常行为
- 栈溢出:由于线程执行需要更大的栈而产生的异常,即线程过程执行需要存储大量内容而引起的
- 内存溢出:由于内存不足无法分配给栈而导致的异常
1.3 JVM Stacks 虚拟机栈
每个Java虚拟机线程都有一个私有的Java虚拟机栈,JVM栈是运行的单位
- 生命周期:因栈是线程私有的,所以与线程的生命周期一致:随线程创建,随线程结束销毁
- 存储数据:Java虚拟机栈中存储栈帧:当前线程运行时所需要的局部变量、对象的引用、计算的结果、返回信息等
- 栈的行为:压入(push)和弹出(pop)栈帧
- 存取类型:压入和弹出栈帧采用FILO先进后出,因为是调用方法总是先完成。
- Java虚拟机栈的内存不需要是连续的
- JVM默认给每个栈分配的空间是1M,也可以自己在configurations中设置;所以如果分配给每个栈的内存越小,那么JVM所支撑的线程就越多
1.3.1 Frames 栈帧
- 存储数据:
- 局部变量表:基本数据类型、变量对象的引用(指针)
- 可操作数栈:方法执行过程中的结果
- 运行环境区:动态连接、方法返回相关信息、抛出异常
- 生命周期:每个方法都有一个栈帧,每次调用一个方法时就会创建一个栈帧,方法调用完后栈帧就被销毁,无论方法是否时正常结束或抛出异常。
- 局部变量与操作数栈的大小在编译时就确定了
- 当前栈:在线程运行时,仅有且只有一个正在运行的方法的栈帧;它对应的方法就是当前运行的方法,当前运行的类就是当前类。在局部变量和操作数栈上的操作是和当前栈帧相关的
- 栈帧会在方法调用另外一个方法或方法结束后停止。当一个方法被调用且控制权转为这个新的方法时,那么一个新的栈帧就会被创建成为当前栈帧。在方法返回时,当前栈帧就会将返回结果传递给调用者的栈帧(如果有的话)这个栈帧就会被之前的那个栈帧取代成为当前栈帧。
1.3.1.1 Local Variables 局部变量表
- 存储局部变量、对象的引用
- 一个单独的局部变量可以存储基本数据类型的值、引用,long和double型可以用两个局部变量来存储
- 局部变量使用索引来表示的,第一个局部变量的索引是0。一个整数可以当做一个索引,只要整数在[0,局部变量表的长度] 区域内。一个long、double类型的值占据两个连续的局部变量
- 虚拟机通过局部变量在方法调用过程中传递参数:
- 在类方法调用时,任何参数都在从局部变量0开始的连续局部变量中传递。
- 在实例方法调用中,局部变量0始终用于存储对调用实例方法的对象的引用(这在Java编程语言中)。 随后,任何参数都在从局部变量1开始的连续局部变量中传递。
1.3.1.2 operand stack 操作数栈
每个栈帧包含一个采用LIFO先进后出的操作数栈
存储执行过程中的结果:Java虚拟机提供指令,将局部变量或常量的值或内容加载到操作数栈上(不是局部变量和常量加载到操作数栈中)
Java虚拟机指令从操作数栈中获取操作数,对它们进行操作,并将结果推回操作数栈中。例如:iadd指令将两个int值相加,于是将操作数栈的前两个int值相加,将结果压入栈中,并且两个int值都从操作数堆栈中弹出删除。
操作数栈具有相关联的深度:其中long或double类型的值占据两个单位深度,而其他类型的值占一个单位。
方法调用时可操作数栈与局部变量的互动
- 调用add()方法时创建add方法的栈帧
- iconst_1 :把int类型的1压入可操作数栈中
- istore_1: 将可操作数栈中的int类型1存入局部变量表中的第一个位置:结果a=1;同理,b=2
- iload_1:将局部变量的第1、2个位置的数(即a、b的值)加载到操作数栈中
- 执行iadd加法:将1、2从栈中弹出相加,并将结果3压入栈中
- istore_3 :将int类型的3压入局部变量表中
- iload_3:将局部变量的第3个位置的数(即c的值)加载到操作数栈中
- return :返回
1.3.1.3 Dynamic Linking 动态链接
即多态:动态链接将抽象的引用转化为具体的引用
1.3.1.4 Normal Method Invocation Completion 方法正常结束
- 当被调用的方法执行到return返回指令时,必须返回适合的值类型(如果有的话)
- 当调用方法正常结束时:
- 当前帧用于恢复调用者的状态,包括其局部变量和操作数堆栈
- 调用者的程序计数器适当增加以跳过调用方法指令:比如调用者执行add()方法时是第9行,那么调用方法结束后PC+1,就变为第10行,这样调用者就会执行第10行的操作)
- 然后执行在调用方法的栈帧会将正常返回的值(如果有)压入该帧的操作数堆栈。
1.3.1.5 Abrupt Method Invocation Completion 方法异常结束
- 非正常结束的调用方法:抛出异常或错误
- 如果当前方法没有捕获到异常,则会导致方法调用突然结束。 这种方法结束调用时永远不会向其调用者返回值
讲完线程私有,接下来的就是线程共享的:
1.4 Heap 堆
堆是存储的单位,栈是运行的单位
- 存储内容:记录所有实例对象和数组
- 生命周期:堆随虚拟机开始时创建
- 堆内存不连续
- 垃圾收集器管理的主要区域:当方法结束后,虚拟机栈会立即消失,而堆内存存储的对象的引用不会立即消失,而是等到gc进行回收
- GC回收原则:当没有任何对象引用指向堆内存里面的对象时
- 从内存回收的角度来看,由于现在的收集器基本都采用分代收集算法,所以堆又可以细分成:新生代和老年代,新生代里面有分为:Eden空间、From Survivor空间、To Survivor空间。
- 默认情况下,新生代中Eden空间与Survivor空间的比例是8:1,可以使用参数-XX:SurvivorRatio对其进行配置。大多数情况下,新生对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,则触发一次Minor GC,将对象Copy到Survivor区,如果Survivor区没有足够的空间来容纳,则会通过分配担保机制提前转移到老年代去。
- 何为分配担保机制?在发送Minor GC前,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果是,那么可以确保Minor GC是安全的,如果不是,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于,直接进行Full GC,如果大于,将尝试着进行一次Minor GC,Minor GC失败才会触发Full GC。注:不同版本的JDK,流程略有不同。
- Survivor区作为Eden区和老年代的缓冲区域,常规情况下,在Survivor区的对象经过若干次垃圾回收仍然存活的话,才会被转移到老年代。JVM通过这种方式,将大部分命短的对象放在一起,将少数命长的对象放在一起,分别采取不同的回收策略。
1.5 Method Area(meta space)方法区
存放数据:
每个class的信息:
类名
字段信息
每个字段名字、类型(如类的全路径名、类型或接口) 、修饰符(如public、abstract、final)、属性
方法信息
每个方法的名字、返回类型、参数类型(按顺序)、修饰符、属性
运行时常量池:全局变量、所有常量、字段引用、方法引用、属性
方法代码
每个方法的字节码、操作数栈大小、局部变量大小、局部变量表、异常表和每个异常处理的开始位置、结 束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
类加载器引用(classLoader)
方法区直接放在本地的物理内存(直接内存),其他的都放在JVM内存中
- 如果内存不足会报内存不足错误
1.5.1 Run-Time Constant Pool 运行时常量池
方法区的一部分,存放在编译期间,就可以确定下来的值
字面量:如final、String及其包装类的值
1
int a=1;//1就是字面量
符号引用:由于不知道所引用类的实际地址,而以符号的形式表现出来的
与直接引用不同,直接引用直接利用指针指向具体的实际地址
- 创建时间:当Java虚拟机创建类或接口时,将构造类或接口的运行时常量池;除了编译产生能存入,运行期间也能将新的常量放入池中(String.intern())
- 节省内存空间:常量池中如果有对应的字符串,那么则返回该对象的引用,从而不必再次创建一个新对象。