JVM学习第二弹——JVM的内存区域
JVM内存区域
1、程序计数器(Program Counter Register):
-
定义:Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。一个线程对应一个 JVM Stack(Java虚拟机栈)。JVM Stack 中包含一组 Stack Frame(栈帧)。当 JVM 调用一个 Java 方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入 JVM 栈中。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
作用:记录程序下一条要执行的指令的地址,由执行引擎读取下一条指令;
特点:空间非常小,基本上不会发生OOM异常;垃圾回收线程也不会对其进行垃圾回收;也是运行速度最快的内存区域;生命周期与线程一致;
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined),因为程序计数器不负责本地方法栈。
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令
2、Java虚拟机栈:
-
每个方法都会对应产生一个栈帧,保存方法的变量、而且是线程私有的,栈帧的回收伴随着方法调用的返回;
-
结构
-
内存中的栈与堆:
栈是运行时的单位,而堆是存放对象的空间;
栈负责解决程序运行时的数据处理;而堆是负责存储程序运行时的数据(怎么放、放在哪里);
栈存放的是基本数据类型的局部变量,以及引用类型数据的对象引用;
生命周期与线程一致;访问速度仅次于程序计数器;
作用:主管java程序的运行,它保存方法的局部变量、8种基本数据类型、对象的引用地址、部分结果,并参与方法的调用和返回。
局部变量:相较于成员变量(成员变量或称属性)
基本数据变量:8种基本数据类型
引用类型变量:类,数组,接口
JVM对栈的操作只有两个:入栈和出栈;因此栈也不考虑垃圾回收,因为操作太复杂;但是会存在OOM异常,以及常见的*Error异常;
如果采用固定大小的Java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个 *Error异常
如果java虚拟机栈可以动态拓展,并且在尝试拓展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个 OutOfMemoryError异常
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Frame)
执行引擎运行的所有字节码指令全都是对当前栈帧操作的;
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
-
栈帧结构:
-
局部变量表:
1.局部变量表也被称之为局部变量数组或本地变量表
2.定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddressleixing
3.由于局部变量表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题
4.局部变量表所需的容量大小是在编译期确定下来的**,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
5.方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间。
6.局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
-
变量槽slot的理解:
1.参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
2.局部变量表,最基本的存储单元是Slot(变量槽)
3.局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
4.在局部变量表里,**32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。**byte、short、char、float在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true;long和double则占据两个slot
5.JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
6.当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照声明顺序被复制到局部变量表中的每一个slot上
7.如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或者double类型变量)
8.如果当前帧是由构造方法或者实例方法创建的(意思是当前帧所对应的方法是构造器方法或者是普通的实例方法),那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序排列。
9.静态方法中不能引用this,是因为静态方法所对应的栈帧当中的局部变量表中不存在this
10. 变量槽是可以复用的, 当代码执行越多某个局部变量的作用域时,新产生的局部变量会利用前面已经利用过但是数据已经失效的槽位;
-
静态变量与局部变量的对别:
变量的类型:(1)基本类型;(2)引用类型;
按照声明位置区分:
(1)成员变量:
- 类变量:被static修饰,类加载链接准备阶段对类变量赋上默认值值(0) —> 初始化阶段给类变量显式赋值,即按照代码赋值;
- 实例变量:随着对象的创建,会在对空间分配实例变量空间,并进行默认赋值;
(2)局部变量:在调用之前必须要显式赋值,否则不会通过编译;
-
补充:
在栈帧中,与性能调优最密切的就是局部变量表,在方法执行的时候,虚拟机利用局部变量表完成方法的传递;
局部变量表的变量也是GC收集线程的回收节点,只要被局部变量表直接或间接引用的对象都不会被回收;
-
操作数栈:
在方法执行的过程中,往栈中写入或者读取数据,即入栈或者是出栈;
主要用于保存计算过程中的中间结果,同时作为计算过程中变量的临时存储空间;
操作数栈是执行引擎一个工作区,在一个方法执行之初就会被创建,同时这个方法的操作数栈是空的;
每个操作数栈都会有一个明确的栈深度用于存储数值,该大小在编译期就已经确定,保存在方法的code属性中,为max_stack值;
栈中元素可以是任意java类型,32bit的占用一个栈深度;64位的占用两个栈深度;
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中, 并更新PC寄存器中下一条需要执行的字节码指令。
Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
3、本地方方法栈;
- 与Java虚拟机栈一样存在*Error和OOM;
4、堆(Java heap):
-
概念:是Java虚拟机管理的最大一块内存区域;
-
特点:该区域被所有线程所共享;
-
作用:存放对象实例;“几乎所有的对象实例以及数据都应该在堆上分配”
-
按照垃圾分代回收理论,将堆划分为以下区域:
-
新生代:该区域由于垃圾回收策略,又会被划分为三个区域,默认的大小划分为 8:1:1
- Eden区
- 大多数对象所处区域都是位于Eden区,其中的对象不可达之后就会被回收;
- 当有新的对象请求时,如果请求对象的空间大于当前空闲空间时会触发Minor GC;
- TLAB(Thread Local Allocation Buffer):
- 这是在共享堆的基础上,又给每个线程分配一个线程私有的内存空间,如果某个变量没有发生逃逸或者存在线程安全问题,就会被优先分配在这里,这样可以提高对象分配效率同时避免加锁;
- Survivor0(from)
- Survivor1(to)
- 上述两个区域有个特点:复制之后有交换,谁空谁是to;
- 在from区域中的对象如果年龄已经到达设置的阈值(一般为15),如果仍然被引用,就会晋升到老年代中;
- 上述两区域存在GC行为,但是是被动的,即是满了也不会主动触发Minor GC;
- Eden区
-
老年代:
- 当一个超大对象在什么区域都无法安放的时候,就会抛出OOM;
- 当老年代内存空间不足的时候,就会触发FULL GC / Major GC,这两个概念都是混淆的,但是需要清楚是不是发生在老年代;
-
永久代 / 元空间
-
在JDK7之前叫永久代;JDK8之后,被元空间取代;
- 元空间的特点:在逻辑上是属于堆的一部分,但是实际上是一个独立的块,使用本地内存空间;
-
-
参数Xms 与 Xmx可以扩大堆的内存空间;
5、方法区:
-
方法区也是Java虚拟机中线程共享的一块内存区域;
-
逻辑上看成是堆的一部分,但是它是独立的一个区域,别名——“Non-Heap”
-
在JDK7以前,方法区的是实现是由“永久代”来完成的,内存区域在JVM内存中;在JDK7以后,开始慢慢移除永久代,到了JDK8,全新的元空间上线,永久代被废除;
-
用途:存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等;
-
运行时常量池(需要与字节码中包含的“常量池(Constant Pool)”做区别);
- 先点一下常量池:
- 一个有效的字节码文件除了包含类的版本信息、字段、方法以及接口等描述信息外,还有一个常量池(Constant Pool Table)包括各种字面量和对类型、域和方法的符号引用。
- 存放在常量池中的各种编译期生成的字面量与符号引用,这些内容会在类加载之后存放到方法区的运行时常量池中。
- 符号引用被翻译出来的直接引用也是存储在运行时常量池中;
- 运行时常量池相较于常量池的一大特征就是**:具有动态性**;
- 运行时常量池位于方法区中,受到方法区内存空间大小的影响,如果无法申请到内存将会报出OOM;
- 先点一下常量池:
-
为什么要用元空间替代永久代?
版本 | 变化 |
---|---|
JDK6及以前 | 有永久代,静态变量存放在永久代上 |
JDK7 | 有永久代,但是已经在逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中 |
JDK8及之后 | 废除永久代,类型信息、字段、方法、常量保存在本地内存的元空间中,但是字符串常量池、静态变量依然在堆中 |
- 在永久代中,基本上不会发生什么GC行为,因此,如果一个项目中引入了大量的类型信息、以及频繁的字符串生成,如果得不到及时的回收就会导致程序非常容易出现内存溢出的问题;
- 而在元空间中,由于使用的是本地内存,瓶颈比较高,不容易出现内存溢出的问题,而将字符串常量池移到堆中也是方便对无用的内存进行及时的回收。
6、内存分配策略:
- 优先分配在Eden区;
- 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态年龄判断
- 如果survivor区中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
- 空间分配担保
- -XX:HandlePromotionFailure
7、内存优化(得益于逃逸分析技术的日渐成熟):
-
栈上分配: 将堆分配转化为栈分配;如果一个对象哎子程序中被分配,要使得指向该对象的指针永远不会逃逸,对象可能是栈上分配的候选,而不是堆分配;
- 目前栈上分配的技术并未成熟,比较常见的优化实现本质都是以下两种居多,特别是标量替换;
- 同步省略: 如果一个对象被发现只能从一个线程中被访问到,对于这个对象的操作可以不用考虑同步;
-
分离对象或者标量替换: 有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存中,而是存储在CPU寄存器中。
- 标量:在Java中无法再继续拆分成更小单元的数据;
- 对象:Java中对象都属于聚合量