jvm内存模型

  对于我们大多数java开发人员来说,jvm是我们不得不深入了解的东西,因为java开发是离不开jvm的,是基于java虚拟机之上运行的,而本节我将和大家分享一下jvm的内存模型(即运行时数据区)以及它们在某种情况下内存溢出时产生的异常。

一、运行时数据区

1、程序计数器

  程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器;为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存;此内存区域是唯一一个在Java虚拟机规范中没有规定任何**OutOfMemoryError**情况的区域

2、Java虚拟机栈

  Java虚拟机栈是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出**StackOverflowError**异常,如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存,就会抛出**OutOfMemoryError**异常本地方法栈与虚拟机栈非常相似,虚拟机栈为虚拟机执行Java方法,而本地方法栈为虚拟机执行Native方法

3、Java堆

  Java堆是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,也是垃圾收集器管理的主要区域,被称作“GC堆”,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出“**OutOfMemoryError**”异常
为了使JVM能够更好的管理堆内存中的对象,包括内存的分配和回收,堆被换分为两个不同的区域:**新生代**、**老年代**,默认比例为1:2(可以通过-XX:NewRatio),新生代又可以被划分为三个区域**Eden**、**From Survivor**、**To Survivor**,默认的比例为8:1:1(可以通过-XX:SurvivorRatio来设定,比如=3即3:1:1)

4、方法区

  方法区(永久代)是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出**OutOfMemoryError**异常

5、运行时常量池

  运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。线程共享,当常量池无法再申请到内存时会抛出**OutOfMemoryError**异常

二、各个数据区产生的异常

1、java堆溢出

  java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的限制后就会产生内存溢出异常
下述代码限制java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmm参数设置为一样即可避免堆自动扩展),通过-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便时候可以分析

// -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
public class HeapOOM {
  static class OOMObject{
  }
  public static void main(String[] args) {
    List<OOMObject> list=new ArrayList<OOMObject>();
    while(true){
    list.add(new OOMObject());
    System.out.println("------");
    }
  }
}

打印异常:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid14612.hprof ...
Heap dump file created [19332773 bytes in 0.186 secs]


2、虚拟机栈溢出

  通过-Xss参数可以设置栈内存容量
  在单线程操作下,无论是由于栈帧太大还是虚拟机栈帧容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常

// -Xss128k
public class JavaVMStackSOF {
  private int stackLength=1;
  public void stackLeak(){
  stackLength++;
  System.out.println("___---___---");
  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;
    }
  }
}

打印异常:
Exception in thread "main" java.lang.StackOverflowError
___---___---stack length:955

如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常

public class JavaVMStackSOF { 
    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) {
        JavaVMStackSOF oom=new JavaVMStackSOF();
        oom.stackLeakByThread();
} 

解释:
此时会抛出OutOfMemoryError异常

3、方法区和运行时常量池溢出         

  String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK1.6及以前的版本中,由于常量池分配在永久代内,我们可以通过-XX:PremSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量,

List<String> list=new ArrayList<String>();
int i =0;
while(true){
list.add(String.valueOf(i++).intern());}

此时会抛出OutOfMemoryError:PerGen space


JDK1.7运行就不会得到相同的结果,while()循环将一直进行下去,因为JDK1.7开始逐步“去永久代”

4、jdk版本不同结果不同

执行下面一段代码(jdk1.6、jdk1.7)

String str1=new StringBuilder("计算机").append("软件").toString();
System.out.printIn(str1.intern() == str1);
String str2=new StringBuilder("ja").append("va").toString();
System.out.printIn(str2.intern() == str2);

详解:
  这段代码在JDK1.6中运行,会得到两个false,而在JDK1.7中,会得到一个true和false,在JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false,而JDK1.7的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前以前出现过
字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true

posted @ 2018-04-25 19:12  StoneGeek  阅读(215)  评论(0编辑  收藏  举报