《JVM高级特性与最佳实践》学习笔记1

    第二章:Java内存区域与内存溢出异常

内存区域

        JVM(Java Virtual Machine) 在运行过程中会把它所管理的内存区域进行分类使用,每个区域都有自己的作用。
    运行时内存分为:

        《JVM高级特性与最佳实践》学习笔记1_第1张图片

  •    方法区是线程共享的一块区域,生命周期与虚拟机相同,用来存放虚拟机运行时加载的类信息、常量、静态变量和热点代码编译器编译后的代码等数据。也被称作"永久代",因为JVM的GC行为在这里发生的比较少,在这里的GC主要是对常量池中的常量进行回收和对类型的卸载(类有装载和卸载的说法),虽然GC行为较少,但还是有必要的,如果不进行GC,也会导致严重的内存泄漏。
       常量池是包含在方法区中的一部分用来存放常量的内存区域,包括编译时生成的各种字面量和符号引用。运行时也可以将新的常量装入常量池,如String类的intern()方法。
      异常情况:当该区域的内存无法满足分配时,抛出OutOfMemoryError。
  •   虚拟机栈是线程私有的一块区域,生命周期与线程相同。
      当线程中,每个方法被执行时,都会生成一个栈帧(Stack Frame),用于存储局部变量表、操作栈、动态链接、方法出口等信息。方法的执行到执行完毕的过程,就对应栈帧在虚拟机栈中入栈和出栈的过程。
       局部变量表是栈帧中比较重要的一部分,我们平常所说的变量存储在“栈”,其实就是存储在栈帧中的局部变量表中,局部变量表中存放了基本变量类型(int,float,short,double...)、对象的引用(对象地址或者对象句柄)和指向了一条字节码的returnAddress。
      异常情况:当虚拟机栈的容量固定时(可以设置,大多数是动态扩展的),线程所请求的栈深度大于虚拟机栈所允许的深度,抛出StackOverFlowError。当可以动态扩展时,线程请求时虚拟机栈无法再申请到足够的内存时,就会抛出OutOfMemoryError。
  •   本地方法栈基本与虚拟机栈一致,区别在于本地方法栈是用来处理JVM所需要的Native方法,在大部分使用的虚拟机中,已经把本地方法栈与虚拟机栈合二为一了。
  •   程序计数器是线程私有的内存区域,概念与普遍的程序计数器没有什么区别,都是通过改变程序计数器的指向,来进行下一条指令。只是在Java虚拟机中,程序计数器指向的是需要解释执行的下一条字节码指令,可以看作是当前线程用来执行下一条字节码行号的记录器。
      执行Java方法时,计数器指向一条字节码指令的地址,但在执行Native方法时,计数器为空(可以看作是JVM中的程序计数器之用来指向字节码指令的地址,Native方法执行时,就使用C/C++的程序计数器)。
      异常情况:程序计数器区域是JVM所有内存区域中唯一一块没有OutOfMemoryError发生情况的区域。
  •   Java堆是被所有线程共享的一块区域,这块区域的唯一目的就是存放Java对象的实例。几乎所有对象都会在这里被创建并分配空间,整个内存空间可以处于物理上不连续的内存空间。这里也是GC行为的“重灾区”,也被称为GC堆。
      异常情况:当无法再为对象的创建申请更多空间时,抛出OutOfMemoryError。
  •   直接内存是一块特殊的内存空间,它并不是虚拟机运行时数据区域的一部分,但也被频繁使用。NIO技术就会用到这部分内存,所以在设置虚拟机内存时,要考虑这一部分内存,虽然它不受Java虚拟机的限制,但是会受实际的物理内存限制。
      异常情况:忽略这一部分内存,可能会使原本计算好的虚拟机内存加上这一部分内存后超出物理内存限制,导致OutOfMemoryError。

