Java与C++之间有一堵由内存动态分配和垃圾收集技术围成的高墙,墙外的人想进去,墙里面的人却想出来。
运行时数据区
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。Java虚拟机所管理的内存包括以下几个运行时数据区域:程序计数器、虚拟机栈、本地方法栈、堆、方法区。
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器是唯一一个不会发生内存溢出(OutOfMemoryError)的区域。
虚拟机栈
虚拟机栈是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
虚拟机栈内存溢出有两种情况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出StackOverflowError异常(HotSpot虚拟机的栈容量不可以动态扩展)
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的本地方法(Native)服务。
HotSpot虚拟机把本地方法栈和虚拟机栈合二为一
本地方法栈与虚拟机栈内存溢出的情况一样。
堆
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块,是被所有线程所共享的一块内存区域,是垃圾收集器管理的内存区域,存储着对象的实例。
Java堆可以实现固定大小,也可以是可扩展的,可以通过指定**-Xmx和-Xms**来设定。
如果Java堆中没有内存完成实例分配,并且无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
方法区
方法区(Method Area)是各个线程所共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
如果方法区无法1满足新的内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述,还有一项是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池。
当常量池无法申请到内存时会抛出OutOfMemoryError异常。
直接内存
直接内存不是虚拟机运行时数据区的一部分,但这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常。
在JDK1.4中新加入了NIO(NEW Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
直接内存的分配不受Java堆大小限制,但是,既然是内存会受到本机总内存大小以及处理器寻址空间的限制,如果配置虚拟机参数时,只设置了运行时数据区相关的内存大小,忽略掉直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。
堆、栈内存参数
参数 | 解释 | 举例 |
---|---|---|
-Xmx | 堆最大值,默认物理内存的1/4 | -Xmx2048M :堆最大值设为2g |
-Xms | 堆最小值,默认物理内存的1/16 | -Xms2048M:堆最小者设为2g |
-Xss | 栈容量 | -Xss1M:栈内存设为1M |
JDK1.8
Java堆溢出
Java堆中存放着对象的实例,只要不断的创建对象,并且保证GC Roots到对之间有可达路径(被引用),保证不会被垃圾收集回收,当总容量达到堆内存最大限制后就会产生内存溢出异常。
- -XX:+HeapDumpOnOutOfMemoryError:当发生内存溢出异常时Dump出当前内存堆转存储快照
- -XX:HeapDumpPath=D:\error:内存堆转存储快照文件位置
堆内存溢出代码
/**
* 堆内存溢出 -Xmx10m -Xms10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\error
*/
private static void heap() {
//保证死循环中每个对象都会被引用,不会被GC回收
List<Object> objectList = new ArrayList<>();
for(;;){
objectList.add(new Object());
}
}
#堆内存溢出异常
java.lang.OutOfMemoryError: Java heap space
堆内存溢出解决思路
通过内存映像分析工具对Dump出来的堆转储快照进行分析,区分出来是内存泄漏还是内存溢出导致的堆内存溢出异常。
- 内存泄漏:如果是内存泄漏,通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots关联,导致垃圾收集器无法回收,跟进泄漏对象的类型信息和GC Roots引用链信息找到对象创建位置,和产生内存泄漏代码的位置,修改不正确的引用,释放对象。
- 内存溢出:如果是内存溢出(内存对象必须存活,也就是还有用),则需要检查虚拟机堆内存设置(-Xmx和-Xms)与计算机内存对比是否还有可以上调的空间(调大堆内存)。再从代码上检查是否某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等,尽量减少程序运行时内存消耗。
栈溢出
虚拟机栈和本地方法栈,《Java虚拟机规范》中描述了两种异常:
- 如果线程的请求的栈深度大于虚拟机所允许的最大深度,将跑出StackOverflowError异常。
- 如果虚拟机的栈允许动态扩展,当扩展容量无法申请到足够内存时,将抛出OutofMemoryError异常。
HotSpot虚拟机不支持动态扩展。所以除非在创建线程申请内存时因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时不会导致内存溢出,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
栈内存设置小于操作系统最小值虚拟机会给出提示,不同系统最小值不一样(取决操作系统内存分页大小)
The stack size specified is too small, Specify at least 108k
栈溢出
/**
* -Xss108K
*/
private static void stack() {
//调用方法压栈
while (true){
stack();
}
}