前言
阅读《深入理解Java虚拟机》相关笔记.
内存区域
Java运行时内存区域
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
程序计数器
线程级别
可以看作是当前线程所执行的字节码的行号指示器。
《Java虚拟机规范》唯一没有规定任何OutOfMemoryError情况的区域。
虚拟机栈
线程级别
- 局部变量表
- …
线程所请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError。
如果栈容量可以动态扩展,无法申请到足够的内存,抛出OutOfMemoryError。
本地方法栈
线程级别
为虚拟机使用到的本地方法服务,native关键字。native方法主要用于加载文件和动态链接库,由于Java语言无法访问操作系统底层信息(比如:底层硬件设备等),这时候就需要借助C语言来完成了。被native修饰的方法可以被C语言重写。
与虚拟机栈一样在栈深度溢出或栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError。
堆
所有线程共享
Java Heap是虚拟机所管理的内存中最大的一块。唯一目的就是存放对象实例。《Java虚拟机规范》描述:“所有的对象实例以及数组都应当在堆上分配”。但就目前技术发展的态势看,渐渐不是那么绝对了。实现上日后可能出现值类型的支持。
Java堆是垃圾回收器管理的区域(so-called GC Heap)。现代大部分垃圾回收器是基于分代收集理论设计的(新生代,老年代,永久代等)。当然也有不采用分代设计的新垃圾回收器。
从分配内存的角度而言,Java堆可以划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。但这些划分都是为了更快,更好的分配和回收内存。无论哪个区域,存储的都只能是对象的实例。
Java堆既可以是固定大小,也可以是可扩展的。当Java堆中没有内存分配给实例,也没法再扩展时,抛出OutOfMemeoryError。
方法区
所有线程共享
用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。别名“非堆”(Non-Heap)。
JDK8以前,当时HotSpot团队将垃圾回收器的分代设计扩展至方法区,从而省去专门为方法区编写内存管理代码的工作,当时使用永久代实现方法区,导致更容易遇到内存溢出问题(永久代有-XX:MaxPermSize上限以及默认大小)。JDK8完全抛弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)。
垃圾回收行为再方法区确实是比较少实现的,但并非数据进入了方法区就永久存在了。方法区内存回收的主要目标是常量池回收和类型的卸载。类型卸载条件相当苛刻,回收效果很难令人满意,但有时又确实必要。低版本的HotSpot中一些Bug就是对该区域未完全回收导致的内存泄漏。
无法满足内存新的内存分配需求时,抛出OutOfMemoryError。
运行时常量池
Runtime Constant Pool是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池表,用于存储编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区运行时常量池中。
《Java虚拟机规范》并没有做细节要求,但一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。
运行时常量池的动态性,运行期间也可以将新的产量放入池中,利用较多的是String类的intern()方法。
受到方法区内存的限制,当常量池无法申请到内存时,抛出OutOfMemoryError。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》定义的内存区域。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配对外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。