深入理解Java虚拟机(自动内存管理机制1)

511 阅读7分钟

这一篇文章我们来学习Java虚拟机运行时数据区域。

1.概述

       对于Java开发同学来讲,在虚拟机自动内存管理机制的帮助下,我们不需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题,但正因如此,一旦出现内存溢出和泄漏的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会成为一项异常艰难的工作。

2.运行时数据区域

       Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和校会的时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图:

    

2.1 程序计数器

       程序计数器是一块较小的内存,它可以看做是当前线程所执行的字节码的行号指示器。字节码解释器在工作时就是通过改变这个计数器的值来获取下一条需要执行的字节码指令,分支、循环、跳转等基础功能都需要依赖这个计数器来完成。

       由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有个独立的程序计数器,各个小城之间计数器互不影响,独立存储。

        如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是Native方法,这个计数器值则为空(undefined)。

2.2 虚拟机栈

       与程序计数器一样,Java虚拟机栈也是线程私有的,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈到出栈的过程。

       局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、long、float、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表队向的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

       在Java虚拟机规范中描述了两种异常情况:
  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,则抛出StackOverflowError异常。
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出StackOverflowError异常。

2.3 本地方法栈

       本地方法栈与虚拟机栈发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native方法服务,与虚拟机栈一样,本地方法栈也会跑出StackOverflowError和OutOfMemoryError异常。

2.4 堆

       Java堆是被所有线程多共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实力都在这里分配内存。

       Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC堆”,从内存回收的角度来看,由于现在垃圾收集器基本都采用分代回收算法,所以Java堆还可以细分为:新生代和老年代,再细致一点的又Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来说,Java堆可能划分出多个线程私有的分配缓冲区。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例。进一步划分的目的是为了更好的回收内存,或者更快的分配内存。

       Java堆的所使用的内存在物理上不需要连续,逻辑上连续即可。Java堆的容量可以是固定的,也可以动态的扩展(通过-Xms 堆的初始化内存 和 -Xmx 堆的最大内存 控制)。

       在Java虚拟机规范中描述了一种异常情况:

  • 如果在堆中没有足够的内存来完成实例分配,并且堆也无法进行扩展时,则会抛出OutOfMemoryError异常。

2.5 方法区

       方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。

        HotSpot虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为HotSpot虚拟机设计团队用永久代来实现方法区而已,这样HotSpot虚拟机的垃圾收集器就可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作,但是这并不是一个好主意,因为这样更容易遇到内存溢出问题(永久代又 -XX:MaxPermSize的上限)。 

       方法区是Java堆的逻辑组成部分,它除了和Java堆一样在物理上不需要连续和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较出现的,但并非数据进入方法区后就“永久存在”了。

       Java虚拟机规范中描述了一种异常情况:

  • 如果方法区的内存空间不满足内存分配需求时,Java虚拟机会抛出OutOfMemoryError异常。

2.6 运行时常量池

       运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件除了有类的版本、接口、字段和方法等描述信息,还包含了常量池,它用来存放编译时期生成的字面量和符号引用,这些内容会在类加载后存放在方法区的运行时常量池中。

       Java虚拟机规范中描述了一种异常情况:
  • 当创建类或接口时,如果构造运行时常量池所需的内存超过了方法区所能提供的最大值,Java虚拟机会抛出OutOfMemoryError异常。

2.7 直接内存

       直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。

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

       本机直接内存的分配不会收到Java堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

参考资料
《深入理解Java虚拟机 第三版》
《Java虚拟机规范(Java SE7版)》