Jvm 内存分布机制

内存分布概览

java虚拟机运行时会将数据分为几个区域,如下图所示
Jvm 内存分布机制_第1张图片

程序计数器

程序计数器是一块比较小的内存区域,它主要的作用是存储当前线程执行字节码的行号。在虚拟机内存模型中(一般的虚拟机会进行优化),字节码解析工作是通过该计数器来确定下一个字节码的执行。

例如以下某个方法的字节码,程序计数器的作用是保存本线程当前执行的指令位置,因此程序计数器是线程私有的,每个线程都有。

    0: iconst_1                                                                                                
    1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;            
    4: astore_1                                                                                                

虚拟机栈

在讲虚拟机栈的信息时,先看看线程执行方法的过程是怎样的。

public void test(){
     //todo 方法内的一些操作
     int res = add(9);
     //todo 方法内一些操作
}
public int add(int c){
     int a = 5;
     int b = 9;
     return a * c - b;
}
  1. java执行test()方法,将创建一个栈帧,用于运行该方法
  2. test()方法中,调用add方法,此时将创建一个test方法的栈帧,然后将该栈帧压至虚拟机栈顶
  3. 创建好add方法栈帧后就开始逐步的运行add方法的字节码指令
  4. 在运行时有局部变量a,b,要将a,b初始化并保存在栈帧中
  5. 计算 a * c - b时,要将a,b,c 在操作数栈进行进栈出栈操作
  6. 计算完成之后,要将计算结果返回值上一个调用的位置
  7. add方法栈帧出栈,此时栈顶为test方法的栈帧

上述线程执行方法的过程大致的列出了线程在执行方法时需要使用哪些内存。

虚拟机栈实际上是java执行方法的内存模型,在执行方法时,为要执行的方法创建一个栈帧,然后运行该方法。在执行过程中,如果有调用其他方法,则将新方法的栈帧压至栈顶,然后执行。

虚拟机栈帧

线程调用方法时的数据结构,它包含了方法的局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法在被执行时都会创建一个栈帧,在创建栈帧时虚拟机就已经确定好局部方法表的深度、操作数栈的深度等信息。

  • 局部变量表

局部变量表存储的是在方法内定义的变量,这些变量的作用域在方法内并且方法执行完毕之后这些变量将会被回收

  • 操作数栈

操作数栈的作用是在进行算术操作时存储变量或者是在调用其他方法时通过操作数栈进行参数调用,它是一个先进后出的栈。操作数栈的每一个元素可以是任意的java数据模型,32位的操作数占栈的容量为1,64位的操作数占的容量为2。

  • 动态链接

每一个栈帧中都有一个指向常量池中该方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,执行方法调用即使用这些符号引用。一些符号引用在类加载阶段或第一次使用时转为直接引用,这种转化成为静态解析。如果符号引用在运行期才转化为直接引用,则称为动态连接。

  • 方法出口

方法推出有两种方式:return字节码退出和遇到异常退出。
return字节码退出:如果有返回值则将返回值传递给上一个调用者,如果没有则直接执行上一个方法剩下的指令
遇到异常退出:方法内遇到异常,则查看本方法内是否有异常处理表,如果有则进行相应的处理,如果没有则将异常抛出,交个上一个调用的方法处理。

本地方法栈

本地方法栈和java虚拟机栈非常类似,也是线程私有,也是具有一些信息存储。他们之间唯一的差别就是java虚拟机栈是为了java程序中的方法也就是字节码的方法服务的,而本地方法栈是给Native方法服务的。

方法区

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

运行时常量池是方法区的一部分,这部分虽属于方法区,但在具体的环境中的作用很关键。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

在Hotspot虚拟机中常把方法区成为“永久代”,但是两者有本质的区别,仅仅是因为HotSpot虚拟机使用永久代来实现方法区而已,这样虚拟机的垃圾回收就可以不去处理该区域的数据。在其他的虚拟机(如BEA JroJrockit、IBM J9等)是没有永久代的概念的。实际上使用永久代来实现方法区并不是一个好主意,它有可能导致内存溢出问题。

堆可以说是虚拟机中内存分布最大的一块,它也是线程共享的。在java中,几乎所有的对象实例都存放在堆区域,但随着JIT编译器和逃逸分析的技术逐渐成熟,线上分配、标量替换优化技术会导致一些微妙的变化,所有实例都在堆上存储变得不再那么绝对。

java堆是垃圾回收的主要区域,从垃圾回收的角度来看,它可以分为新生代和老年代。在新生代中又可以细分为Eden空间、From Survivor空间和To Survivor空间。其实无论怎么划分,都与存放的内容无关,进一步的划分只是为了更好更快的分配内存。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的的一部分。但这部分内存在项目中也会被频繁的使用,而且也可能导致OOM异常,所以我们一起进行归类。

在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方法,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的对象堆这块内存进行操作。

参考文章和书籍:

  • java 深度解析JVM内存分布机制
  • 《深入理解java虚拟机》

你可能感兴趣的:(JVM从入门到放弃)