第二章
Java内存区域与内存溢出异常
一、概述
对与Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每个new操作去写delete/free代码,不容易出现内存泄露和内存溢出问
题,由虚拟机管理这一切看起来很美好。但是一旦出现内存泄露和内存溢出问题,如果不了解虚拟机是怎么使用内存的,那么排查错误将会成为
一项异常艰难的工作。
二、运行时数据区域
JVM所管理的内存将会包括以下几个运行时数据区域
-
程序计数器
定义:
程序计数器是一块较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器。
进一步了解:
1在虚拟机概念模型里(仅是概念模型,各种虚拟机可能会通过更高效的方式实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个功能要依赖这个计数器来完成。
2.多线程执行时,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为”线程私有”的内存。
3.如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空。
溢出:
此内存区域是Java虚拟机规范中唯一没有规定任何OutMemoryError情况的区域
-
Java虚拟机栈
定义:
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
额外的:
1.每个方法调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中。线程私有的,它的生命周期与线程相同。
2.经常有人把Java内存分为堆内存 和 栈内存,这种分法比较粗糙,Java内存区域的划分实际上远比这复杂 。这种划分方式的流行只能说明程序员最关注的、与对象内存分配关系最密切区域是这两块。其中所指的堆即是后面将写的的Java堆,而所指的”栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。
进一步理解:
而局部变量表存放了编译期可知(方法运行期间不会改变局部变量表的大小)的各种基本数据类型、对象引用(后面讲对象的访问定位会具体说) 和 returnAddress类型(指向了一条字节码指令的地址)。
溢出:
- 线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常
- 如果虚拟机栈可以动态扩展(大部分VM都可以),扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
(3)本地方法栈
定义/区别:
本地方法栈与虚拟机栈所发挥的作用非常相似,它们间的区别不过是虚拟机栈执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
额外的:
VM规范没有没有强制规定,因此虚拟机可以自由实现它,甚至有的虚拟机(如 HotSpot VM)直接就把本地方法栈和虚拟机栈合二为一
溢出:
同Java虚拟机栈。
(4)Java堆
定义:
Java虚拟机管理的内存最大的一块,是被所有线程共享的,在虚拟机启动时创建。唯一的目的就是存放对象实例。
额外的:
几乎所有对象实例都在堆上分配内存,但随着JIT的发展与逃逸分析技术逐渐成熟,渐渐所有对象都分配堆上变得不那么”绝对”了。
进一步:
Java堆是垃圾收集器管理的主要区域,因此很多时候也称”GC堆”。从内存回收角度看,由于现在收集器基本都采用分代收集算法(分代算法把 jdk1.7及以前分代 分为 新生代、老年代、永久代,jdk1.8开始把 永久代 移除),而刚好就是Java堆被分为 新生代 和 老年代,再细致一点有Eden空间、From Survivor空间、To Survior空间等。而从内存分配角度,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。
溢出:
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
(5)方法区
定义:
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
额外的:
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但它却有一个别名叫 None-Heap(非堆),目的应该是与Java堆区分开来。
进一步:
对于习惯在HotSpot虚拟机上开发、部署的开发者来说,很多人愿意把方法区称为”永久代”,仅仅是因为HotSpot使用永久代实现了方法区,因为hotspot的垃圾收集器采用GC分代算法,并把分代范围扩展到了方法区(给方法区分为”永久代”),可以节省单独写方法区的内存管理代码。
然而,现在看来不是好主意,这样会更容易遇到内存溢出,因此jdk1.7中已经把”永久代”中的字符串常量池移出了,然后在jdk1.8中甚至直接移除了”永久代”,改用元空间实现方法区且元空间不在虚拟机了而是使用本地内存。
回收:
这区域内存回收目标主要是针对常量池的回收和对类型的卸载。
溢出:
如果无法满足内存分配需求时,将会抛出OutOfMemoryError异常。
三、运行时数据区域附带的讲解
(1)运行时常量池
定义/位置:
运行时常量池 是 方法区的一部分。
额外的:
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
进一步:
运行时常量池相对于Class文件常量池的区别:
- Class文件常量池得符合Java虚拟机规范(Class文件每一部分都严格要求,当然也包括常量池)。运行时常量池则不作要求,由具体VM实现。
- 运行时常量池具备动态性。Java语言不要求常量一定只有编译期产生(也就是并非预置入Class文件中常量池的内容才能进入运行时常量池),运行期间也可以将常量加入运行时常量池。比如String的intern()方法
溢出:
运行时常量池 是 方法区的一部分。同样,如果无法满足内存分配需求时,将会抛出OutOfMemoryError异常。
(2)直接内存(堆外内存)
介绍:
直接内存 并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是也经常使用,也会发生OutOfMemoryError,所有一起在这讲。
进一步:
Jdk1.4中加入了NIO,引入了一种基于通道(channle)与缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提升性能,因为避免了在Java堆和Native堆中来回复制数据。
溢出:
还是受到本级总内存大小以及处理器寻址空间的限制。当各个内存区域总和大于物理内存限制从而导致动态扩展时出现OutOfMemoryError异常。
四、HotSpot VM在Java堆中的对象分配、布局和访问
(1)对象的创建(限于普通Java类,不包括数组和Class对象)
首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号所代表的类是否已经被加载、解析、和初始化过。如果没有,那必须先执行相应的类加载过程(《深入理解Java虚拟机》第七章将会讨论这个过程)
类加载检查通过后,接下来虚拟机将会为新生对象分配内存。对象所需的内存的大小在类加载之后就可完全确定(如何确定在《深入理解Java虚拟机》2.3.2节介绍)。