在学习JVM的时候,其中最重要也是最基础的一部分就是了解JVM对内存的划分设计。我们知道Java通过GC来自动管理内存,这样的好处是我们不需要时刻关心内存的回收,但是这不代表我们就一劳永逸,我们仍然需要学习JVM的内存管理,了解JVM的内存区域划分,知道内存溢出的问题所在,能够排错。
在Java程序中,虽然有GC为我们管理对象,回收内存,但是程序实践中仍然会发生内存溢出等问题,这些问题源于JVM对内存的管理。JVM全称Java Virtual Machine,它是运行在操作系统之上的一个虚拟机程序,它的作用是执行Java字节码程序,管理Java程序的内存。JVM管理的内存通常被分为以下几个部分:
- 程序计数器,虚拟机栈,本地方法栈
- 堆,方法区
我习惯这样去分类,程序计数器、虚拟机栈以及本地方法栈是线程隔离的,而堆与方法区是虚拟机中所有java程序共享的。下面就挨个介绍这些内存区域的概念。
一、程序计数器
程序计数器是当前线程所执行的字节码的行号指示器。重点如下:
- 每个线程都有独立的程序计数器
- 执行Java方法时候,程序计数器记录虚拟机字节码指令的地址;执行本地方法时,程序计数器为空
- 程序计数器不会出现内存溢出等问题
字节码解释器在工作时通过改变程序计数器的值来执行字节码指令,完成程序的分支、循环、跳转、异常、线程恢复等功能。
二、虚拟机栈
虚拟机栈首先是一个栈,它描述Java方法执行的内存模型。
每个Java在执行的时候都会创建一个栈帧,用于存储方法执行的一些信息,方法的调用和执行完成对应于虚拟机栈中栈帧的入栈和弹栈过程。
- 局部变量表中存储编译器可知的基本数据类型、对象引用类型、returnAddress类型。局部变量表的内存空间在编译期完成分配,运行期这部分内存空间是完全确定的。
- 在虚拟机栈中可能出现StackOverflowError,即线程请求的栈深度大于虚拟机允许的最大深度,主要是由递归程序引起。
- 在虚拟机栈中也可能出现OutOfMemoryError,即虚拟机栈在扩展时无法申请到足够的内存。
三、本地方法栈
类似虚拟机栈,用于描述本地方法的内存模型。在本地方法栈中可以使用其他语言和数据结构,取决于具体的虚拟机实现。
- 本地方法栈中也可能出现StackOverflowError和OutOfMemoryError
- 在HotSpot虚拟机中,本地方法栈和虚拟机栈两块内存合二为一
四、堆
堆是JVM中用于存放对象实例的内存区域。在虚拟机规范中描述:所有的对象实例以及数组都要在堆上分配。堆内存是所有线程共享的,意味着JVM中的对象实例基本上都在堆上存放。
它也是GC的主要区域,所以根据不同的GC算法,堆内存也会有不同的划分:
- 分代收集算法中,堆内存划分为新生代和老年代
- 复制算法中,新生代往往还会划分为1块Eden空间和2块Survivor空间。
既然是存放对象实例的内存区域,堆也可能会产生内存溢出的问题,当堆无法扩展时会抛出OutOfMemoryError。
五、方法区
方法区也是线程共享的内存区域,它存储JVM加载的数据(类信息、常量、静态变量、JIT编译后的代码等)
- 方法区中存放的信息主要是静态不变的,但是有的虚拟机也会在方法区中实现GC,用来回收无用的常量和类型。
- 另外,方法区在无法满足内存扩展时,也会抛出OutOfMemoryError
- 在方法区中,运行时常量池比较重要,它主要用于存放编译期类文件生成的字面量和符号引用。类文件中的信息包含类的版本,字段,方法,接口以及常量池部分。
六、直接内存
直接内存并不是JVM所控制管理的内存,它属于JVM之外的内存区域,可以通过JVM内存里的对象来引用外部内存。在书中提到了一个例子,NIO中可以通过本地函数库来直接分配外部内存,然后通过堆中的DirectByteBuffer对象来作为该内存的引用进行操作,可以在一些场景中提高性能。
- 因为同属内存,虽然直接内存不归JVM管理,但是仍然收到物理内存的限制,因此在动态扩展时一旦物理内存不够用,也会抛出OutOfMemoryError
总结
- 在Java程序中,内存分为JVM管理的内存和直接内存
- JVM管理的内存区域划分为几个运行时数据区:程序计数器、虚拟机栈、本地方法栈、堆、方法区
- 程序计数器、虚拟机栈和本地方法栈是线程隔离的
- 堆和方法区是线程共享的
- 程序计数器不会发生内存溢出问题;虚拟机栈和本地方法栈会发生栈溢出(StackOverflowError)和内存溢出(OutOfMemoryError)问题;堆和方法区、直接内存会发生内存溢出问题
- 方法区中有一块运行时常量池,类加载后常量池中的内容(字面量,符号引用)等都会被存放在运行时常量池中