zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    循环
    list和tuple
    Python字符串和编码注意点
    【转载】国内安卓推送的希望
    【转载】Android属性动画和视图动画的区别
    【转载】Android 属性动画详解
    【转载】 android 属性动画详解
    java8 新特性学习详细篇2
    java8 新特性详细篇
    JAVA8十大新特性
  • 原文地址:https://www.cnblogs.com/sxkgeek/p/8946658.html
Copyright © 2011-2022 走看看