JVM 内存结构与内存溢出异常

  • 2017-02-17
  • 642
  • 0

本文为《深入理解Java虚拟机》读书笔记,加入了一些自己的见解。

Jvm内存溢出异常就是我们常说的OOM,即java.lang.OutOfMemoryError,当然还包括java.lang.StackOverflowError。

那么它和内存泄漏有什么区别与联系呢?

对于内存泄漏,维基百科的定义是:在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。

对应于Java虚拟机,一块内存区域如果已经没被使用了,并且之后也不会再使用,但是此时GC无法回收这块内存区域,那么此时就发生了内存泄漏。这里说的是此时,不排除之后内存又能被正常回收。内存泄漏并不会立刻对运行的程序造成影响,但是足够多的内存泄漏便会造成内存溢出。

那什么情况下会造成内存溢出,出现内存溢出该怎么排除,先看一下JVM的内存结构:

包括:

PC Register(program counter register)程序计数器:

主要作用是记录当前线程所执行的字节码的行号,线程私有,不会出现OOM。

JVM Stack 虚拟机栈:

线程私有,存放的是Java方法执行时的数据,既描述的是Java方法执行的内存模型:每个方法开始执行的时候,都会创建一个栈帧(Stack Frame)用于储存局部变量表、栈操作数、动态链接、方法出口等信息。每个方法从调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表存放的是编译器可知的各种基本数据类型(boolean 、byte、int、long、char、short、float、double)、对象引用(reference)和returnAddress类型(它指向了一条字节码指令的地址)。在Java虚拟机规范中,对这部分区域规定了两种异常: 1、当一个线程的栈深度大于虚拟机所允许的深度的时候,将会抛出StackOverflowError异常; 2、如果当创建一个新的线程时无法申请到足够的内存,则会抛出OutOfMemeryError异常。

Native Method Stack 本地方法栈:

与虚拟机栈类似,区别在于为Native方法服务。线程私有,可抛出StackOverflowError,OutOfMemeryError异常。

JVM Heap 堆:

堆(heap)是虚拟机中最大的一块内存区域了,被所有线程共享,在虚拟机启动时创建。它的目的便是存放对象实例。堆是垃圾收集器管理的主要区域,从垃圾回收的角度来讲,现在的收集器包括HotSpot都采用分代收集算法,所以堆又可以分为:新生代(Young)和老年代(Tenured),再细致一点,新生代又可分为Eden、From Survivor空间和To Survivor空间。从内存分配的角度来讲,又可以分为若干个线程私有的分配缓冲区(Thread Local Allocation Buffer ,TLAB)。当堆空间不足切无法扩展,会抛出OutOfMemoryError异常。

JVM Method Area 方法区:

各个线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

它被分为两个主要的子区域
永久代 —— 这个区域会 存储包括类定义,结构,字段,方法(数据及代码)以及常量在内的类相关数据。它可以通过-XX:PermSize及 -XX:MaxPermSize来进行调节。如果它的空间用完了,会导致java.lang.OutOfMemoryError: PermGen space的异常。

代码缓存——这个缓存区域是用来存储编译后的代码。编译后的代码就是本地代码(硬件相关的),它是由JIT(Just In Time)编译器生成的,这个编译器是Oracle HotSpot JVM所特有的。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了类的版本、字段、方法、接口等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后进入方法区存放。运行时常量池相对于Class文件常量池的另外一个重要特征是具有动态性,运行期间也可能有新的常量池放入持重,比如String.intern()方法。运行时常量池属于方法区一部分。会抛出OutOfMemoryError异常。

注意:Java8中已移除永久代查看Java PermGen 去哪里了? ,在JDK7之前的版本,对于HopSpot JVM,interned-strings存储在永久代(又名PermGen),会导致大量的性能问题和OOM错误。从PermGen移除interned strings的更多信息查看这里

Interned strings are currently stored in the permanent generation. A new approach for managing meta-data is being designed and it requires interned strings to live elsewhere in the the heap (young gen and/or old gen).

Direct Memory 直接内存:

直接内存(Direct Memory)不属于虚拟机中定义的内存区域,而是堆外内存。
JDK1.4 中新加入了NIO(new Input/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这快内存的引用进行操作。这样能在一些场景中显著提高新能性能。如果直接内存不足时,会抛出OutOfMemoryError异常。
下面看一下会造成内存溢出的代码:


import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
	
	public static class OOMObject {
	}

	public static void main(String[] args) {
		List objHolder = new ArrayList<>();
		while (true) {
			objHolder.add(new OOMObject());
		}
	}

}

运行结果:


Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.ArrayList.grow(Unknown Source)
	at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
	at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
	at java.util.ArrayList.add(Unknown Source)
	at HeapOOM.main(HeapOOM.java:14)

抛出了OOM,并且提示 Java heap space,堆内存溢出。JVM堆内存大小可以通过参数:-Xms,-Xmx 设置大小。
下面是栈内存溢出,方法递归调用会造成该异常。


public class JvmStackSOF {
	public static class SOFObject {
		public int invokeCount = 0;
		public void method() {
			invokeCount++;
			method();
		}
	}
	public static void main(String[] args) {
		SOFObject obj = new SOFObject();
		try {
			obj.method();
		} catch (Throwable e)  {
			System.out.println("count:" + obj.invokeCount);
			throw e;
		}
	}
}

运行结果


count:11102
Exception in thread "main" java.lang.StackOverflowError
	at JvmStackSOF$SOFObject.method(JvmStackSOF.java:10)
	at JvmStackSOF$SOFObject.method(JvmStackSOF.java:10)
	at JvmStackSOF$SOFObject.method(JvmStackSOF.java:10)

可以看到发生了栈内存溢出:StackOverflowError。栈内存大小可以通过-Xss参数设置

再看下面这段代码:


import java.util.ArrayList;
import java.util.List;

public class StringOOM {
	static String  base = "string";
	public static void main(String[] args) {
		List list = new ArrayList();
        for (int i=0;i< Integer.MAX_VALUE;i++){
            String str = base + base;
            base = str;
            list.add(str.intern());
        }
	}
}

在JDK8下运行:


Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Unknown Source)
	at java.lang.AbstractStringBuilder.expandCapacity(Unknown Source)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(Unknown Source)
	at java.lang.AbstractStringBuilder.append(Unknown Source)
	at java.lang.StringBuilder.append(Unknown Source)
	at StringOOM.main(StringOOM.java:12)

需要注意的是该代码在JDK6下运行会有差异,主要原因在于String.intern()方法在JDK7中改了实现方法,参考

Java8内存模型

由常量池 运行时常量池 String intern方法想到的(三)之String内存模型

>> 转载请注明来源:JVM 内存结构与内存溢出异常

●非常感谢您的阅读,欢迎订阅微信公众号(右边扫一扫)以表达对我的认可与支持,我会在第一时间同步文章到公众号上。当然也可点击下方打赏按钮为我打赏。

●推荐一家海外 VPS 服务商 Vultr,提供东京,洛杉矶等多个地区机房,最低2.5美元每月。点击>>注册邀请可以免费获得 $10 。另外还可以使用微林 vxTrans 国内中转来加速海外路线,助你打开新世界大门:)

免费分享,随意打赏

感谢打赏!
微信
支付宝

评论

还没有任何评论,你来说两句吧

发表评论