对象访问

  • Object object = new Object();
        这是Java中最简单的对象创建,但是它也涉及到Java虚拟机栈、堆、方法区的使用。
        首先Object object这部分会反映到Java虚拟机栈中的本地变量表里,作为一个对象引用(reference)指向对象实例的内存区域(区域可能为空,之后才会被对象实例填充)。然后new Object()这一部分会在Java堆中生成Object对象的所有实例数据的结构化内存。Java堆中还需要包含对象类型数据信息指针,这些数据信息的实体在方法区中保存。
  • 对象创建之后,要访问对象就需要使用存放在虚拟机栈本地变量表中的对象引用,对象引用的主流访问方式有两种:
    1、使用句柄池访问,就是在Java堆中划分出一块内存作为句柄池,本地变量表中的reference指向句柄池中对应的对象指针,句柄池的对象指针再指向对象实例的地址。如图:
    《JVM高级特性与最佳实践》学习笔记1_第2张图片
    2、还有一种就是指针直接指向对象的实例数据,这样的话对象的类型指针会存放在实例数据中,如图:
        《JVM高级特性与最佳实践》学习笔记1_第3张图片
      区别:这两种方式各有各的好处:
            使用句柄池访问,就无需对reference进行频繁的修改(因为在进行GC时,对象地址变动是很普遍的),只需要对句柄 池指针进行更新即可。
            使用直接指针的优点就是速度快,因为只需要进行一次指针定位,在Java中对对象的访问很频繁,所以积少成多,使用直接指针可以减少时间耗费,sun公司的Hotspot虚拟机就是使用直接指针进行对象访问的。

溢出异常实战

  • Java堆的OOM
      在Java虚拟机上运行代码之后,就会产生OOM错误
import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:-Xms20M -Xmx20m -Xmn10M -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails
 * -verbose:gc
 * @author chen
 *
 */
public class OutOfMemoryInHeap {
	static class OOMObject{
	}
	
	public static void main(String[] args){
		//使用List来保存循环创建的对象,使对象不会被GC回收
		List list = new ArrayList();
		
		while(true){
			list.add(new OOMObject());
		}
	}
}

        错误信息:

《JVM高级特性与最佳实践》学习笔记1_第4张图片

    当Java堆中无法再为新创建的对象分配内存时,就会抛出OOM异常,之后的堆内存信息也体现了这一点。

    排查堆内存异常的思路:1、判断是内存泄漏还是溢出。2、如果是泄露,进一步查看泄露的内存到GC ROOTS的引用链。3、如果没有泄露,就需要尝试是否能调整-Xms和-Xmx的值将堆内存调大,或者查看代码,是否可以让一些对象不存活那么久。

  • 虚拟机栈和本地方法栈的OOM
        在栈中,Java虚拟机规定了两种异常,当申请的栈深度超过范围时抛出StackOverFlowError,虚拟机内存无法满足栈的自动扩展需要的内存时抛出OutOfMemoryError。
        在单线程并指定栈深度情况下,只会抛出StackOverFlowError异常。
    代码:单线程方法递归导致SOF异常
/**
 * VM Args:-Xss128k(指定栈深度)
 * @author chen
 *
 */
public class JavaVMStackSOF {
	private int stackLength = 1;
	
	public void stackLeak(){
		stackLength ++;
		//不断递归使栈帧深度增大
		stackLeak();
	}
	
	public static void main(String[] args) throws Throwable {
		JavaVMStackSOF oom = new JavaVMStackSOF();
		try {
			oom.stackLeak();
		} catch (Throwable e) {
			System.out.println("stack length:" + oom.stackLength);
			throw e;
		}
	}
}

    错误信息:stack length代表递归次数,也可以用来表示栈的深度

    在栈中要出现OOM异常,应该是在多线程场景下,每个线程都会有私有的栈空间,所以有可能你设置的栈大小越大,就会导致在多线程场景中,就越可能出现OOM异常,因为每个线程都会被分配一块栈内存,如果虚拟机内存无法满足分配了,就会出现OOM。所以在建立线程过多发生OOM异常的时候,有两种解决办法是:1、缩小最大堆容量 2、缩小栈容量,这种用缩小来解决OOM的方法不常见。

    代码:不断创建线程导致OOM

/**
 * 
 * VM Args:-Xss2M(设置更大的栈容量,使多线程时更易发生OOM)
 * @author chen
 *
 */
public class JavaVMStackOOM {
	
	//死循环,使线程不终结
	private void dontStop(){
		while(true){
			
		}
	}
	
	//不断创建、开启线程
	public void stackLeakByThread(){
		while(true){
			Thread thread = new Thread(new Runnable(){
				@Override
				public void run(){
					dontStop();
				}
			});
			thread.start();
		}
	}
	
