Java虚拟机解析篇之---内存模型
今天闲来无事来,看一下Java中的内存模型和垃圾回收机制的原理。关于这个方面的知识,网上已经有非常多现成的资料能够供我们參考,可是知识还是比較杂的,在这部分知识点中有一本书不得不推荐:《深入理解Java虚拟机》,如今已经是第二版了。这本书就从头開始详细介绍了Java整个虚拟机的模型以及Java的类文件结构,载入机制等。这里大部分的知识点都是能够在这本书中找到的,当然我是主要还是借鉴这本书中的非常多内容的。以下就不多说了。进入主题吧。
首先来看一下Java中的内存模型图:
第一、程序计数器(PC)
程序计数器(Program Counter Register)是一块较小的内存空间,它能够看做当前线程所运行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来取下一条须要运行的字节码指令,分支、跳转、循环、异常处理、线程恢复等基础功能都须要这个计数器来完毕
注:程序计数器是线程私有的。每条线程都会有一个独立的程序计数器
第二、Java栈(虚拟机栈)
Java栈就是Java中的方法运行的内存模型,每一个方法在运行的同一时候都会创建一个栈帧(关于栈帧后面介绍)。这个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至运行完毕的过程,就相应着一个栈帧在虚拟机栈中入栈到出栈的过程。
注:Java栈也是线程私有的。
异常可能性:对于栈有两种异常情况:假设线程请求的栈深度大于栈所同意的深度。将抛出StackOverflowError异常,假设虚拟机栈能够动态拓展,在拓展的时无法申请到足够的内存,将会抛出OutOfMemoryError异常
栈帧的概念:
栈帧用于支持虚拟机进行方法调用和运行的数据结构。
1) 局部变量表
局部变量表(Local Variable Table)是一组 变量值存储空间。用于存放 方法參数和方法内部定义的局部变量.局部变量表的容量以变量槽(Variable Slot。下称Slot)为最小单位. 一个Slot能够存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference[3]和returnAddress 8种类型,对于 64位的数据类型,虚拟机会以高位对齐的方式为其 分配两个连续的Slot空间(long double).
2) 操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈,当一个方法刚刚运行的时候。这种方法的操作数栈是空的。在方法的运行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作,比如。在做算术运算的时候通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行參数传递的。
举个样例:整数假发的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int类型的数值,当运行这个指令时。会将这两个int值出栈并相加,然后将相加的结果入栈。
3) 方法返回地址
一个方法開始运行后,仅仅有 两种方式能够退出这种方法。
第一种方式是运行引擎遇到随意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将依据遇到何种方法返回指令来决定,这样的退出方法的方式称为 正常完毕出口(Normal Method Invocation Completion)。第二种退出方式是。在方法运行过程中 遇到了异常,而且这个异常没有在方法体内得到处理,不管是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,仅仅要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这样的退出方法的方式称为 异常完毕出口(Abrupt Method Invocation Completion)。 一个方法使用异常完毕出口的方式退出,是不会给它的上层调用者产生不论什么返回值的
4) 附加信息
虚拟机规范同意详细的虚拟机实现添加一些规范里没有描写叙述的信息到栈帧之中,比如与调试相关的信息,这部分信息全然取决于详细的虚拟机实现,这里不再详述。在实际开发中,通常会把动态连接、方法返回地址与其它附加信息所有归为一类,称为栈帧信息。
第三、本地方法栈
本地方法栈与Java栈所发挥的作用是非常类似的,它们之间的差别只是是Java栈运行Java方法,本地方法栈运行的是本地方法。
注:本地方法栈也是线程私有的
异常可能性:和Java栈一样。可能抛出StackOverflowError和OutOfMemeryError异常
第四、Java堆
对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,差点儿所有的对象实例都在这里分配内存,当然我们后面说到的垃圾回收器的内容的时候,事实上Java堆就是垃圾回收器管理的主要区域。
注:堆是线程共享的
异常可能性:假设堆中没有内存完毕实例分配,而且堆也无法再拓展时,将会抛出OutOfMemeryError异常
第五、方法区
方法区它用于存储已被虚拟机载入的类信息、常量、静态常量、即时编译器编译后的代码等数据。
注:方法区和堆一样是线程共享的
异常可能性:当方法区无法满足内存分配需求时,将抛出OutOfMemeryError异常
1)运行时常量池
运行时常量池是方法区的一部分,Class文件里除了有类的版本号、字段、方法、接口等描写叙述信息外,另一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类载入器后进入方法区的运行时异经常量池存放。
上面就介绍了Java的内存的几个模块的相关概念,事实上我们须要知道这些知识。最基本的目的是不要在项目中写那些OOM的代码,由于我们假设知道了内存模型之后。即使代码中出现了OOM的问题,我们能够定位到哪里出了问题。
以下也来看一下上面说到的几个内存模块导致的内存溢出异常问题:
(这个也是面试的时候经常会被问到:比方叫你写一段让堆内存溢出的代码,或者是问你假设假设改动堆大小)
第一、堆溢出
public class HeapOOM { static class OOMObject{} /** * @param args */ public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while(true){ list.add(new OOMObject()); } } }我们上面看到堆主要是存放对象的,所以我们假设想让堆出现OOM的话。能够开一个死循环,然后产生新的对象就能够了。
然后在将堆的大小调小点。
加上JVM參数
-verbose:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError。
就能非常快报出OOM:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
第二、栈溢出
package com.cutesource; public class StackOOM { /** * @param args */ private int stackLength = 1; public void stackLeak(){ stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable{ // TODO Auto-generated method stub StackOOM oom = new StackOOM(); try{ oom.stackLeak(); }catch(Throwable err){ System.out.println("Stack length:" + oom.stackLength); throw err; } } }我们知道栈中存放的方法运行的过程中须要的空间,所以我们能够下一个循环递归,这样方法栈就会出现OOM的异常了。
设置JVM參数:-Xss128k。报出异常:
Exception in thread "main" java.lang.StackOverflowError
打印出Stack length:1007。这里能够看出。在我的机器上128k的栈容量能承载深度为1007的方法调用。
当然报这样的错非常少见,一般仅仅会出现无限循环的递归中。另外,线程太多也会占满栈区域:
package com.cutesource; public class StackOOM { /** * @param args */ private int stackLength = 1; private void dontStop(){ while(true){ try{Thread.sleep(1000);}catch(Exception err){} } } public void stackLeakByThread(){ while(true){ Thread t = new Thread(new Runnable(){ @Override public void run() { // TODO Auto-generated method stub dontStop(); } }); t.start(); stackLength++; } } public static void main(String[] args) throws Throwable{ // TODO Auto-generated method stub StackOOM oom = new StackOOM(); try{ oom.stackLeakByThread(); }catch(Throwable err){ System.out.println("Stack length:" + oom.stackLength); throw err; } } }这个栈的溢出,就是我们上面说到栈的时候的两种异常情况。
报出异常:Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread
第三、方法区溢出
public class MethodAreaOOM { static class OOMOjbect{} /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub while(true){ Enhancer eh = new Enhancer(); eh.setSuperclass(OOMOjbect.class); eh.setUseCache(false); eh.setCallback(new MethodInterceptor(){ @Override public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable { // TODO Auto-generated method stub return arg3.invokeSuper(arg0, arg2); } }); eh.create(); } } }我们知道方法区是存放一些类的信息等,所以我们能够使用类载入无限循环载入class,这样就会出现方法区的OOM异常。
手动将栈的大小调小点
加上JVM參数:-XX:PermSize=10M -XX:MaxPermSize=10M,运行后会报例如以下异常:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
第四、常量池溢出
public class ConstantOOM { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub List<String> list = new ArrayList<String>(); int i=0; while(true){ list.add(String.valueOf(i++).intern()); } } }我们知道常量池中存放的是运行过程中的常量,同一时候我们知道String类型的intern方法是将字符串的值放到常量池中的。所以上面弄能够开一个死循环将字符串的值都放到常量池中。这样常量池就会出现OOM异常了。由于常量池本身就是方法区的一部分,所以我们也能够手动的调节一下栈的大小。
总结:上面仅仅是从宏观的角度介绍了一下内存模型,详细关于内存中每一个区域的详细信息,能够阅读开头说到的那本非常不错的书籍。
当然我们在学习Java的时候能够分为四大模块:Java的Api、Java虚拟机(内存模型和垃圾回收器)、Java的Class文件、设计模式,关于Api的知识我们在工作的过程中用到的比較多,而且这部分内容全然是靠使用度,你用多了,api你自然就知道了。Java虚拟机和Java的Class文件的相关知识在工作中可能不一定能用到,可是这方面的知识能够让你更了解Java的整个体系结构。至于设计模式这个就是修炼的过程,也是最难的过程。得慢慢的体会其的强大之处。