本文是基于周志明的《深入理解Java虚拟机》
对从事C和C++的程序员来说,在内存管理方面,他们既是拥有最高权利的人,也是从事最基础工作的“劳动人民”。
而对于Java程序员来说,JVM自动进行内存管理,程序员不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄露和内存溢出问题。
但是,正因为JVM帮我们管理了内存,一旦出现内存泄露或溢出问题,如果不了解虚拟机是怎么管理内存的,那么排查错误会成为一项异常艰难的工作
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分若干个不同的数据区域。这个区域都各自的用途,以及创建的销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立销毁。Java虚拟机所管理的内存将会包括以下几个运行时数据区域。
其中程序计数器、虚拟机栈、本地方法栈3个区域会随线程线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。因此这几个 区域的内存分配和回收都具备确定性,在这几个区域就不需要过多考虑回收的问题,因为方法结束或者线程时,内存自然就跟随着回收了。
1、程序计数器(Program Counter Register)
或者叫:程序计数寄存器、PC寄存器,学过计算机组成原理应该就懂。
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
注意:这块内存是JVM规范中唯一没有规定任何OutOfMemoryError的区域
2、虚拟机栈(Virtual Machine Stacks)
与程序计数器一样,也是线程私有的,其生命周期和线程一样,每个Java线程有一个虚拟机栈。平常我们讲的“栈内存”就是虚拟机栈,或者说是虚拟机栈中局部变量表部分。
2.1.作用
虚拟机栈描述的是Java方法执行的内存模型,即:每个方法在执行的时候都会创建一个栈帧(Stack Frame),栈帧中存储:
1).局部变量表
存放了编译期就可知的:各种基本数据类型(8个基本数据类型)、对象引用(reference类型)、returnAddress类型(指向一条字节码指令地址)
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存大小在编译期就完成了分配,也就是说当进入一个方法时,此方法需要在栈帧中分配多大的局部变量表空间时完全确定的,运行期不会改变
2).操作数栈
3).动态链接
4).方法出口等
方法从调用到执行完成的过程,就对应了,一个栈帧在虚拟机栈中的入栈和出栈的过程
2.2.异常状况
有两种异常:
1).如果线程请求的栈深度大于JVM所允许的深度,将抛出*Error异常
2).如果栈扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常
3、本地方法栈(Native Method Stack)
3.1.作用
作用和虚拟机栈非常相似,区别:
虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务
JVM规范没有强制规定本地方法栈中的方法使用的语言、使用方式、数据结构,所以具体JVM不同实现。
有的虚拟机如HotSpot虚拟机直接把虚拟机栈和本地方法栈合二为一了
3.2.异常状况
有两种异常:同虚拟机栈
4、Java堆(Java Heap)
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只在逻辑上是连续的即可。对于大多数应用来说,Java堆是虚拟机管理的内存中最大的一块。是被所有线程共享的一块区域,在虚拟机启动时创建,通过参数“-Xmx和-Xms”控制。
4.1.作用
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存(当然也有例外)
Java堆是垃圾回收器管理的主要区域,因此很多时候也被称做“GC堆”。
4.2.分类
下面是一些具体的细分,但是不论如何分类,其存储的仍然是对象实例,进一步划分的目的是为了更好的回收、更快的分配内存。
1).从内存回收的角度看
由于现代GC基本都采用分带收集算法,所以Java堆还可以细分为:
①.新生代
②.老年代
再细分一下还可分为:
①.Eden空间
②.From Survivor空间
③.To Survivor空间
2).从内存分配角度看
线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
4.3.异常状况
如果堆中内存不够继续进行实例分配,且堆也无法再扩展时,将会抛出OutOfMemoryError
5、方法区(Method Area)
方法区和Java堆一样,是各个线程共享的内存区域。
5.1.作用
用于存储已被虚拟机加载的
1).类信息(class metadata)
2).常量(包括interned Strings)
3).静态变量(类变量 class static variables)
4).即时编译器编译后的代码等
虽然JVM规范把方法区描述为堆的一个逻辑部分,但是它确有一个别名叫Non-Heap,目的应该是与Java堆区分开来
5.2.永久代(HotSpot特有,但现已经被移除)
对于使用HotSpot VM的程序员来说,很多人把方法区称之为“永久代(Permanent Generation)”
为什么叫永久代?按内存回收的角度,有新生代、老年代,所以就有了这里的“永久代”。另外,其它虚拟机是没有永久代这个概念的
方法区与永久代本质上两者不等价,仅仅是因为HotSpot团队把GC分代收集扩展到了方法区(或者说用永久代这种方式来实现方法区),这样的话GC就可以像管理Java堆一样管理这部分内存,省去了为方法区编写内存管理代码的工作
1).坏处
如何实现方法区JVM规范并没有强制规定,但是现在看来“永久代”并不是一个好主意
①.更容易遇到内存溢出问题
②.因为有参数“-XX:MaxPermSize”的上限限制,其它虚拟机只要不达到进程可用内存上限,例如32系统的2GB,就不会出现内存溢出
③.有极少数方法(如String.intern()),会因此导致在不同的JVM下有不同表现
2).现状
在JDk1.7的HotSpot中,字符串常量池已经被从永久代中移除了
在Java8中,根据JEP122,永久代PermanentGeneration已经被从HotSpot中removed.
这是JDK1.8中JRockit和HotSpot合并的成果
5.3.垃圾回收
JVM规范对方法区限制非常宽松,甚至可以选择不实现垃圾收集
但并不是如其“永久代”的名字一样,方法区的垃圾回收只是比较少出现。回收目标是:类信息的卸载、常量池的回收
5.4.异常状况
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
6、运行时常量池(Run-Time Constant Pool)
运行时常量池其实是方法区的一部分。
class文件中有一项信息是常量池表(constant_pool table),用于存放编译期生成的“字面量”和“符号引用”,这部分内容将在类加载后进入方法区的运行时常量池中(Run-Time Constant Pool)存放
也就是说:每一个class都会根据constant_pool table 来1:1创建一个此class对应的Run-Time Constant Pool
6.1.作用
就是运行时所需要的常量数据的容器
JVM规范对class文件的每一部分(包括constant_pool table)都有严格的规范,但是对于运行时常量池却没有做任何细节要求,不过一般来说,除了class文件中的符号引用外,直接引用也会存储在运行时常量池中
运行时常量池具备动态性,Java语言并没有要求常量一定只能编译期产生,运行期也可以将新常量放入池中。这个特性用的较多的便是String类的intern()方法
6.2.异常状况
既然运行时常量池是方法区的一部分,自然受到方法区限制,当运行时常量池无法再申请到内存时,将抛出OutOfMemoryError异常
-----------------------------------------------------------
7. 直接内存(Direct Memory)
直接内存不是JVM规范中定义的内存区域,也不是运行时数据区的内容(我现在理解为:直接控制的属于物理机的内存,不属于JVM线程使用的内存)
但是,这部分内从也被频繁的使用,且可能导致OutOfMemoryError异常,所以罗列为在这里叙述
7.1.缘由
JDK1.4中加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,这个类可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场合显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
7.2.异常状况
直接内存不会受到Java堆大小限制,但是会受到本机总内存大小,以及处理器寻址空间的限制。JVM管理员在配置JVM参数时,会根据本机实际内存设置(如-Xmx等参数),但是经常忽略了要被使用的这一份“直接内存”。最终使得各个内存总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常