Java内存模型介绍
Java虚拟机所管理的内存可分为5大块,分别为:堆区(Java Heap)、虚拟机栈(Virtual Machine Stacks)、本地方法栈(Native Method Stacks)、方法区(Method Area)、程序计数器(Program Counter Register)。
程序计数器
特点:内存空间小,线程私有。也是JVM管理的五块区域中,唯一一个不会抛出OutOfMemory异常的区域。
作用:
① 字节码解释器通过改变程序计数器的值来依次读取指令,从而实现代码流程的控制,如:顺序执行、选择、异常处理等。
② 在多线程环境下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪个位置。
程序计数器是一块比较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值则为(Undefined)。
Java虚拟机栈
特点:
① 内部结构是栈桢(何为栈桢?栈桢就是用于支持虚拟机进行方法调用和执行的数据结构。),每个方法在执行的时候都会创建一个栈桢,栈桢用于存储局部变量表
、操作数栈
、动态链接
、方法返回地址
等信息。
② 某方法在调用另一个方法时,是通过栈桢中的动态链接在常量池中查询另一个方法的引用,进而完成方法的调用。
③ 虚拟机中的方法入栈的顺序和方法的调用顺序是一致的。
④ 虚拟机栈也是线程私有的。且随着线程的创建而创建,随着线程的死亡而死亡。
⑤ 每一个方法从调用直至执行结束,就对应着一个栈桢从虚拟机栈中入栈到出栈的过程。
虚拟机栈结构如图所示:
其中,局部变量表
:主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用。
操作数栈
:操作数栈也常称为操作栈,操作数栈的每一个元素可以是任意的Java数据类型,包括long、double。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。例如,在做算术运行的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的,举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行iadd指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。
动态链接
:每个栈桢都包含一个指向运行时常量池中该栈桢所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态链接。
方法的返回地址
:当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者,比如方法A中调用了当前方法B,就方法A就称为调用者),这红推出方法的方式称为正常完成出口。另外一种退出方法是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使athrow字节码指令产生的异常只要在本方法中的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种推出的方式称为异常完成出口。当一个方法是以异常完成出口的方式退出时,是不会给他的上层调用者产生任何返回值的。
无论以哪种方式退出,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。
详细的虚拟机栈执行如下:
Java虚拟机栈会出现个两种异常:
① StackOverFlow:栈溢出。若Java虚拟机的内存大小不允许动态扩展,当线程请求栈的深度超过当前Java虚拟机栈的最大深度时,就会抛出StackOverFlow异常。
② OutOfMemory:内存溢出。当Java虚拟机的内存允许栈动态扩展时,且当线程请求栈时,内存用完了,无法再进行动态扩展了,此时就会抛出OutOfMemory异常。
使用-Xss来设置栈的大小,虚拟机栈的大小决定了方法调用的深度。
本地方法栈
本地方法栈和虚拟机栈的区别在于:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
本地方法被执行的时候,在本地方法栈也会创建一个栈桢,用于存放该本地方法的局部变量表,操作数栈,动态链接,方法出口等信息。同时也会出现OOM和StackOverFlow异常。
堆
特点:
① 用于存放Java对象和数组
② 虚拟机中存储空间比较大的区域
③ 可能出现OOM异常
④ 该区域是GC的主要区域,堆区由年轻代、老年代组成。其中年轻代又分为:Eden区、S0区(from survivor)、S1区(to survivor);新生代对应MinorGC(Young GC)、老年代对应Full GC(Old GC)。
从图中可以看出,堆大小 = 新生代+老年代,这里的永久代,我们暂不考虑。
默认的,新生代(Young) 与 老年代(Old)的比例为:1:2。(该值可以通过参数-XX:NewRatio来指定),即新生代占 1/3的堆空间大小,老年代占2/3的堆空间大小。
其中, 新生代
又被分为Eden
和Survivor
区域,Survivor又被细分为S0(from survivor)
、S1(to survivor)
两个区域。
默认的,Eden:from:to
= 8:1:1(可以通过参数-XX:SurvivorRatio来设定),即Eden区域占新生代总区域的8/10,from、to两个区域各占1/10。
JVM每次只会使用Eden和其中一块的Survivor区域来为对象服务,所以无论什么时候,总有一块Survivor区域是空闲着的。因此新生代实际可用的内存空间为9/10。
Java中的堆,也是GC收集垃圾的主要区域。其中GC分为两种:Minor GC
、FullGC
。
Minor GC是发生在新生代中的垃圾收集动作,所采用的是复制算法。
新生代几乎是所有Java对象出生的地方,即Java对象申请的内存以及存放都是在这个地方,Java中的大部分对象通常不需要长久的存活,当一个对象被判定"死亡"时,GC就有责任来回收掉这部分对象的内存空间。所以新生代是GC手机垃圾最频繁的区域。
当对象在Eden出生后(包括一个Surivivor区域,假设是在from区域),在经过一轮GC后,如果对象还存放,并且能够被另外一块Survivor区域(即能被to区域)所容纳,则使用复制算法,将这些还存活的对象复制到to区域中,然后清理所使用过的Eden以及from区域,并将存活下来的对象的年龄设置为1,以后对象在Survivor区域每熬过一次Minor GC,对象的年龄就会+1,当年龄达到某个阈值时,这些对象就会进入老年代。
Full GC是发生在老年代的垃圾收集动作,所采用的的是标记-清除算法,进入老年代的对象,就没有那么容易的 “死掉” 了。
JVM中堆区参数的设置:
-Xms1024m
# 设置堆区的存储空间最大值,一般与堆区的初始大小相等
-Xmx1024m
# 设置年轻代堆的大小
-Xmn512m
# 设置如下参数,在出现OOM时进行堆转储
-XX:+HeapDumpOnOutOfMemoryError
# 设置以上设置时,需配置以下参数,堆转储文件输出的位置
-XX:HeapDumpPath=/usr/log/java_dump.hprof
永久代说明:
jdk1.6之前,常量池分配在永久代。
jdk1.7:有,但已经逐步“去永久代”。
jdk1.8及以后,移除永久代吗,取而代之的是一个叫元空间的区域。
方法区
方法区与Java堆一样,是各个线程共享的区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译期编译后的代码等数据。
运行时常量池是方法区的一部分。在jdk1.7及之后的版本,JVM已经将运行时常量池从方法区中移了出来,在Java堆中(Heap)中开辟了一块区域存放运行时常量池
方法区与永久代
方法区是下城共享的,采用永久代的方式实现了方法区。
JDK8以前,存在永久代,JDK8以后,移除了永久代。如下图:
方法区在不同JDK版本的变化: