JVM之内存区域

72 阅读6分钟

运行时数据区域

java虚拟机为了方便内存管理,将内存划分了不同的区域,不同的区域各司其职,保存着java程序在运行时的各项数据信息。要想了解java内存模型,首先就要对虚拟机内存的各个区域有一定的了解。虚拟机将内存划分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区这五个区域,其中程序计数器、虚拟机栈、本地方法栈是线程依赖用户线程的启动和结束而建立和销毁,线程之间相互隔离;而堆和方法区是域随着虚拟机进程的启动而一直存在,各个线程共享。 image.png


程序计数器

是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处 理、线程恢复等基础功能都需要依赖这个计数器来完成。由于线程的执行是相互独立的,在进行线程切换时,每个线程都能接着上次继续执行,因此每个线程都需要有一个独立的程序计数器,因此说这块区域是“线程私有”的内存区域。
当线程在执行一个java方法时,程序计数器记录的是正在执行的虚拟机字节码指令地址,如果执行的是一个本地方法,则记录为空(undefined),此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。


虚拟机栈

这块区域是线程私有的,从名字可以看出是一个“先进后出”的区域,记录的是一个正在执行的java方法,一次方法调用就意味着一个栈帧入栈,方法结束就意味着栈帧出栈。我们可以从正常的方法调用来理解这个区域,假如main方法调用A方法,A方法嵌套调用方法B,那么main方法的栈帧先入栈,然后A对应的栈帧先入栈,在A执行的过程中调用B,B栈帧入栈,B方法执行完毕,则B栈帧出栈,然后A栈帧出栈,最后main栈帧出栈。

image.png 虚拟机栈是一个保存栈帧的栈,栈帧是一个保存方法必要信息的数据结构,它记录了局部变量表、操作数栈、动态连接、方法出口等信息。
这个区域可能出现StackOverflowError或者OutOfMemoryError,这两种异常根据不同情况出现,如果虚拟机栈不可动态扩展,在栈深度达到虚拟机运行的最大限度时就会出现StackOverflowError,如果可以动态扩展,在无法申请到内存是就会出现OutOfMemoryError。HotSpot虚拟机的栈容量是不可以动态扩展的,所以如果申请栈空间成功,就不会出现OutOfMemoryError,但是在申请时失败,仍然是会出现OOM异常的。


本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,区别在于本地方法栈是为调用本地方法服务的。


堆是虚拟机管理的内存中最大的一块区域,几乎所有的对象都在堆上分配内存,但并不绝对,可能经过逃逸分析等技术,一些对象是有可能不会在堆上分配内存的。在为对象分配内存时,当堆中的对象占用的内存达到最大限制且无法扩展时,就会出现OutOfMemoryError。
Java堆是垃圾收集器管理的内存区域,基于分代收集理论的垃圾收集器会对堆进行逻辑上的区域划分,例如:新生代、老年代等,这些并不是虚拟机对内存的划分,只是那些垃圾收集器为了方便进行垃圾收集,进行的逻辑划分。由于堆是线程共享的区域,所以在进行内存分配时,是必然要保证线程安全的,否则不同线程就有可能将对象分配在同一个地址空间。虚拟机通过TLAB+cas的方式保证内存分配时的线程安全以及效率问题。
内存分配且听下回分解...


方法区

与堆一样是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,在JDK8之前,HotSpot将收集器的分代设计扩展至方法区,使用永久代来实现方法区,注意:永久代是HotSpot虚拟机垃圾收集器的概念,而方法区是虚拟机规范中的概念。JDK7HotSpot将字符串常量池、静态变量等移出永久代,JDK8之后HotSpot移除了永久代,将剩下的信息保存到本地内存中,这部分区域叫元空间,注意:元空间不是虚拟机中的内存区域。根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError异常。
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池和class文件中的常量池的区别在于,运行时常量池可以在运行时加入新的常量,例如String类的 intern()方法,而class文件中的常量池只能在编译期间生成。


直接内存

这部分区域不是虚拟机管理的内存区域,在JDK 1.4中新加入了NIO类,引入了一种基于通道与缓冲区 的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据,这部分区域受总内存影响,也会出现OutOfMemoryError异常。