引言

JVM在Java编程中起到了至关重要的作用,他屏蔽了底层细节,这样只要有对应平台的JVM版本,便可以运行自己的Java程序,从而实现了“一处编译,到处运行”的特点。同时JVM的内存管理机制避免了类C语言中的delete/free操作,不容易出现内存泄漏和内存溢出问题。但是并不能因为JVM提供的便利性就可以认为不用深入了解他,恰恰相反,由于JVM这么复杂的特性我们更应该将仔细的研究它。只有这样,才能写出更高效的代码,在出现内存问题的时候不至于毫无头绪。而了解JVM的第一步,就是了解它的内存模型。

 


 

在正式开始Java虚拟机内存区域的讲解的时候,我有两点小建议或者提示:

1.HotSpot是一种Java虚拟机的具体实现,而下边我们讲的大部分都是Java虚拟机规范中的内容,与具体实现略有差别。

2.当遇到不懂的地方,希望大家也不要放弃,我尽量避免没有涉及的内容的出现,但是由于虚拟机本身是个整体,所以也会稍微提及一些没有涉及的内容,但是重在体会概要,不一定要全部弄懂。

 

运行时数据区

 

Java虚拟机运行时数据区包括上图的几个部分,同时又可以根据是否线程共享分为两类,下边我们来逐个介绍这几个区域:

 

程序计数器(线程私有

程序计数器是一块小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。这个概念有点像我们在学习冯诺依曼计算机体系结构中的控制器。字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能的实现都需要依赖这个计数器。

Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,因此,为了在线程切换后程序能够恢复到正确的执行位置,每条线程都需要拥有一个自己的程序计数器,各个程序计数器之间互不影响,独立存储。所以这块区域是每个线程“私有”的内存。

如果线程正在执行Java的方法,计数器记录的就是正在执行的虚拟机字节码的指令地址,如果正在执行的是Native方法,这个计数器的值为空。这块内存区域是唯一一个不存在OutOfMemoryError情况的区域。

 

虚拟机栈(线程私有

虚拟机栈是描述Java方法执行的内存模型:每个方法在执行的时候都会创建一个叫做栈帧的东西(后边还会讲到,其实就是用来存储方法信息的东西),用来存储局部变量表、操作数栈、动态链接、方法出口等消息。和我们平常认识的一样,方法的调用时,就把方法压栈,方法执行完成时,就把方法出栈。从我们现在了解的来看,更准确的说其实是栈帧的压栈和出栈的过程。

在Java虚拟机规范中,这个区域有两种异常的情况:

1.线程请求栈的深度大于虚拟机允许的深度,则抛出StackOveflowError异常

2.如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,则抛出OutOfMemoryError异常

 

本地方法栈(线程私有

本地方法栈与虚拟机栈的作用十分相似,从名字上我们基本上可以比较出差别来,本地方法栈是为Native方法服务的,虚拟机栈是为Java方法服务的。虚拟机规范中没有规定怎么实现它,所以有的虚拟机(Sun 的 HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一了。

本地方法栈同样也会和虚拟机一样抛出StackOverflowError和OutOfMemoryError异常。

 

Java堆(线程公有

Java堆是用来存放对象实例的,Java虚拟机规范对堆的描述是:所有的对象实例都要在这里分配内存。但是随着技术的发展,这句话也变的不是这么“绝对”了。Java堆是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域。

说到这里可能会有一个疑问,方法中也有对象,那么对象到底分配在堆上还是栈上呢。首先栈中存放的是基本数据类型和对象引用,而不是真正的对象,对象引用指向的内存几乎都在堆上(为什么说几乎呢,因为一些常量分配在方法区,所以对象引用也可能指向方法区,但是这里只需要知道栈和堆到底存放的是什么就可以了)。

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的就可以了。

在Java虚拟机规范中,这个区域有一个异常情况:当堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

 

方法区(线程公有

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是平常我们在分析的时候还是习惯分开去说,方法区还有一个别名叫非堆。

很多开发者也把方法区称之为“永久代”,本质上两者并不是等价的,只不过HotSpot虚拟机选择把GC分代手机扩展至方法区,或者是永久代来实现方法区,才产生了这种称呼。

Java虚拟机规范对方法区的限制十分宽松,除了和Java堆一样不需要连续的物理内存和可以选择固定大小或者可扩展之外,还可以选择不进行垃圾收集,因为这个区域的垃圾收集条件比较苛刻,但是并不代表这个区域不需要垃圾回收,Sun公司的bug里表中,就曾出现过若干个严重的bug是由于低版本的HotSpot虚拟机对此区域未完全回收而导致的内存泄漏。

这里我们重点讲一个方法区中的重要概念——运行时常量池。运行时常量池是方法区的一部分,Java文件编译形成的Class文件中除了有类的版本、字段、方法、接口等信息之外,还有一项信息是常量池,用于存储编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

运行时常量池具有动态性,并不一定是编译期产生的常量才能放入常量池,运行期间也可以将新的常量放入常量池中,比如String类的intern()方法。

在Java虚拟机规范中 ,这个区域有一个异常情况:当方法区中无法满足内存分配需要时,将抛出OutOfMemoryError异常。

 

以上就是Java虚拟机管理的5块运行时数据区,我们还有一块内存区域要介绍,这一块虽然不在虚拟机管理范围内,但是也很重要。

直接内存

在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存区域的引用进行操作。这样能够在一些情况下显著提高性能,避免在Java堆和Native堆中来回复制数据。

这块内存不会受到Java堆的大小限制,但是还是受到本机总内存限制,所以也有可能导致OutOfMemoryError异常。

 

到这里Java的内存区域基本上就介绍完成了。

 


 

如有错误,欢迎指摘

10-03 18:28