	public static void main(String[] args) throws Throwable{
		JavaVMStackOOM oom = new JavaVMStackOOM();
		oom.stackLeakByThread();
	}
}

    错误信息:(我没有截取到,等了十分钟,仍然没有报错...但是系统会变的很卡,有要尝试的可以把栈容量继续调大。注意在Windows系统会有系统卡顿或卡死情况)  如果报错应该是

    

  • 运行时常量池的溢出
       
    常量池在1.8之前是方法区(永久代)的一部分,但是我使用的是JDK8版本,在1.8中,永久代变成了一个元数据区域(Metadata Space),与堆独立,而且在笔者实践中,指定永久代区域的大小对常量池溢出没有影响,所以与书上的参数不同。在实际线程中,常量池溢出所抛出异常的区域还是Java Heap Space,所以得出,常量池还是存在于Heap中的,只要指定Heap区域大小即可。
        代码如下:


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

/**
 * VM Args: -Xmx20M 
 * -XX:-UseGCOverheadLimit(这个参数是为了让虚拟机自行溢出,而不是通过GC时间来预判溢出并抛出异常)
 * 1.8之前设定常量池可以通过-XX:PermSize 和 -XX:MaxPermSize来设置。
 * @author chen
 *
 */
public class RuntimeConstantPoolOOM {
	public static void main(String[] args){
		// 使用List保持着常量池引用,避免被Full GC回收
		List list = new ArrayList();
		// 10MB 的PermSize 在 integer 范围内足够产生OOM
		int i = 0;
		while(true){
			list.add(String.valueOf(i++).intern());
		}
	}
}

    错误信息:

    《JVM高级特性与最佳实践》学习笔记1_第5张图片

    可以看到发生OOM异常的区域,还是在Java Heap space中。

  • 方法区(永久代)溢出
    代码如下:

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:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
 * @author chen
 *
 */
public class JavaMethodAreaOOM {

	public static void main(String[] args) {
		while(true) {
                        //使用cglib中的Enhancer对类进行增强,这样就会把大量的动态类信息放入方法区
			Enhancer enhancer = new Enhancer();
			enhancer.setSuperclass(OOMObject.class);
			enhancer.setUseCache(false);
			enhancer.setCallback(new MethodInterceptor() {
				
				public Object intercept(Object obj, Method method, Object[] args,
						MethodProxy proxy) throws Throwable {
					
							return proxy.invokeSuper(obj, args);
					
				}
				
			});
			enhancer.create();
		}
	}
	static class OOMObject{
	}
} 

错误信息:
    《JVM高级特性与最佳实践》学习笔记1_第6张图片

      可以看出是Metaspace区域溢出,这是JDK1.8中加入的元数据区域,与堆独立。是JDK8中新规定的永久代区域。

  • 本机直接内存(DirectMemory)溢出
        由于书中的Unsafe类已经在1.8中弃用,所以我使用NIO中的ByteBuffer类去申请堆外直接内存,使用这个去申请时,在抛出异常的时候,线程没有去真正申请内存,而是通过计算发现不足,就会抛出错误。
        代码如下:


import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 * @author chen
 *
 */
public class DirectMemoryOOM {
	private static final int _1MB = 1024*1024;
	
	public static void main(String[] args) {
		List list = new ArrayList();
		while(true){
			//list存放,防止被回收并重新分配
			list.add(ByteBuffer.allocateDirect(_1MB));
		}
		
	}
}

    错误信息:

    《JVM高级特性与最佳实践》学习笔记1_第7张图片

总结

    第二章学习了JVM运行时内存区域的分布,和内存区域各自的作用。并且在之后对每个内存区域会发生的异常情况都做了代码实践,溢出的原因大致是:
    Java堆:有太多无用的类但是都有到GC Roots的引用链无法被回收,抛出OOM;常量池被设置在堆中,有太多正在使用的常量(无法被回收)超出内存限制,也会在堆抛出OOM。
    Java虚拟机栈和本地栈:不断递归的方法使栈帧超出栈深度或者不断定义本地变量使本地变量表超出栈限制,并且在规定了栈的大小之后,抛出SOF。不断创建线程并分配相应的栈大小,会导致栈内存分配超出本机内存限制,导致OOM。
    方法区:有很多被增强的类正在使用,方法区会保存这些动态类的信息,导致Metaspace(新的方法区)抛出OOM。
    直接内存:通过NIO的类去申请堆外直接内存空间,并且保存引用链,在申请前计算出无法有足够的空间分配所需申请的空间时,抛出OOM异常。
    程序计数器:没有规定异常,所占空间也非常小,可以忽略。


你可能感兴趣的:(深入理解Java虚拟机)