zoukankan      html  css  js  c++  java
  • Java内存区域

    JVM系列随笔主要是对《深入理解Java虚拟机:JVM高级特性与最佳实践 第2版》的学习总结

    Java内存区域大纲

    概述

    Java虚拟机自动内存管理机制,能够让程序员不必为每个对象new/delete,不容易出现内存泄露和内存溢出。

    运行时数据区域

    根据Java虚拟机规范,运行时数据区域如下图所示:

    运行时数据区域

    程序计数器

    • 当前线程锁执行的字节码的行号指示器,用来指示下一条字节码指令。
    • 线程私有。
    • 唯一一个Java虚拟机规范没有规定任何OutOfMemoryError的区域

    Java虚拟机栈

    • 描述Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等。局部变量表存放了编译器可知的各种基础数据类型、对象引用和returnAddress类型。当进入一个方法时,方法所需帧中分配的局部变量空间完全确定。
    • 线程私有。
    • 虚拟机规范定义了两种异常:StackOverFlow-线程请求深度大于允许值;OutOfMemoryError-无法申请到更多内存

    本地方法栈

    • 与虚拟机栈类似,区别是虚拟机栈执行Java方法,而本地方法栈执行Native方法。
    • 线程私有
    • 抛出StackOverFlowOutOfMemoryError两种异常

    Java堆

    • Java堆是JVM管理内存中最大的一块,几乎所有的对象都在这里分配。是垃圾回收管理的主要区域,也被称为GC堆。可以处于物理上不连续的内存空间,只要逻辑上连续即可。
    • 线程共享
    • 抛出OutOfMemoryError

    方法区

    • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。JVM规范描述为堆的一个逻辑部分,别名Non-Heap。习惯Hotspot上叫做永久带,原因是设计团队把GC分带收集扩展至方法区,使用永久带来实现罢了。
    • 运行时常量池是方法区的一部分,用于存放Class文件编译期生成的各种字面量和符号引用。常量池具有动态性,可以运行时期间将新的常量放入池中。用于存放JDK1.7中把字符串常量池移除。
    • 线程共享
    • 抛出OutOfMemoryError

    直接内存

    • 直接内存不是JVM运行时数据区的一部分,但是也常被使用,例如NIO的DirectByteBuffer操作方式。也可导致OutOfMemoryError

    HotSpot对象

    对象的创建

    在语言层面,创建对象仅需要一个new关键字。

    虚拟机层面,当遇到一个new时:

    • 首先检查常量池中能否定位到一个符号引用。符号引用所代表的类如果没有被加载,需要先加载、解析、初始化改类。
    • 然后,类加载同构后,在堆上为新对象分配内存。内存分配又分为“指针碰撞”和“空闲列表”两种方式,取决于Java堆是否规整,而Java堆是否规整又取决于垃圾回收器是否带有压缩整理功能。
    • 然后,将分配的内存空间初始化为零值(不包括对象头)。
    • 接着,对这个对象进行设置,比如是哪个类的实例,如何找到类元数据、对象哈希码、GC分代年龄信息等。这些对象信息放在对象头中。
    • 最后,一般情况下都会执行方法,按照程序员的意愿进行初始化。至此,虚拟机层面对象创建完成。

    对象的内存分布

    HotSpot中,对象的内存中存储布局可以划分为3块区域:对象头,实例数据和对齐填充。

    • 对象头

      对象头包含两部分数据:

      • 自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,也称为Mark Word。

      • 类型指针,即对象指向它的元数据的指针,JVM通过这个指针确定这个对象是哪个类的实例。

    • 实例数据

      也即程序代码中定义的各种类型的字段内容,包括父类继承下来的和子类中定义的。

    • 对齐填充

      不是必然存在的,也没有特别含义,仅起占位符作用。由于HotSpot VM自动内存管理要求对象起始地址必须是8字节的倍数。因此当对象数据没对齐时,需要填充补全。

    对象的访问定位

    Java程序通过栈上的reference数据来操作堆上的具体对象。目前主流reference定位对象的方式包括以下两种:

    • 句柄方式

      Java堆中会划分出来一块内存作为句柄池。reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体地址。

      句柄方式

    • 直接指针访问

      Java堆对象中放置访问类型数据的相关信息,而reference存储的就是对象地址。

    直接指针

    直接指针访问方式的好处是速度更快,比句柄访问方式减少了一次指针定位时间开销。Sun HotSpot使用的这种方式

    OutOfMemoryError异常

    通过手动产生溢出的方式,加深对运行时数据区的理解

    Java堆溢出

    Java堆用于存放实例对象,因此制造溢出的思路是限制堆大小的情况下,不断生成对象进行填充,直至溢出。设置参数-Xms最小值,-Xmx最大值,当两个值相同时表示不可扩张。

    另外,通过设置-XX:+HeapDumpOnOutOfMemoryError可以让JVM在内存溢出时Dump出当前的内存堆转储快照便于事后分析。

    /**
     * -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
     */
    public class HeapOOM {
    	public static void main(String[] args) {
    		List<OOMObject> list = new ArrayList<OOMObject>();
    		while (true) {
    			list.add(new OOMObject());
    		}
    	}
    
    	static class OOMObject {
    	}
    }
    

    运行几秒种后输出下面的结果:

    java.lang.OutOfMemoryError: Java heap space
    Dumping heap to java_pid8328.hprof ...
    Heap dump file created [2474990 bytes in 0.009 secs]
    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)
    	...
    

    Profiler分析图

    虚拟机栈和本地方法栈溢出

    栈中以线程为单位,存放的方法调用的各类数据。HotSpot中并不区分虚拟机栈和本地方法栈,因此虽然存在设置本地方法栈的参数-Xoss,但是实际上无效。栈容量只由参数-Xss设定。JVM规范中定义了两种异常:

    • 如果线程请求的栈深度大于JVM允许的最大深度,抛出StackOverFlowError异常
    • 如果JVM在扩展栈空间时无法申请到足够的空间,抛出OutOfMemoryError异常

    首先测试StackOverFlowError,代码如下:

    /**
     * -Xss128k
     */
    public class VmSOF {
    
    	int stackLength = 1;
    
    	public void stackLeak() {
    		stackLength++;
    		stackLeak();
    	}
    
    	public static void main(String[] args) throws Throwable {
    		VmSOF sof = new VmSOF();
    		try {
    			sof.stackLeak();
    		} catch (Throwable e) {
    			System.out.println("stack length:" + sof.stackLength);
    			throw e;
    		}
    	}
    }
    

    输出:

    stack length:981
    Exception in thread "main" java.lang.StackOverflowError
    	at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:11)
    	at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:12)
    	at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:12)
    	at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:12)
    	...
    

    上述代码只会抛出StackOverFlowError异常,而调整-Xss参数变大变小只影响方法栈调用深度变多变少,而并不能产生出OutOfMemoryError

    通过分析,运行时区域中虚拟机栈和本地方法栈是线程隔离的,当栈空间一定时,支持的线程数量是一定的。因此OutOfMemoryError可以通过不断生成线程来制造出来。

    测试OutOfMemoryError的代码:

    /**
     * -Xss128k
     */
    public class VmsOOM {
    	public static void main(String[] args) {
    		while (true) {
    			Thread t = new Thread(new Unstoppable());
    			t.start();
    		}
    	}
    
    	static class Unstoppable implements Runnable {
    		@Override
    		public void run() {
    			while (true);
    		}
    	}
    }
    

    结果如下

    Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
    	at java.lang.Thread.start0(Native Method)
    	at java.lang.Thread.start(Unknown Source)
    	at edu.uestc.l08.VmsOOM.main(VmsOOM.java:15)
    

    注:上述代码容易造成系统假死,需要慎重测试

    方法区溢出

    方法区用于存放Class的类型元数据,测试的思路是在限定方法区大小的情况下,产生大量的类去填充直至溢出。

    本机测试使用JDK1.8,此版本不在使用永久带来实现方法区,有运行提示为证:

    Java HotSpot(TM) Client VM warning: ignoring option PermSize=10m; support was removed in 8.0
    Java HotSpot(TM) Client VM warning: ignoring option MaxPermSize=10m; support was removed in 8.0

    JDK1.8后使用称为元空间的MetaSpace来实现,因此JVM启动参数设置为-XX:MaxMetaspaceSize=10m -XX:MaxMetaspaceSize=10m,测试使用CGLib填充方法区,代码如下:

    import java.lang.reflect.Method;
    
    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    /**
     * -XX:MaxMetaspaceSize=10m  -XX:MaxMetaspaceSize=10m
     */
    public class MethodAreaOOM {
    	
    	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 arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
    					return arg3.invokeSuper(arg0, arg2);
    				}
    			});
    			enhancer.create();
    		}
    	}
    	
    	static class OOMObject {
    	}
    }
    

    输出为:

    Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
    	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
    	at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
    	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
    	at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
    	at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
    	at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
    	at edu.uestc.l08.MethodAreaOOM.main(MethodAreaOOM.java:22)
    Caused by: java.lang.reflect.InvocationTargetException
    	at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    	at java.lang.reflect.Method.invoke(Unknown Source)
    	at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:413)
    	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336)
    	... 6 more
    Caused by: java.lang.OutOfMemoryError: Metaspace
    	at java.lang.ClassLoader.defineClass1(Native Method)
    	at java.lang.ClassLoader.defineClass(Unknown Source)
    	... 11 more
    

    另外,由于运行时常量池作为方法区中特殊的一块,也有存在OOM的可能。在JDK1.6及之前的版本中,通常使用String.intern()的例子来制造溢出。主要代码为:

    while (true) {
    	list.add(String.valueOf(i++).intern());
    }
    

    在JDK1.7之后,intern()方法被修改,不在复制实例,而是在常量首次出现时仅保留堆中对象的引用,从而上例比较难制造溢出。具体不在赘述,详情可参考:http://blog.csdn.net/seu_calvin/article/details/52291082

    本机直接内存溢出

    直接内存通过-XX:MaxDirectMemorySize指定,如果不指定则与堆大小相同。周总的例子如下:

    import java.lang.reflect.Field;
    import sun.misc.Unsafe;
    
    /**
     * -Xmx20M -XX:MaxDirectMemorySize=10M
     */
    public class DirectMemoryOOM {
    
    	private static final long _1MB = 1024 * 1024;
    
    	public static void main(String[] args) throws Exception {
    		Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
    		unsafeField.setAccessible(true);
    		Unsafe unsafe = (Unsafe) unsafeField.get(null);
    		while (true) {
    			unsafe.allocateMemory(_1MB);
    		}
    	}
    }
    

    输出结果如下:

    Exception in thread "main" java.lang.OutOfMemoryError
    	at sun.misc.Unsafe.allocateMemory(Native Method)
    	at edu.uestc.l08.DirectMemoryOOM.main(DirectMemoryOOM.java:17)
    

    由本地内存导致的内存溢出有一个特征,就在Heap Dump中不会看到明显的异常。如果OOM后dump文件很小,而程序使用了NIO,则可以考虑是这方面原因。

  • 相关阅读:
    Chrony时间同步
    使用cfssl生成自签证书
    Docker运行时资源限制
    Docker的OverlayFS存储驱动
    Docker文件挂载总结
    Docker配置文件deamon.json详解
    Docker网络模式详解
    Dcoker命令使用详解
    Docker架构分解
    《数据采集和分析平台》笔记
  • 原文地址:https://www.cnblogs.com/zhiqianye/p/6165961.html
Copyright © 2011-2022 走看看