第2章 Java内存区域与内存溢出异常--《深入理解 Java 虚拟机》笔记

时间:2021-08-14 00:42:18

1概述

基本就是C和C++程序编写需要维护没一个对象生命开始到终结。


Java把内存控制权利交给了Java 虚拟机,但是有时却会出现内存溢出等问题。


2运行时数据区域

Java虚拟机在执行Java程序的过程中会把他所管理的内存换分为若干不同的内存区域。

第2章 Java内存区域与内存溢出异常--《深入理解 Java 虚拟机》笔记
===运行时数据区====
方法区(Method Area)   虚拟机栈(VM Stack)   本地方法栈(Native Method Stack)
堆(Heap)               程序计算器(Program Counter Register)
===运行时数据区====
↓↑ ↓↑
     执行引擎 本地库接口  →    本地方法库
     
方法区和堆:由所有线程共享的数据区
其他区:线程隔离的数据区






1、程序计数器:
程序计数器(Program Counter Register)是一块较小的内存空间,他的作用可以看做是当前线程所执行的字节码的行数指示器。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理机执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程上的指令。
因此,为了线程切换后能恢复到正常的执行位置,每条线程都需要有一条单独的程序计数器,各条线程之间的计数器互不影响,独立存储,我们成这个内存区域为“线程私有”的内存。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。






2、Java虚拟机栈
与程序设计器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,他的生命周期和线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会被创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
每个方法被调用直至执行完成的过程,就对应这一个栈帧在虚拟机栈中从入栈到出栈的过程。


在Java虚拟机规范中,对这个区域规定了两种异常情况:
如果线程请求的栈深度大于虚拟机所允许的深度,讲抛出*Error异常;
如果虚拟机可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。






3、本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行方法(也就是字节码)服务,而本地方法栈为虚拟机使用Native方法服务。




4、Java堆
对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域存在的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。


Java堆是垃圾收集器管理的主要区域,因此很多时候也被成为“GC”堆(Collected Heap)。
还有一些划分是为了更好的回收内存,或者更快的分配内存,如:Eden控件、From Survivor空间、To Survivor空间等。


一般主流的虚拟机都可以控制扩展这块区域的大小(-Xmx 和 -Xms)。
如果在堆中没有内存完成实际分配,并且堆无法再扩展,将会抛出OutOfMemoryError异常。




5、方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。是堆的一个逻辑部分。


这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。


也有OutOfMemoryError。






6、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的 一部分。用于存储编译期生成的各种字面量和符号引用。


也有OutOfMemoryError。






7、直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。
用于提高性能,如NIO(New Input/Output)类,缓存区(Buffer)之类的。



3对象访问

例子:
Object obj = new Object();
假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。
而“new Object()”这部分的语义将会反映到Java堆中,形成一个存储了Object类型所有实例数据值(Instance Data)的结构化内存。
另外,在Java堆中,还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。


reference对方反问方式主要有两种:使用句柄和直接指针。
使用句柄访问方式,Java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
如果使用直接指针访问方式,Java堆对象的布局中就必须考虑如何放置访问类型的数据的相关信息,reference中直接存储的就是对象地址。



4实战:OutOfMemoryError异常

1、Java堆溢出
java.lang.OutOfMemoryError: Java heap space
Java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清楚这些对象,就会在数量达到最大堆的容量限制后产生内存溢出异常。


Demo:
先配置DebugVM

//VM arguments:
//	-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

package cn.partner4java.vm;

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

/**
 * VM args:-Xmx20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * 
 * @author partner4java
 * 
 */
public class HeapOOM {
	static class OOMObject {

	}

