Java运行时数据区基本介绍

42 阅读5分钟

java虚拟机在执行过程中会把所管理的内存分为若干个数据区域。根据Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域

image.png 其中方法区和堆是线程共享的,虚拟机栈,本地方法栈和程序计数器是线程私有(各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存)的

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过改编计数器的值来选取下一条要执行的字节码指令,它是程序控制流的指示器,例如循环,分支等基础功能都要依赖程序计数器完成。因为Java虚拟机的多线程是通过线程切换的,分配处理器执行时间的方式实现的,所以在任何时刻一个处理器都只会处理一个线程。因此为了线程切换后能恢复到正确的执行位置,需要程序计数器来记录命令的位置,所以每条线程都有自己对应的程序计数器,且是线程私有的

虚拟机栈和本地方法栈

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,虚拟机都会同步创建一个栈帧用于存储局部变量表,操作数栈,动态连接,方法出口等信息。 其中局部变量表存放了编译器内的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型是通过局部变量槽表示。

局部变量槽
  • 槽大小为32位,超过32位则会占用两个变量槽
  • 栈帧中局部变量表的槽位是可以复用的,如果某个局部变量的作用域已经结束了,那么在它后面声明的局部变量可以使用它的槽位

在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。(如果你的电脑内存支持动态扩展,JVM虚拟机启动需要内存,但是内存不够了。你的电脑会报oom异常。)

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

Java堆是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例, 在Java7以及Java7之前堆空间分为新生代,老年代和永久代,在Java8后永久代被元空间所取代,使用了直接内存来存储。java8之后划分为新生代和老年代

image.png

  • 堆大小 = 新生代 + 老年代。堆的大小可通过参数–Xms(堆的初始容量)、-Xmx(堆的最大容量) 来指定。
  • 其中,新生代 (Young) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。默认的,Edem : from : to = 8 : 1 : 1 。(可以通过参数 –XX:SurvivorRatio 来设定 。
  • 即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
  • JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。(为了之后的垃圾回收)
  • 新生代实际可用的内存空间为 9/10 (即 90%) 的新生代空间。 堆空间是一个线程共享的内存区域

方法区

方法区与堆一样是一个线程共享的内存区域,它用于存储已经被虚拟机加载的类型信息,常量,静态变量(java7后存储到堆中),即使编译器编译后的代码缓存等数据 在Java8前方法区基本与永久代等同,到Java7后初步将永久代中的常量字符串,静态变量等移到堆空间中。到Java8后已经完全废弃永久代空间,改用元空间进行替代。

《Java虚拟机规范》对方法区的约束是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集(方法区垃圾回收的难度也十分之高,考虑的场景较为复杂)。