【深入理解Java虚拟机】自动内存管理机制——内存区域划分
Java与C++之间有一堵有内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。C/C++程序员既拥有每一个对象的所有权,同时也担负着每一个对象生命从开始到终结的维护责任,而Java最大的优势之一就是将内存控制的权力交给了Java虚拟机。在虚拟机自动内存管理机制的帮助下,程序员几乎不用担心内存泄漏和内存溢出的问题。
然而,这里说的仅仅是几乎不会出现,特殊情况下,一旦出现了类似问题,程序员也需要对虚拟机的自动内存管理机制有所了解,才可能排查出相应的错误。
Java虚拟机内存区域划分
Java虚拟机在执行java程序的过程中,会把它所管理的内存划分为几个不同的数据区域,每个数据区域有各自的用途,有些随虚拟机进程启动而存在,有些随用户线程而存在。这些区域主要包括:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、直接内存。
1. 程序计数器
程序计数器是很小的一个空间,可以看做是当前线程所执行的字节码的行号指示器,也就是用以决定下一条将要执行的字节码指令,比如分支、循环、跳转等语句都需要依赖PC来完成。
由于多线程之间是通过时间片轮转来占用CPU资源,所以每一个线程都有自己独立的程序计数器,各线程独立存储,互不影响,因此,这一小块内存是线程私有的。
需要注意一点:如果线程执行的是java方法,那么PC记录的是正在执行的字节码指令地址;而如果正在执行的是Native方法(本地方法,实现不是由java语言完成的),那么PC值为空。
2. Java虚拟机栈
虚拟机栈就是我们常说的内存中的栈空间,也是线程私有的,生命周期与线程相同,是为字节码(Java方法)服务的,用来支持java方法的执行。
虚拟机栈里存放的元素是栈帧(用以存放局部变量表、操作栈、动态链接、方法返回值、附加信息等),每一个方法从调用直到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。java方法是基于栈的执行引擎,这里的栈指的就是栈帧中的操作栈。
栈帧的结构重点是局部变量表和操作栈,其余都属于栈帧信息。需要注意的是,栈帧中需要多大的局部变量表,多大的操作栈,这些都是在编译时就确定的,存在了方法表的Code属性中,运行时不会改变。
局部变量表用以存放方法的参数和内部定义的局部变量,基本单位是slot(变量槽,一般是32位),64位的long和double会占据两个连续的slot。局部变量表的开头存放的是参数,对于实例方法,第0位索引的slot默认是this参数,后续依次是其他参数。参数表之后是内存定义的其他局部变量。
操作栈是java方法执行所依赖的基本结构,各种字节码指令都是通过向操作栈中写入和提取内容来完成相应的运算或者调用其他的方法。所有的运算过程,都以操作栈的出栈和入栈作为信息交换的途径。
3. 本地方法栈
本地方法栈和虚拟机栈是完全相同的,唯一的差别是本地方法栈是为native方法服务,有的虚拟机将这两部分合二为一,有的分成两部分实现,本质上没有差别。
4. Java堆
堆空间是虚拟机所管理内存中最大的一块,所有的对象实例以及数组都在堆上分配(简单理解就是一般所有new出来的都在堆空间中),由虚拟机启动时创建,所有线程共享这一块区域。
Java堆不一定物理上连续,逻辑上连续即可。更重要的一点是,Java堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”。现在的垃圾收集器一般都采用分代收集算法,也就是为了更好对内存进行回收,进一步将堆空间划分为:新生代和老年代。新生代更进一步可以划分为:Eden空间、From Survivor空间和To Survivor空间。
5. 方法区
方法区又称为“永久代”,类似于堆空间,是所有线程共享的内存区域,用以存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区同样可以不一定在物理上连续,这一块区域可以实现垃圾收集,也可以不实现,或者直接将堆空间的垃圾收集机制扩展到方法区即可。原因是,这块区域的回收目标主要是常量池的回收和对类型的卸载,这个区域很少情况下需要回收,这也是被称为“永久代”的原因。
方法区中很重要的一部分是运行时常量池,用于存放在编译期生成的各种字面量和符号引用,在类加载完成后进入方法区的运行时常量池中存放。其中,字面量很好理解,比如文本字符串、声明为final的常量值等,而符号引用主要指的是以下三类:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
6. 直接内存
直接内存不是虚拟机运行时数据区的一部分,是jdk1.4以后引入的一种基于通道和缓冲区的I/O方式,可以使用native本地函数库直接分配堆外内存。