zoukankan      html  css  js  c++  java
  • 《深入理解Java虚拟机》学习笔记之内存分配

    JVM在执行Java程序的过程中会把它所管理的内存划分若干个不同的数据区域,如下图:

    大致可以分为两类:线程私有区域和线程共享区域。

    线程私有区域

    1. 程序计数器(Program Counter Register): 是一块很小的内存,可以看做是当前线程所执行的字节码行号指示器,虚拟机根据计数器值获取吓一条要执行的指令。
    2. JVM栈:虚拟机栈(JVM stacks),每个方法被执行时都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    3. 本地方法栈(Native Method Stacks):与虚拟机栈的作用类似,但区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用的Native方法服务。有些虚拟机如Sun HotSpot把它与虚拟机栈合二为一。

    线程共享区域

    1. Java堆(Java Heap):是Java编程最频繁使用和最大的内存区域,也是垃圾收集器管理的主要区域,此区域唯一存在的目的就是存放对象实例和数组。
    2. 方法区(Method Area):用于储存已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。
    3. 运 行时常量池(Runtime Constant Pool):是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法去的运行时常量池中。

    其实还一块 内存即直接内存(Direct Memory),它并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存也被频繁地使用,并且可能导致OOM异 常。如NIO可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用 进行操作,这显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

    内存异常

    程 序计数器是唯一一个在Java虚拟机规范中没有规定任何OOM(OutOfMemoryError)情况的区域,其他区域在无法再申请到足够的内存时就会 抛出OOM异常,而虚拟机栈和本地方法栈除了OOM异常外,当线程请求的栈深度大于虚拟机所允许的深度时将抛出StackOverflowError异 常。

    对象访问

    即使最简单的对象访问,都会涉及到Java栈、Java堆和方法区这三个最重要的区域。如:

    Object obj = new Object();

    “Object obj”将反映到JVM栈的局部变量表中,作为一个reference类型数据出现;而“new Object()”将反映到Java堆中,形成一块储存了Object类型所有实例数据值的结构化内存;另外在Java堆中还必须包含能查找到此对象类型 数据信息(如对象类型、父类、实现的接口、方法等),这些类型数据则储存在方法区。

    由于Java虚拟机规范之规定了reference类型指向对象的引用,并没有定义寻址方式,因此目前有两种主流的寻址方式:使用句柄和直接指针。

    如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下图所示

     如果使用直接指针访问方式,Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址,如下图所示

    异常示例

    1. Java堆溢出OOM
      package net.oseye.demo;
      
      import java.util.ArrayList;
      import java.util.List;
      /**
       * VM Args:-Xms20m -Xmx20m
       */
      public class App 
      {
          public static void main( String[] args )
          {
              List<OOMObject> list=new ArrayList<App.OOMObject>();
              
              while(true){
              	list.add(new OOMObject());
              }
          }
          static class OOMObject{}
      }
      执行
      java -Xms20m -Xmx20m net.oseye.demo.App
      报的溢出信息

      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.ensureCapacityInternal(Unknown Source)
      at java.util.ArrayList.add(Unknown Source)
      at net.oseye.demo.App.main(App.java:15)

      ps:通过虚拟机参数“ -Xms20m -Xmx20m”将Java堆最大最小大小设置为20M,即不可扩展;使用List是为了保证GC Roots到list对象之间有可达路径来避免垃圾回收机制清除list对象;
    2. 虚拟机栈StackOverFlowError
      package net.oseye.demo;
      /**
       * VM Args:-Xss256k
       */
      public class App {
      
      	private int stackLength = 1;
      
      	public void stackLeak() {
      		stackLength++;
      		stackLeak();
      	}
      
      	public static void main(String[] args) throws Throwable {
      		App oom = new App();
      		try {
      			oom.stackLeak();
      		} catch (Throwable e) {
      			System.out.println("stack length:" + oom.stackLength);
      			throw e;
      		}
      	}
      }
      执行
      java -Xss256k net.oseye.demo.App
      异常信息

      stack length:1890
      Exception in thread "main" java.lang.StackOverflowError
      at net.oseye.demo.App.stackLeak(App.java:11)
      at net.oseye.demo.App.stackLeak(App.java:11)
      at net.oseye.demo.App.stackLeak(App.java:11)

      这个测试我是在ubuntu下做的,是单线程的,每个线程都有属于自己的虚拟机栈,当一个方法被调用时就会产生这个方法相关的一个栈帧,当方法执行完毕这个栈帧才会从栈顶pop掉,而实例中使用了递归就会一直向虚拟机栈push栈帧直到深度大于所允许的深度时旧会抛出StackOverFlowError。
      PS:这里扑捉异常使用的是Throwable,如果使用Exception就不能显示出println的信息,此处不解,有待学习
    3. 虚拟机栈 OOM
      可以开多个线程让虚拟机栈OOM,但其实这不是虚拟机栈抛出的,只是由于分给栈的内存多了自然会让虚拟机进程内存少了。这里不妨把XSS设置大一些。注意这里有风险哦,会造成操作系统假死,我在ubuntu下执行不仅ubuntu死了,我直接按电源启动ubuntu还让操作系统崩溃了,启动时报的异常“No init found. Try passing init= bootarg”,然后按照这个方法才修复了操作系统
      package net.oseye.demo;
      
      /**
       * VM Args:-Xss10m
       */
      public class App {
      
      	public static void main(String[] args) throws Throwable {
      		App app = new App();
      		app.stackLeakByThread();
      	}
      
      	private void dontStop() {
      		while (true) {
      			System.out.println(Thread.currentThread().getId());
      		}
      	}
      
      	private void stackLeakByThread() {
      		while (true) {
      			Thread thread = new Thread(new Runnable() {
      				@Override
      				public void run() {
      					dontStop();
      				}
      			});
      			thread.start();
      		}
      	}
      }
      执行后报的异常类似

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

    4. 运行时常量池溢出
      如果向要运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含了一个等于此String对象的字符串,则返回idaibiao池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量:
      package net.oseye.demo;
      
      import java.util.ArrayList;
      import java.util.List;
      
      /**
       * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
       */
      public class App {
      
      	public static void main(String[] args) throws Throwable {
      		List<String> list=new ArrayList<String>();
      		int i=0;
      		while(true){
      			list.add(String.valueOf(i).intern());
      		}
      	}
      }
      这是书上的例子,说会报异常:

      Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
      at java.lang.String.intern(Native Method)

      但我一直没跑出这个异常(win7,⊙﹏⊙b汗这个总结先后在win2003、ubuntu、win7上做的),及时又重新设小了PermSize。
      PS:google了之后才知道在jdk6及之前都会报上述异常,但jdk7就不会,而我用的是jdk7.简单来说就是在JDK 7里String.intern生成的String不再是在perm gen分配,而是在Java Heap中分配。
    5. 方法区溢出
      访法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这个区域的测试,基本思路是运行时产生大量的类去填满访法区,直到溢出。书中是借助CGLib框架直接操作字节码,生成大量的动态类:
      package net.oseye;
      
      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
       */
      public class App {
      
      	public static void main(String[] args) throws Throwable {
      		while (true) {
      			Enhancer enhancer = new Enhancer();
      			enhancer.setSuperclass(OOMObject.class);
      			enhancer.setUseCache(false);
      			enhancer.setCallback(new MethodInterceptor() {
      
      				public Object intercept(Object arg0, Method arg1,
      						Object[] arg2, MethodProxy arg3) throws Throwable {
      					return arg3.invokeSuper(arg0, arg2);
      				}
      			});
      			enhancer.create();
      		}
      	}
      
      	static class OOMObject {
      	}
      }
      书中说会报异常:

      java.lang.OutOfMemoryError: PermGen space
      at java.lang.ClassLoader.defineClass1(Native Method)
      at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
      at java.lang.ClassLoader.defineClass(ClassLoader.java:616)

      但遗憾的是我的测试依然没有出现上述异常,而只是报(JDK7)

      Exception in thread "main"
      Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

    6. 本机直接内存溢出
      DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样,代码清单2-9越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为,虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。
      package net.oseye;
      
      import java.lang.reflect.Field;
      import sun.misc.Unsafe;
      
      /**
       * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
       */
      public class App {
      
      	private static final int _1MB = 1024 * 1024;
      
      	public static void main(String[] args) throws Exception {
      		Field unsafeField = Unsafe.class.getDeclaredFields()[0];
      		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 net.oseye.App.main(App.java:19)

    出处:http://www.zhaiqianfeng.com    
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    SQL必知必会-笔记(五)函数
    软件测试面试题:系统中的图片不显示如何排查原因
    windows用浏览器访问linux目录文件
    记测试工作中一次印象深刻的事
    怎么快速适应新的测试工作?
    xshell如何导出日志文件和上传文件
    jmeter+fiddler高效率整理接口脚本
    python-用requests库处理form-data格式的参数
    软件自动化测试工程师面试题集锦(4)
    shell脚本批量检查某个或多个服务的端口和进程是否正常
  • 原文地址:https://www.cnblogs.com/zhaiqianfeng/p/4621232.html
Copyright © 2011-2022 走看看