《深入理解java虚拟机》笔记(1)运行时数据区域


1、Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。

2、运行时数据区域划分

  java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个区域,这些区域都有各自的用途,创建和销毁时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁,根据《Java虚拟机规范(Java SE 7版)》的规定,java虚拟机分为以下区域。

  

  2.1、程序计数器(Program Counter Register)

  程序计数器属于线程私有,是一块较小的空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。每条线程都有独立的计数器,各条线程之间计数器互不影响,独立存储。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

  2.2、java虚拟机栈(VM Stack)

  和程序计数器一样,都属于线程私有,生命周期与线程相同,描述的是java方法执行的内存模型,每个方法执行都会创建一个栈帧,用于存储局部变量表,操作栈,动态链接,方法出口等信息,每一个方法被调用直至执行完成的过程,就对应一个栈帧在虚拟机栈从入栈到出栈的过程。局部变量表存放了编译期可知的各种数据基本类型(Boolean,byte,char,short,int,float,long,double),以及对象的引用。

  这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;虚拟机栈在进行动态扩展时,无法申请到足够的内存,将抛出OutOfMemoryError异常。

  2.3、本地方法栈(Native Method Stack)

  本地方法栈与虚拟机栈所发挥的作用非常相似,他们之间区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

  举例(StackOverflowError):

  

 1 public class TestStackSOF {
 2     public long stackLenth = 1;
 3     
 4     public void stackSOF(){
 5         stackLenth++;
 6         System.out.println(stackLenth);
 7         stackSOF();
 8     }
 9     public static void main(String[] args) {
10         TestStackSOF tss = new TestStackSOF();
11         try{
12             tss.stackSOF();
13         }catch(Throwable e){
14             System.out.println("stackLenth: "+tss.stackLenth);
15             try {
16                 throw e;
17             } catch (Throwable e1) {
18                 // TODO Auto-generated catch block
19                 e1.printStackTrace();
20             }
21         }
22     }
23 }

  执行的结果:

  stackLenth: 326323

  java.lang.StackOverflowError

      at com.cn.TestStackSOF.stackSOF(TestStackSOF.java:9)

      at com.cn.TestStackSOF.stackSOF(TestStackSOF.java:9)

  如果把-Xss调到50M,执行的结果是:

  stackLenth: 1637043

  java.lang.StackOverflowError

      at com.cn.TestStackSOF.stackSOF(TestStackSOF.java:9)

      at com.cn.TestStackSOF.stackSOF(TestStackSOF.java:9)

  总结不难看出,-Xss大小不一样,执行的结果也不一样。如果以后在项目中遇到java.lang.StackOverflowError异常,可以先检查代码是否有无限递归,如果不是,可加大-Xss大小再看运行效果。

  2.4、Java堆(Heap)

  java堆是Java虚拟机所管理的内存中最大的一块,被所有线程共享的内存区域。此区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

  java堆是垃圾收集器管理的主要区域,也叫做”GC堆“。

  java堆的大小可扩展,通过-Xmx和-Xms控制,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

  举例(OutOfMemoryError):

import java.util.ArrayList;
import java.util.List;

public class TestHeapOOM {
    /**
     * vm args -Xmn120M -Xmx1024M
     */
    public static void main(String[] args) {
        List list = new ArrayList();
        while(true){
            list.add("sss");
        }
    }
}

  运行结果:

  Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

      at java.util.Arrays.copyOf(Unknown Source)

  2.5、方法区(Method Area)

  方法区与Java堆一样,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也叫做”永久代“,并非是指数据永久存在。该区域内存回收目标主要是针对常量池的回收和对类型的卸载。这个区域的回收成绩比较难以令人满意,尤其是类型的卸载,条件相当苛刻。但这部分内存回收是必要存在的。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

  2.6、直接内存(Direct Area)

  直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分被频繁的使用,也可能导致OutOfMemoryError异常出现。

  直接内存不受Java堆大小的限制,在JDK1.4中新加入了NIO(New Input/Output)类,NIO的Buffer提供一个可以直接访问系统物理内存的类——DirectBuffer。DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同。普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的限制。而DirectBuffer直接分配在物理内存中,并不占用堆空间。在访问普通的ByteBuffer时,系统总是会使用一个“内核缓冲区”进行操作。而DirectBuffer所处的位置,就相当于这个“内核缓冲区”。因此,使用DirectBuffer是一种更加接近内存底层的方法,所以它的速度比普通的ByteBuffer更快。