JVM内存模型
1、本地方法栈Native Method Stack
本地方法栈(Native Method Stacks)是为虚拟机使用到的Native方法服务(调用C)。
2、本地接口Native Interface
本地接口的作用是融合不同的编程语言为 Java 所用(使用越来越少)。
3、程序计数器/PC寄存器
PC寄存器就是一个指针,指向我们下一个需要运行的方法;
程序计数器是一块非常小的内存空间,主要是用来对当前线程所执行的字节码的行号指示器;
而且程序计数器是内存区域中唯一一块不存在OutOfMemoryError的区域;
每个线程都有一个程序计数器,是线程私有的。
4、方法区Method Area
方法区,是线程共享的区域;它存储了每一个类的结构信息;
方法区是用来存储:已经被虚拟机加载的类信息、常量、静态变量以及class字节码文件等;
而且方法区还有另一种叫法:【非堆】,也有人给方法区叫做永久代。
5、java虚拟机栈
5.1、虚拟机栈的基本介绍
虚拟机栈描述的是:Java方法执行的内存模型;(说白了就是:虚拟机栈就是用来存储:局部变量、栈操作、动态链表、方法出口等);是线程私有的。
5.2、虚拟机栈的生命周期
栈不存在垃圾回收问题,只要程序执行结束,栈就over释放,生命周期和线程一致,是私有线程区域。
存储8种基本类型的变量+对象的引用+实例方法等。
5.3、虚拟机栈当中存放的数据
栈帧中主要保存3 类数据:
局部变量(Local Variables):输入参数和输出参数以及方法内的变量。八种基本数据类型 + 对象的引用(String,以及自己定义的class类等)
栈操作(Operand Stack):记录出栈、入栈的操作;
栈帧数据(Frame Data):包括类文件、方法等等。
(栈帧:其实就是一个方法,在jvm当中,给每一个方法起了一个名字,叫做栈帧)
5.4、虚拟机栈运行原理
遵循“先进后出”/“后进先出”原则(弹夹模型)。每个方法执行的同时都会创建一个栈帧;
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,
当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,B方法又调用了 C方法,
于是产生栈帧 F3 也被压入栈;
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
public class StackIn {
//程序入口方法
public static void main(String[] args) {
StackIn stackIn = new StackIn();
//调用A 方法,产生了栈帧 F1
stackIn.A();
}
//最后弹出F1
public void A(){
System.out.println("A");
// F1栈帧里面调用B方法,产生栈帧F2
B();
}
//然后弹出F2
public void B(){
//F2栈帧里面调用C方法,产生栈帧F3
System.out.println("B");
C();
}
//栈帧F3 执行完成之后,先弹出F3
public void C(){
System.out.println("C");
}
}
5.5、局部变量复用slot
局部变量表用于存放方法参数和方法内部定义的局部变量。方法表的Code属性: max_locals 数据项指明了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以Slot(Variable Slot:变量槽)为最小单位,其中64位长度的long和double类型的数据占用2个Slot,其余数据类型(boolean、byte、
char、short、int、float、reference、returnAddress)占用一个Slot(一个Slot可以存放32位以内的数据类型)。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0到局部变量表最大Slot数量。32位数据类型的变量,索引n就代表使用第n个Slot,
如果是64位数据类型的变量,则说明会同时使用n和n+1两个Slot。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程,如果是实例方法(非static的方法)那么局部变量表中第0位的Slot
默认是用于传递方法所属实例对象的引用,在方法中可以通过 this 关键字来访问到这个隐含的参数,其余参数则按参数表顺序排列,占用从索引1开
始的局部变量Slot。参数列表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。
为了节省栈帧空间,局部变量表中的Slot是可以复用的,当方法执行位置已经(程序计数器在字节码的值)超过了某个变量,那么这个变量的Slot可以被其他变量复用。
除了能节省栈帧空间,还伴随着可能会影响到系统垃圾收集的行为。
5.6、方法的参数对调用次数的影响
没有参数的方法,调用的次数会更多。
没有参数且没有局部变量的方法调用次数远远比有参数有局部变量的方法调用的次数多得多。
6、java虚拟机堆
6.1、JVM虚拟机堆的基本组成
- JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
- 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
- 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
- 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
6.2、jdk1.8为什么要移除永久代
在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace);
元空间并不在JVM中,而是使用本地内存。
元空间有两个参数:
- MetaspaceSize :初始化元空间大小,控制发生GC阈值;
- MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存;
移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。
有了元空间就不再会出现永久代OOM问题了!
6.3、新生代
新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,
Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。
老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,进行养老区的内存清理,所以对响应要求高的
应用尽量减少发生Major GC(Full GC),避免响应超时。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
Minor GC : 清理年轻代
Major GC : 清理老年代
Full GC : 清理整个堆空间,包括年轻代和永久代;所有GC都会停止应用所有线程。
6.4、如何判断哪些数据需要“回收”
6.4.1、引用计数器计算方法
给对象添加一个引用计数器,每当有一个地方引用它时计数器就+1,当引用失效时计数器就-1。只要计数器等于0的对象就是不可能再被使用的。
优点:实现简单、判断效率高。
缺点:很难解决对象之间循环引用的问题。如下:
Object a = new Object();
Object b = new Object();
a=b;
b=a;
a=b=null; //这样就导致gc无法回收他们。
a引用b,b引用a,且a、b长期不用,但引用计数不为0
6.4.2、可达性分析计算方法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有使用任何引用链时,则说明该对象是不可用的。
上图中gc root与对象展示的联系。所示灰色区域对象是存活的,Object5/6/7均是不可用的对象。不可用对象变为可回收对象至少需要经过2次标记。
在Java语言中,可作为GC Roots 的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中静态变量引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈(即一般说的 Native 方法)中JNI引用的对象;
6.4.3、两次标记
宣告一个对象死亡,至少要经历两次标记。
(1)第一次标记
如果对象进行可达性分析算法之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选。
筛选条件:判断此对象是否有必要执行finalize()方法。
筛选结果:当对象没有覆盖finalize()方法、或者finalize()方法已经被JVM执行过,则判定为可回收对象。如果对象有必要执行finalize()方法,
则被放入F-Queue队列中。稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;
(2)第二次标记
GC对F-Queue队列中的对象进行二次标记。
如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。
(3)finalize() 方法
finalize()是Object类的一个方法、一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;
6.6、垃圾回收算法
(1)标记-清除算法
算法分为”标记“和”清除两个阶段“:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多。
(2)复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
(3)标记-整理算法
标记需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
(4)分代收集算法
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。