JVM 内存模型
此文只记录jvm内存模型。
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些数据区域可以分为两个部分:一部分是线程共享的,一部分则是线程私有的。
图中的运行时数据区内的各个模块大小不按实际大小,仅供参考。
从线程维度分类JVM内存模型归类分为:线程私有内存、线程共享内存、以及不在堆内的直接内存。
线程私有型内存
线程私有型内存由三部分构成:程序计数器、Java虚拟机栈、本地方法栈。
一、程序计数器
程序计数器是线程私有的一块很小的内存空间。
因为在多线程中线程是执行是抢占式调度,一个时刻只能执行一条线程里的代码,其他线程都会占时“休眠”。那么线程“睡醒”之后是怎么找回记忆的呢?
这时程序计数器出来了,它记录线程各自正在执行的字节码指令地址,其可以看做是当前线程所执行的字节码的行号指示器。这样线程就有记忆了,就算中途“休眠”,醒来后也可以继续正常执行指令,使程序正常运行。
运行java程序一般记录的都是正在执行的字节码指令的地址,但有个例外就是Native 方法,Native表示本地方法,有可能是C或者汇编语言写的直接向操作系统或硬件发生指令的方法,在jvm的程序计数器里是记录不了的,所以如果正在执行的是 Native 方法,则计数器的值为空。
程序计数器是唯一一个没有规定任何 OutOfMemoryError (内存溢出异常)的区域。
二、Java虚拟机栈
Java虚拟机栈就是平时我们口中所说的栈。
Java虚拟机栈是线程私有的,一条线程有独有的一个,所以声明周期与所属线程的声明周期是相同的。
每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,而且 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。
- 局部变量表:
顾名思义就是用来存储java方法中的局部变量的,局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小,这也是为什么局部变量使用前必须初始化的原因之一。 - 操作数栈:
数据运算的地方,用于在方法运行时可以存放以及获取操作数,其所需要的最大栈深度也是在编译期间定下的,大多数指令都在操作数栈弹栈运算,然后结果压栈。 - 动态连接:
常量池中会存储方法的符号引用,而每个栈帧中都会存储一个引用,用于指向常量池中该方法对应的符号引用,字节码指令中方法的调用就以方法对应的符号引用为参数来进行,在类加载阶段的解析步骤中,部分符号引用会被解析为直接引用,称为静态解析,在方法的运行过程中,另一部分符号引用会被实时的解析为直接引用(比如通过反射获取引用),称为动态连接
在这个区域有两种异常情况:
OutOfMemoryError异常:
内存溢出异常,栈的大小可以是固定的,也可以是动态扩展的,若虚拟机栈可以动态扩展(大多数虚拟机都可以),但扩展时无法申请到足够的内存(比如没有足够的内存为一个新创建的线程分配栈空间时),则抛出 OutofMemoryError 异常。
StackOverflowError异常:
栈溢出异常。jvm规定了栈的最大深度,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误,比如一个没有出口的递归方法进栈,JVM规定了栈可以进800个方法的深度,所以无限递归到801次的时候就会报StackOverflowError异常。
三、本地方法栈
本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 Native 方法服务。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
线程共享型内存
线程共享的数据区 具体包括 Java堆 和 方法区 两个区域
一、Java堆
Java堆就是平时我们口中所说的堆。因为Java堆是垃圾收集的主要区域,所以有时也叫"GC堆"。
Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存。
从内存回收角度划分,Java堆还可以分为新生代和老年代,其中新生代还可以分为Eden空间、From Survivor空间、To Survivor空间。此文只介绍内存模型,其余内容请看GC文章.
Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。而且,Java堆在实现时,既可以是固定大小的,也可以是可拓展的。
如果在堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出 OutOfMemoryError 异常。
Java堆里有一块特殊的区域叫字符串常量池,下文中介绍.
二、方法区
方法区与Java堆一样,也是线程共享的并且不需要连续的内存。有个别名叫做非堆(Non-Heap)。
方法区里存放着类的版本,字段,方法,接口和常量池。常量池里存储着字面量和符号引用。
反射理解加强:
- 方法区里有对类加载器的引用
jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。 - 方法区里有对Class类的引用
jvm为每个加载的类都创建一个java.lang.Class的实例(存储在堆上)。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据(类的元数据)联系起来, 因此,类的元数据里面保存了一个Class对象的引用;
因为方法区里储存的数据大部分是与进程同“生死”的,所以从GC的角度考虑有时候也叫做“永久代”。正因为“永久代”里的数据有着和小强一样的生命力,所以很难进行GC,有时还因生命力太顽强导致了内存泄漏。
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。而内存泄漏又可能导致内存溢出,所以当方法区无法满足内存分配的需求时,将抛出 OutOfMemoryError 异常。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用。
字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等;
符号引用则属于编译原理方面的概念,包括以下三类常量:类和接口的全限定名、字段的名称和描述符 和 方法的名称和描述符。
因为运行时常量池(Runtime Constant Pool)是方法区的一部分,那么当常量池无法再申请到内存时也会抛出 OutOfMemoryError 异常。
运行时常量池相对于Class文件常量池的一个重要特征是具备动态性。Java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如字符串的手动入池Srting类的方法intern()。
直接内存:
不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;
1、如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;
2、 这块内存不受java堆大小限制,但受本机总内存的限制,所以也会发生OutOfMemoryError 异常。
字符串常量池
在介绍Java堆中说到堆中有一块特殊的区域叫做字符串常量池:
在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
在JDK7.0版本,字符串常量池被移到了堆中了。
先看一下以下代码的结果
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = "abc";
System.out.println(s1 == s2); //结果为false
System.out.println(s1.equals(s2)); //结果为true
}
解析:
- main方法被压进栈中。
- 方法的局部字符串变量s1被赋值指向一个对象实例。
- 此时判断字符串常量池中是否有字符串"abc",如果有的话直接引用,没有的话创建一个字符串对象"abc"放入字符串常量池中,并使x0001地址的对象实例指向字符串常量池中的"abc"。
- 方法的局部字符串变量s2被赋值"abc",判断字符串常量池中有字符串"abc",直接指向。
因为堆存放的是对象实例,所以字符串常量池是以对象的形式存储字符串的。
使用new关键字会创建新的对象,所以 String s1 = new String(“abc”); 会创建两个对象。
还有一种情况:
public static void main(String[] args) {
String s1 = "a" + "b" + "c";
String s2 = "abc";
System.out.println(s1 == s2); //true
System.out.println(s1.equals(s2)); //true
}
因为JVM在编译的时候对于字符串类型的数据进行 + 操作时会去掉 + 号,所以 “a”+“b”+"c "等价于 “abc” ,所以String s1 = "a" + "b" + "c";
只创建了一个对象。