3.1、内存模型
JVM加载class文件时,将其存放在方法区(JDK8前是永久代,之后为元空间)。当JVM运行后,产生的数据存放在栈、堆中。其中程序计数器类似一个行指针,它表示当前class文件正在执行哪一行的字节码,而每次JVM调用一个方法时,都会在虚拟机栈中创建一个栈帧,通过字节码指令每次将一个变量的值压入操作数栈,然后在局部变量表保存变量名,并将值赋予相应的变量,执行完方法后弹出当前栈帧并舍弃。
-
虚拟机栈
:用来描述java方法执行的内存模型,每执行一个方法就会创建一个对应的栈帧,方法的调用和执行就是一个栈帧的整个入栈到出栈的流程(先进后出,后进先出)。每个栈帧主要包括四部分:- 局部变量表:存放一个方法中的数据类型、对象引用、returnAddress
- 操作数栈
- 动态链接
- 方法出口
-
程序计数器:占用较小的内存空间。它可以看作是一个指针,字节码运行到哪一行就指向哪一行。每一个线程都一个独立的程序计数器,程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。
虚拟机的多线程是通过不断的切换线程,然后分配CPU资源的方式来实现的,而且这种切换的速度非常快,所以为了线程在不断的切换的过程中能准确的恢复到正确的位置,才有了程序计数器的存在。线程本身没有记忆功能,程序计数器就是为了实现这个记忆功能,线程从新获得资源后就从程序计数器所在位置继续执行。
-
本地方法栈:本地方法,带了 native 关键字
-
堆
:存放的是对象的实例。是java虚拟机所管理的内存中最大的一块,通常采用分代的方法划分空间。GC主要处理的地方- 新生代:一个Eden区,两个Servivor区(To、From)。内存大小为 8:1:1
- 老年代
-
方法区:存放了类信息、常量、静态变量、编译器编译后的代码等数据。
jdk8以前是永久代实现方法区,存放在虚拟机中,有默认大小,容易出现内存溢出。
jdk8后使用元空间,在本地的内存中存放,大小和计算机相关。
3.2、虚拟机栈
3.2.1、运行原理
每一个Java线程在创建时都会创建一个虚拟机栈,是线程私有的,内部包含了一个个的栈帧,每个栈帧对应了一个方法的调用。虚拟机栈是为了描述程序中方法的执行,方法开始调用为进栈,方法结束为出栈。
虚拟机栈的栈帧遵循“先进后出” / "后进先出”原则。在一个虚拟机栈中只能有一个正在活动的栈帧,为当前栈帧,对应的是此线程正在执行的方法,而不同线程的虚拟机栈的栈帧是不能互相引用的,当前栈帧位于栈的顶端,如果内部有方法的调用,在执行时,会压入新的栈帧位于栈顶,新压入的栈帧成为当前栈帧,当此方法执行完后,通过方法出口返回上一个栈帧并返回一个执行结果,而此栈帧会被弹出栈废弃,使上一个栈帧成为当前栈帧。(如果方法产生异常一样会使此栈帧弹出栈)。
3.2.2、栈帧模型
每个栈帧包含:
- 局部变量表:是一个数组,主要储存方法参数和定义在方法体内的局部变量(基本数据类型、引用数据类型、returnAddress返回的地址),局部变量表的大小在方法编译的时候就确定了,保存在code属性中。局部变量表中的变量只在当前方法调用中有效,随栈帧的销毁而销毁。(其中64位的long和double会占2个槽)
方法内的变量会按顺序存放在局部变量表中,序号即为数组的索引值。
-
操作数栈:遵循后进先出的栈规则,它是根据字节码指令,进行写入或读取数据的操作。方法中的计算过程在此完成,它保存计算结果,同时也为计算中变量提供临时的存储空间,最后将结果保存到局部变量表中。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为
max-stack
(操作栈的最大深度)。
-
动态链接:Java在加载.class字节码时,会为变量和方法生成一个字符组成的符号引用,保存在常量池中。描述一个方法引用另一个方法就是通过指向这个符号引用(我们代码中A方法调用了B方法,就是A方法指向B的符号引用),主要是方便指令进行识别(符号引用就像id一样,根据符号引用就能找到相应的方法实体)。每一个栈帧内部都包含一个指向常量池中该栈帧所属方法的引用,目的就是能实现动态链接。
①静态链接:当一个字节码的文件加载到JVM时,被调用的方法在编译期可知且运行期不变,这种情况下类加载的解析阶段就会将符号引用转换成直接引用,这就是静态链接。(主要是静态方法、私有方法。)
②动态链接:当一个被调用的方法在编译期无法被确定,只能在执行期间将符号引用转换成直接引用,这种引用是动态的,所以称为动态链接。
虚拟机栈通过字节码的方法调用指令就是以方法对应的符号引用为参数进行的,在类加载解析的步骤中符号引用被解析成直接引用,为静态解析。在运行期间,符号引用实时的解析为直接引用的为动态链接 -
方法返回地址:方法有两种结束方式为异常和执行结束,无论那种方法都要回到调用的位置。本质就是当前栈帧弹出,将下一个栈帧变成当前栈帧,然后执行相应的字节码指令
-
其他的附加信息
可以在在IDEA中导入插件查看栈帧、字节码指令等信息
3.3、堆模型
堆是JVM管理的内存最大的一块,为线程共享区域,它主要存放对象的实例。同时堆也是垃圾回收主要处理的区域。
在G1回收器之前将堆内存中分为老年代和新生代,其中新生代有1个Eden和2个Survivor区。
- 新生代:
- Eden:存放Java新生成的对象
- From:存放GC后还存活的,年龄小于阈值的对象
- To:每次GC时暂时存放存活对象,GC后将对象复制到From区
- 老年代:存放生命周期长的对象
- 一个对象在from和to区域连续15次还在存活状态,则转为老年代
- 对象的大小大于eden的1/2
而在G1回收器后把堆内存等分成多个Region,每个Region都可以作为Eden、Survivor和olde。
3.3.1、新生代
新生代保存的数据分为1个Eden和2个Survivor(From、To)。比例为8:1:1。
新创建的对象首先分配到Eden区 (对象大小超过1/2的直接为老年代),这些对象在经历过一次Minor GC后,会把存活的对象放入Survivor 区,同时年龄+1。在Survivor区的对象都是至少经历过一次GC的,内部的对象每经过一次Minor GC后,存活年龄都会+1,当年龄>15时 (默认的),就会转入老年代。
新生代中对象采取复制算法
,GC开始时To是空的,Eden保存新生对象,From保存经过GC存活但年龄在阈值内的对象。
- 新生对象存放在Eden区,当Eden区满时,触发Minor GC
- GC开始判断Eden中对象是否存活,Eden区大部分的对象都会被回收,少部分存活对象被复制到To区,同时年龄+1。
- GC开始判断From区,如果对象存活则年龄+1,超过年龄阈值的对象被转入老年代,不超过的复制进入To区。
- 复制完毕后,将本次GC的To区作为下次GC的From区,本次GC的From作为下次GC的To区(两个区互换角色)。保证To区在GC开始时是空的。
3.3.2、老年代
老年代存放的是在Java中生命周期较长的对象,比较稳定,不会频繁的执行Major GC的操作。在新生代的对象转入老年代后出现空间不足的时候才会触发GC,新创建的大对象空间不足时也会触发。使用的算法是:标记整理算法
。
首先会扫描一次老年代对象,先标记出存活的对象后,将这些存活的对象移到内存的一段,然后再回收边界外的对象,如果内存还不足则会抛出OOM异常。
晋升老年代的对象
- 当新生代中的对象达到阈值后,默认为15,晋升老年代
- 大对象,当一个对象需要分配大量的连续内存时,会直接将其放入老年代
- 判断Survivor区存在相等对象年龄的对象超过Survivor from/to的一半时,直接将超过的对象放入老年代
常见的参数
-Xms:堆容量初始大小
-Xmx:堆的最大容量(Xms一般和Xmx设置为相同值)
-Xmm:新生代容量
-XX:SurvivorRatio=8 为默认值比例大小 8:1:1,设置Eden、To、From大小