Java虚拟机中的内存区域划分——运行时数据区域解析

前言: Java虚拟机提供的自动内存管理与垃圾回收机制,使得Java与C++之间隔着一座难以逾越的界线,也正是因为Java虚拟机为Java程序员提供了内存管理上的便利,导致了一旦发生内存泄漏和内存溢出问题,不熟悉Java虚拟机运行原理的Java程序员将会对发生的问题一筹莫展;所以,对Java虚拟机的运行原理进行学习理解是成为一名合格的Java程序员的必修课!

深入理解Java虚拟机的前提是熟悉Java虚拟机所管理的内存区域结构;在执行Java程序的过程中,Java虚拟机将其所管理的内存区域划分为以下五个运行时数据区域:程序计数器、虚拟机栈、本地方法栈、堆以及方法区;从数据区域是否由Java程序中所有线程所共享的角度,这五类运行时数据区域又分为线程私有数据区域和线程共享数据区域;其中,程序计数器、虚拟机栈以及本地方法栈是线程私有的运行时数据区域,堆和方法区是线程共享的运行时数据区域;

了解了Java虚拟机管理的内存区域结构之后,就需要对各个运行时数据区域进行深入的了解,从而为后续的Java虚拟机提供的其他功能奠定理论基础;

下面按照先对线程私有的运行时数据区域进行分析,后对线程共享的运行时数据区域进行分析,对每一块数据区域的分析将从该区域的作用、存放内容以及可能出现的异常等方面展开;

程序计数器:

    程序计数器是一块占用内存较小的数据区域,其作用是作为当前线程所执行的字节码的行号指示器;在Java虚拟机的概念模型中,字节码解释器就是通过程序计数器中的值来选取下一条待执行的字节码指令;Java程序中的分支、循环、跳转、线程恢复以及异常处理等都是通过程序计数器的帮助来完成的;
    程序计数器之所以是线程私有的数据区域,原因在于Java中的多线程是通过不同线程的轮流切换并分配处理器执行时间来实现的,所以为了线程恢复后,程序能够找到正确的执行位置,不同线程必须维护自己的程序计数器,用以线程恢复后能够接着之前的执行位置继续执行指令;因此,不同线程具有不同的程序计数器,彼此独立,互不影响;
    程序计数器中存放的内容视当前线程执行的方法是Java方法还是native方法而定;如果当前线程执行的是Java方法,那么该线程的程序计数器中记录的是该线程正在执行的字节码指令的地址;如果当前线程执行的是native方法,那么该程序计数器中的值为空(undefined);
    程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域;

Java虚拟机栈

    Java虚拟机栈是为虚拟机执行Java方法服务的(因此也可以称其为Java方法栈),其描述了Java虚拟机执行Java方法的动态内存模型;
    Java虚拟机栈中存放的是一种称为栈帧的数据,每一个Java方法从开始执行到执行结束对应着一个栈帧在Java虚拟机栈中的入栈和出栈;每一个线程拥有其专属的虚拟机栈,该线程执行属于它的Java方法时,就会将该方法对应的栈帧入栈以及出栈;栈帧中存放了该方法的局部变量表、操作栈、动态链接以及方法出口等信息;
    栈帧中最常被程序员关系的就是局部变量表,局部变量表中存放了编译期可知的各种基本数据类型(char、boolean、byte、short、int、long、float、double)、对象引用(reference)以及returnAddress类型(指向了一条字节码指令的地址)的变量;由于栈帧中存放的变量都是编译期可知的,因此,局部变量表占用的内存大小在编译期就是确定的并且完成分配,所以当进入一个Java方法时,局部变量表的大小就已经确定并在运行期间不会改变;
    Java虚拟机规范规定了Java虚拟机栈对应的内存区域可能会出现StackOverflowError异常和OutOfMemoryError异常;其中,当线程所申请的栈深度超出了虚拟机规定的范围,将会抛出StackOverflowError异常;当虚拟机栈可以动态扩展时,无法申请到足够的内存,将会抛出OutOfMemoryError异常;

