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