JAVA虚拟机系列-内存数据区域

概述

JVM虚拟机(这里讲的是HotSpot)的内存可以抽象的分为两大块

  • 线程私有
    • 程序计数器
    • 虚拟机栈
    • 本地方法栈
  • 线程共享
    • 方法区

20191121-1

程序计数器

程序计数器是一块比较小的内存区域,主要的作用是记录当前线程执行的字节码指令位置。线程在执行字节码指令时,根据这个来定位当前执行的字节码指令位置,而且还可以通过它来计算下一个字节码指令。

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

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

线程在上下文切换的时候要保存运行现场,在恢复现场时,就可以根据程序计数器来确定该线程上次执行的指令的位置,从而让线程继续往下执行。

如果线程执行的是本地方法,此时程序计数器的值为空

虚拟机栈

虚拟机会为每个正在执行的方法创建一个栈帧,保存该方法的局部变量、操作数栈、动态链接、方法出口等信息。

每个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机中入栈到出栈的过程。

  1. 方法栈没有OOM这一说法
  2. 方法栈有自己的栈的大小,超过了该大小就抛异常
  3. 方法栈的大小可以配置

20191121-2

1.局部变量表

存放着方法运行时在该方法内定义的变量,这些变量在方法执行结束后会被回收

2.操作数栈

进行算术操作时存储变量,方法的参数也存在操作数栈中

3.动态链接

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

4.方法出口

方法退出有两种方式:return字节码退出和遇到异常退出。

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

本地方法栈

本地方法栈和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堆中的对象堆这块内存进行操作。

总结

按照线程私有和共享的角度区分

  • 线程私有
    • 程序计数器
    • 虚拟机栈
    • 本地方法栈
  • 线程共享
    • 方法区