本地方法栈

    本地方法栈和Java虚拟机栈的作用相似,只不过本地方法栈是为当前线程执行native方法所服务的,而Java虚拟机栈是为当前线程执行Java方法服务的;本地方法栈中存放的也是方法对应的栈帧,本地方法栈和Java虚拟机栈一样,也有可能抛出StackOverflowError异常和OutOfMemoryError异常;由于Java虚拟机栈和本地方法栈的相似,有些虚拟机直接将二者合二为一,如Sun的HotSpot虚拟机;

Java堆

    Java堆是Java虚拟机所管理的内存中最大的一块,其作用也是唯一作用就是存放对象实例,几乎所有的对象实例都在Java堆上完成分配;Java虚拟机规范中对这一点的描述是:所有的对象实例和数组都在Java堆上完成分配,但是,随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术等将会导致所有的对象实例和数组在Java堆上分配变得不那么绝对了!
    由于Java堆是Java虚拟机所管理的内存区域中最大的一块,并且几乎所有的Java对象实例都在Java堆上完成分配,因此,该区域也是垃圾收集器所管理的主要区域,从而Java堆又被成为“GC堆”;从这一点来看,Java堆可以从内存回收和内存分配两个角度进行进一步的划分;由于现在的垃圾收集器采用分代收集算法,所以可以将Java堆分为新生代和老年代,如果再细致地进行划分,又可以将新生代划分为Eden空间、From Survivor空间以及To Survivor空间;由于多线程可能会带来线程安全问题,因此为了避免不同线程进行内存分配时可能造成线程安全问题,可以将Java堆划分为一块块线程私有的分配缓冲区(TLAB);
    Java虚拟机规范中规定了Java堆可能出现OutOfMemoryError异常;由于现在的商用虚拟机都是可扩展的(通过参数-Xmx和-Xms控制),所以当无法申请到足够的内存时,将会抛出OutOfMemoryError异常;

方法区

    方法区和Java堆一样都是所有线程所共享的内存区域,方法区用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
    方法区被Java虚拟机描述为Java堆的一个逻辑部分,但是其别名Non-Heap(非堆)说明了该区域与Java堆的区别;
    由于Sun HotSpot虚拟机将垃圾收集器管理的区域扩展至方法区,并采用永久代来实现方法区,因此很多人会将方法区叫做永久代;但是对于其他虚拟机,是不存在永久代这一概念的;由于方法区中的常量池会导致内存溢出的问题,因此将方法区实现为永久代不是一个合理的方案;从JDK1.7开始,已经将原本放在永久代的字符串常量池移出;
    对于方法区的垃圾回收目标主要是常量池的回收以及类型的卸载;虽然这部分的内存回收成绩不怎么令人满意,但是确实有必要的,因为该区域曾出现过若干个由该区域未完全回收导致内存泄漏所带来的严重的BUG;
    Java虚拟机规范中规定了方法区可能出现OutOfMemoryError异常;当无法申请到足够的内存时,将会抛出OutOfMemoryError异常;

方法区之运行时常量池

    运行时常量池是方法区的一部分,Class文件中的常量池用于存放编译器生成的各种字面量和符号引用,常量池中的内容将会在类加载之后进入方法区的运行时常量池中存放;
    运行时常量池相对于Class文件中常量池的另外一个重要特征就是具备动态性,Java语言并不要求常量一定在编译期才能产生,运行期间也可以将新的常量放入池中,这种特性被开发人员利用比较多的是String类的intern()方法;
    运行时常量池是方法区的一部分,因此,也会抛出OutOfMemoryError异常;       

不可忽略的直接内存

    直接内存不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域;但是由于该部分内存也被频繁使用,并且也可能导致OutOfMemoryError异常,因此需要对其进行了解;
    直接内存是与NIO有关的内存区域,JDK1.4中新加入的NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存放在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作;由于这样可以避免在Java堆和Native堆之间来回复制数据,从而在一些场景中可以显著地提高性能;
    直接内存的分配不受Java堆大小的限制,但是会受到本机总内存大小和处理器寻址空间的限制;因此在根据实际内存进行参数-Xmx等的设置时,一定要考虑直接内存,从而使得各个内存区域总和小于物理内存限制,避免动态扩展时出现OutOfMemoryError异常;

猜你喜欢

转载自blog.csdn.net/boker_han/article/details/79371728