	public static void main(String[] args) {
		List<OOMObject> list = new ArrayList<HeapOOM.OOMObject>();
		int i = 0;
		try {
			while (true) {
				i++;
				list.add(new OOMObject());
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			System.out.println("i=" + i);
		}

//		后台打印:
//		[GC [DefNew: 8192K->1024K(9216K), 0.0175111 secs] 8192K->4442K(19456K), 0.0175397 secs]
//		 [GC [DefNew: 6433K->1024K(9216K), 0.0241072 secs] 9852K->9737K(19456K), 0.0241371 secs]
//		 [GC [DefNew: 7581K->7581K(9216K), 0.0000196 secs][Tenured: 8713K->10240K(10240K), 0.0482572 secs] 16294K->11919K(19456K), 0.0483254 secs]
//		 [Full GC [Tenured: 10240K->7992K(10240K), 0.0443166 secs] 18432K->14504K(19456K), [Perm : 2060K->2060K(12288K)], 0.0443550 secs]
//		 [Full GC [Tenured: 8593K->8593K(10240K), 0.0532794 secs] 17809K->17809K(19456K), [Perm : 2060K->2060K(12288K)], 0.0533182 secs]
//		 [Full GC [Tenured: 8593K->8590K(10240K), 0.0618192 secs] 17809K->17806K(19456K), [Perm : 2060K->2058K(12288K)], 0.0618540 secs]
//		 i=1507964
//		 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
//		 	at java.util.Arrays.copyOf(Arrays.java:2760)
//		 	at java.util.Arrays.copyOf(Arrays.java:2734)
//		 	at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
//		 	at java.util.ArrayList.add(ArrayList.java:351)
//		 	at cn.partner4java.vm.HeapOOM.main(HeapOOM.java:23)
//		 Heap
//		  def new generation   total 9216K, used 9216K [0x04550000, 0x04f50000, 0x04f50000)
//		   eden space 8192K, 100% used [0x04550000, 0x04d50000, 0x04d50000)
//		   from space 1024K, 100% used [0x04d50000, 0x04e50000, 0x04e50000)
//		   to   space 1024K,   0% used [0x04e50000, 0x04e50000, 0x04f50000)
//		  tenured generation   total 10240K, used 8598K [0x04f50000, 0x05950000, 0x05950000)
//		    the space 10240K,  83% used [0x04f50000, 0x057b58c8, 0x057b5a00, 0x05950000)
//		  compacting perm gen  total 12288K, used 2079K [0x05950000, 0x06550000, 0x09950000)
//		    the space 12288K,  16% used [0x05950000, 0x05b57d58, 0x05b57e00, 0x06550000)
//		 No shared spaces configured.

	}
}

错误: java.lang.OutOfMemoryError: Java heap space


首先分辨出是出现了内存泄漏(Memory Leak)还是内存溢出(MemoryOverflow)






2、虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
Demo:

java.lang.*Error:
package cn.partner4java.vm;

/**
 * VM args:-Xss128K
 * @author partner4java
 *
 */
public class JavaVMStackSOF {
	private int stackLength = 1;
	
	private void stackLeak() {
		stackLength++;
		stackLeak();
	}
	
	public static void main(String[] args) throws Exception {
		JavaVMStackSOF stackSOF = new JavaVMStackSOF();
		try {
			stackSOF.stackLeak();
		} catch (Exception e) {
//			throw e;
		}finally{
			System.out.println("stack length:" + stackSOF.stackLength);
		}
//		后台打印:
//		stack length:7651
//		java.lang.*Error
	}
}

OutOfMemoryError:
java.lang.OutOfMemoryError: unable to create new native thread

package cn.partner4java.vm;

/**
 * VM args:-Xss2M
 * @author partner4java
 *
 */
public class JavaVMStackOOM {
	private void dontStop() {
		while(true){
			
		}
	}
	
	private void stackLeakByThread() {
		while(true){
			Thread thread = new Thread(){
				public void run() {
					dontStop();
				};
			};
			thread.start();
		}
	}
	
	public static void main(String[] args) {
		JavaVMStackOOM stackOOM = new JavaVMStackOOM();
		stackOOM.stackLeakByThread();
//		后台打印:
//		java.lang.OutOfMemoryError: unable to create new native thread
	}
}

3、运行时常量池溢出
java.lang.OutOfMemoryError: PermGen space


Demo:

package cn.partner4java.vm;

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

/**
 * VM args:-XX:PermSize=10M -XX:MaxPermSize=10M
 * @author partner4java
 *
 */
public class RuntimeConstantPoolOOM {
	public static void main(String[] args) {
		//使用list保存常量池的引用,避免Full GC回收常量池行为
		List<String> list = new ArrayList<String>();
		//10MB的PermSize在integer范围内足够产生OOM了
		int i=0;
		while(true){
			list.add(String.valueOf(i++).intern());
		}
//		后台打印:
//		java.lang.OutOfMemoryError: PermGen space
	}
}

4、方法区溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
java.lang.OutOfMemoryError: PermGen space
Demo:

package cn.partner4java.vm;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * VM args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author partner4java
 *
 */
public class JavaMethodAreaOOM {

	public static void main(String[] args) {
		while(true){
			Enhancer enhancer = new Enhancer();
			enhancer.setSuperclass(OOMObject.class);
			enhancer.setUseCache(false);
			enhancer.setCallback(new MethodInterceptor() {
				
				@Override
				public Object intercept(Object obj, Method method, Object[] args,
						MethodProxy proxy) throws Throwable {
					return proxy.invoke(obj, args);
				}
			});
			enhancer.create();
		}
	}

//	后台打印:
//	java.lang.OutOfMemoryError: PermGen space
	
	static class OOMObject {

	}
}



5、本机直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,默认与Java堆的最大值(-Xmx指定)一样。
java.lang.OutOfMemoryError


Demo:

package cn.partner4java.vm;

import java.lang.reflect.Field;

import sun.misc.Unsafe;

/**
 * VM args: -Xmx20M -XX:MaxDirectMemorySize=10M
 * @author partner4java
 *
 */
public class DirectMemoryOOM {
	private static final int _1MB = 1024*1024;
	
	public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
		Field unsafeField = Unsafe.class.getDeclaredFields()[0];
		unsafeField.setAccessible(true);
		Unsafe unsafe = (Unsafe) unsafeField.get(null);
		while(true) {
			unsafe.allocateMemory(_1MB);
		}
//		后台打印:
//		java.lang.OutOfMemoryError
	}

}