JVM的内存区域
不知不觉都大三了,回头看看以前的Android开发过程,遇到的很多问题都需要深入到源码去解决,这也促使我不断地深入学习Java的相关知识,首先要学习的肯定是JVM。不过JVM的知识体系十分庞杂,JVM的内存区域的划分是学习JVM的基础,所以就从这里开始总结。
运行时数据区域
JVM在执行Java的程序的时候会将内存划分为几个不同的区域:方法区、虚拟机栈、本地方法栈、堆和程序计数器,理解这几个区域后对于Java使用会有很大的帮助,在一定程度上可以减少程序崩溃的概率。
先来一张结构图:
1. 方法区
方法区是各个线程共享的内存区域,它用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,在Java虚拟机规范里,这部分被描述成了堆区的一个逻辑部分,但又有“no-heap”的说法,因此不能把它看成堆。
对于主流的HotSpot虚拟机而言,在JDk1.8以前,方法区还有“永久代”的说法,但并不意味着这个区域内不会被GC管理到,相反,HotSpot只是使用“永久代”来实现方法区,这样GC就可以像管理堆内存一样,管理这部分内存,也就不必单独为这块区域编写内存管理代码。但在JDK1.8中,已经不再使用永久代来实现方法区了,取而代之的是“Metaspace”,也叫元空间。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,当然也可以通过指令手动指定元空间的大小。
在方法区之中,还有一个运行时常量池,里面储存着编译时期生成的各种字面量和符号引用,这部分的内容将在类加载后进入其中。
对于运行时常量池,虚拟机没有做任何细节要求,不同的提供商实现的虚拟机可以按照自己的需求去实现这个内存区域。
值得注意的是,运行时常量池具备动态性,Java语言并不要求常量一定要只有编译期才能产生,在运行期间也可能有新的常量进入常量池中,String类的intern()方法便利用了这一特性。
1. 虚拟机栈
虚拟机栈是线程私有的,它的生命周期与线程相同,它描述的是java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧,用来储存局部变量表、操作数栈、动态链接和方法出口等信息。
一个方法从调用用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
在栈帧中还有一个局部变量表,用于存储编译时期可知的各种基本数据类型和对象引用(reference类型,可能是一个指向对象起始地址的指针,也可能是一个指向代表对象的句柄)。局部变量表的内存空间在编译的时候就已经确定了,在方法运行期间不会改变局部变量表的大小。
其实,我们偶尔遇到的StackOverflowError异常就是是因为调用方法的深度过深,又遇到虚拟机栈在动态扩展时无法申请到足够多的内存。
3. 本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈用于执行java方法,而本地方法栈则用来执行Native方法。
在某些虚拟机中,更是把虚拟机栈和本地方法栈合在了一起使用。
4. 堆
我在最初接触面向对象的概念的时候就被灌输了“栈中存放对象的引用,堆中存放对象的实例”的思想,现在看来对于多数应用来说,堆是JVM所管理的内存最大的区域,也是GC回收垃圾的主要战场,这是被所有线程共享的区域。它的唯一作用就是存放对象的实例,几乎所有对象都会在这里分配内存。
按照JVM分代回收垃圾的思想,堆中可以被分为新生代和老年代,再细致一点的话,堆还可以分为Eden空间、From Survivor空间、To Survivour空间等。并且,从内存分配的角度看,线程共享的堆中可能划分出多个线程私有的分配缓冲区(TLAB)。之所以有这么多的划分方式,都是为了能更好地回收、分配内存。
此外,堆中的物理内存可以不用是连续的,只要逻辑上连续即可,如果在堆中没有内存空间完成对象实例的内存分配,并且又无法扩展时,将会抛出OOM(OutOfMemoryError)异常。
5. 程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码的解释器工作时就是通过改变这个计数器来实现指令的执行,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
并且,每个线程都有一个独立的程序计数器,各条线程之间的程序计数器不会互相影响,独立储存,负责读取各自线程需要执行的字节码指令,这是一块线程私有的区域。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,这个计数器则为空。
值得注意的是,这是唯一一个没有规定任何OutOfMemoryError的情况的区域。