JVM 探秘 1:Java 内存区域

本贴最后更新于 2402 天前,其中的信息可能已经沧海桑田

本系列笔记目前主要基于《深入理解 Java 虚拟机:JVM 高级特性与最佳实践 第 2 版》,后续还会加入《实战 Java 虚拟机:JVM 故障诊断与性能优化》、《Java 性能权威指南》、《Java 性能优化权威指南》的阅读笔记。

概述

Java 虚拟机为程序员分担了很多内存管理的工作,不再像 C/C++ 那样容易出现内存泄漏和内存溢出问题了,也正是这样,导致一旦出现了内存泄漏和溢出方面的问题,就难以排查。因此一个优秀的 Java 程序员应该对 Java 虚拟机有充足的了解,JVM 是你的必修课。

运行时数据区域

根据《Java 虚拟机规范(Java SE 7 版)》,JVM 所管理的内存区域的划分如下:
image
每个内存区域都有各自的用途,以及创建和销毁时间,有的区域会随着虚拟机的启动而存在,有的区域依赖用户线程而建立和销毁,接下来一次介绍这些内存区域。

程序计数器

程序计数器(Program Counter Register)是线程私有的内存区域,是当前线程所执行的字节码的行号指示器,是一块较小的内存空间。

字节码解释器(java 源码编译成字节码,运行时解释成机器码执行)工作时,就是通过改变程序计数器的值,来选取下一条要执行的字节码。分支、循环、跳转等都依赖程序计数器执行。

当线程执行 Java 方法时,程序计数器记录的是正在执行的虚拟机字节码指令地址;当线程执行 Native 方法时,程序计数器的值为空(Undefined)。程序计数器是唯一一个在 Java 虚拟机规范中没有规定 OutOfMemoryError 的内存区域。

Java 虚拟机栈

Java 虚拟机栈(Java Stack)也是线程私有的内存区域,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用以存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从开始调用到执行完成的过程,就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型(boolean byte char short int long double float)、对象引用,以及 returnAddress 类型(指向一条字节码指令的地址)。局部变量表所需内存空间在编译期间完成分配,当进入一个方法时,需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

在 Java 虚拟机规范中,Java 虚拟机栈有可能会出现两种异常:StackOverflowError 和 OutOfMemoryError。如果线程请求的栈深度大于虚拟机栈的深度,则会 StackOverflowError。如果虚拟机栈动态扩展时申请不到足够的内存,则会 OutOfMemoryError。

本地方法栈

本地方法栈(Native Method Stack)与 Java 虚拟机栈的作用一样,是线程私有的,区别就是 Java 虚拟机栈为执行 Java 方法服务,本地方法栈为 Native 方法服务,虚拟机规范并没有对本地方法栈做强制规定,在 HotSpot 虚拟机中把本地方法栈和虚拟机栈合二为一了。此内存区域也会抛出 StackOverflowError 和 OutOfMemoryError。

Java 堆

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

Java 堆是垃圾收集器管理堆主要区域,也被称做“GC 堆”。现在垃圾收集器基本都采用分代收集算法,所以从内存回收角度看,Java 堆还可以细分为:新生代和老年代;新生代可以再细分为 Eden 空间、From Survivor 空间、To Survivor 空间。
如下图所示:
image

从内存分配角度看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。虚拟机为新生对象分配内存时,为每个线程在 Java 堆 中预先分配一小块内存,称做本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB 时,才会同步锁定(为了并发情况下的线程安全)。

Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果堆中没有足够内存完成实例分配,并且也无法扩展时,会抛出 OutOfMemoryError。

方法区

方法区(Method Aera)与 Java 堆一样,也是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在虚拟机规范中,它被描述为堆的一个逻辑部分,但实际它应该要与 Java 堆区分开来。

从分代收集的角度看,方法区也被称做永久代(Permanent Generation),实际上两者并不等价,只是因为 HotSpot 虚拟机使用永久代实现了方法区,对于其他虚拟机(JRockit、IBM J9)是不存在永久代概念的。

使用永久代实现方法区,更容易出现内存溢出问题(永久代有 -XX:MaxPermSize 的上限),所以在 JDK1.8 中,HotSpot 就取消了永久代(JEP122),取而代之的是元空间(MetaSpace),元空间是方法区新的实现,而且使用的是本地内存不是虚拟机内存。原先永久代中类的元信息会放入元空间,类的静态变量和常量会放入 Java 堆。

永久代也并不是指真的“永久”存在,只是说这部分内存回收(常量池回收和对类型的卸载)的成绩难以令人满意,条件也非常苛刻。

方法区会有 OutOfMemoryError 异常。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本信息、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中,另外翻译出来的直接引用也会存储在这个区域中。

这个区域另外一个特点就是动态性,Java 并不要求常量就一定要在编译期间才能产生,运行期间也可以在这个区域放入新的内容,String.intern()方法就是这个特性的应用。此区域有 OutOfMemoryError 异常。

JDK1.6 及之前,常量池是位于方法区中的,但在 JDK1.7 的时候常量池挪到了堆内存,也就是常量池和对象共享 Java 堆,所以在 Java7 以后,常量池就不在方法区分配了,而是在 Java 堆 中分配。

直接内存

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

JDK1.4 中加入了 NIO,这是一种基于通道(Channel)和缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样显著提高了性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM、SWAP 区)大小以及处理器寻址空间的限制。在配置虚拟机参数时,会根据实际内存配置 -Xmx 等,但经常忽略直接内存,使得各个内存区域总和大于了物理内存,从而导致动态扩展时出现 OutOfMemoryError。

发表于 2018-03-30,最后编辑于 2018-04-13
本文作者: Cellei
本文链接: http://www.cellei.com/blog/2018/03301
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3453 回帖 • 203 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • greekp

    因为堆和方法区域都是共享的
    是不是没有加线程安全的措施(加锁等操作)都是非线程的安全的?