Java 运行时数据区域
@[toc]
---
## Java 运行时数据区域划分
根据《Java 虚拟机规范》的规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域:

> 简单解释下线程共享和线程私有是啥意思:
>
> - 所谓线程私有,通俗来说就是每个线程都会创建一个属于自己的空间,每个线程之间的这块私有空间互不影响,独立存储。比如程序计数器就是线程私有的,每个线程都会拥有一个属于自己的程序计数器,互不干涉。
> - 线程共享就没啥好说的,简单理解为公共场所,谁都能去,存储的数据所有线程都能访问。
线程共享:堆和方法区,线程私有:虚拟机栈、本地方法栈、程序计数器
### 堆 Heap
Java 堆是虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,“**几乎**” 所有的对象实例都在这里分配内存(技术发展至今,其实并非所有的对象实例都会分配到堆上,比如逃逸技术,如果确定一个对象不会逃逸出线程之外,虚拟机会为对象直接在栈上分配内存)
### 方法区 Method Area
在虚拟机完成类加载之后,存储这个类相关的类型信息(Class 对象)、常量(final)、静态变量(static)、即时编译器编译后的代码缓存等数据
> 举个简单的小例子:
>
> 
#### 永久代
**JDK 8 之前,堆和方法区其实是连在一起的**,或者说,**方法区就是堆的一部分**。但是呢,方法区存储的东西又有些特别,在过去自定义类加载器使用不普遍的时候,类几乎是 “静态的” 并且很少被卸载和回收,因此类也可以被看成 “永久的”(这也就是永久代的含义),另外由于类作为 JVM 实现的一部分,它们不由程序来创建,**所以为了和堆区分开来呢**,就给了 “方法区” 这样一个名字用来存储类的信息,也有人把方法区称为 “非堆”。
需要注意的是,**无论是方法区还是非堆,其实都只是一个逻辑上的概念**
在 JDK 8 之前,其具体的实现方法是【**永久代**】(永久代是 HotSpot 虚拟机给出的实现,但是对于其他虚拟机实现,譬如 BEA JRockit、IBM J9 等来说,是不存在永久代的概念的)

**永久代是一段连续的内存空间,并且永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。**
#### 元空间
HotSpots 在 JDK 8 抛弃永久代,转而用【**元空间**】来实现方法区的**两大原因**:
- 由于永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收,增大了 OOM 发生的概率
- 有少数的方法例如 `String` 的 `intern()` 方法会因永久代的原因而导致不同虚拟机下有不同的表现,不利于代码迁移(比如 JRockit 虚拟机就没有永久代的概念)
**元空间与永久代之间最大的区别在于:元空间不再与堆连续,并且是存在于本地内存(Native memory)中的**。

运行时数据区域的对比如下图:

元空间存在于本地内存,意味着只要本地内存足够,它就不会 OOM,不会出现像永久代中的 `java.lang.OutOfMemoryError: PermGenspace`
#### 方法区中的存储变化
JDK 1.7 开始字符串常量池和静态变量被移动到了堆中:

### 虚拟机栈 Java Virtual Machine Stack
虚拟机栈其实是由一个一个的 **栈帧(Stack Frame)** 组成的,一个栈帧描述的就是一个 Java 方法执行的内存模型。也就是说**每个方法在执行的同时都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法的返回地址等信息**。
**每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程**。

其中,局部变量表存放了以下三种类型的数据:
- **编译期可知的各种 Java 虚拟机的基本数据类型**:boolean、byte、char、short、int、float、long、double
- **对象引用,reference 类型**:它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置。(关于 reference 类型,具体涉及到对象的访问定位的两种方式,使用句柄访问和使用直接指针访问,这个在后续文章中会详细介绍)
- **returnAddress 类型**:指向了一条字节码指令的地址
这些数据类型在局部变量表中的存储空间以【**局部变量槽 (Slot)**】来表示,或者说局部变量表的基本存储单元是 Slot,JVM 为每一个 Slot 都分配了一个访问索引,通过这个索引就可以成功访问到局部变量表中存储的某个值。
其中,64 位长度的 long 和 double 类型的数据会占用两个 Slot,其余的数据类型都是 32 位只占用一个。
### 本地方法栈 Native Method Stacks
本地方法栈和上面我们所说的虚拟机栈作用基本一样,区别只不过是本地方法栈为虚拟机使用到的 Native 方法服务,而虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务。
> 这里解释一下 **Native 方法**的概念,其实不仅 Java,很多语言中都有这个概念。
>
> "A native method is a Java method whose implementation is provided by non-java code."
>
> 就是说一个 Native 方法其实就是一个接口,但是它的具体实现是在外部由非 Java 语言比如 C 或 C++ 等来写的。Java 通过 JNI 来调用本地方法, 而本地方法是以库文件的形式存放的(在 WINDOWS 平台上是 DLL 文件形式,在 UNIX 机器上是 SO 文件形式)。
>
> 所以同一个 Native 方法,如果用不同的虚拟机去调用它,那么得到的结果和运行效率可能是不一样的,因为不同的虚拟机对于某个 Native 方法都有自己的实现,比如 Object 类的 `hashCode` 方法。
>
> **那么为什么需要 Native 方法呢**?
>
> 其主要原因就是 Java 虽然使用起来很方便,但是有些层次的任务用 Java 实现起来不容易,或者对程序的效率有比较高的要求时,Java 语言可能并不是最好的选择。所以 Native 方法使得 Java 程序能够超越 Java 运行时的界限,有效地扩充了 JVM。
### 程序计数器 Program Counter Register
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。**字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成**。
由于 Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。
**因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各个线程之间的计数器互不影响**。
> 那么程序计数器里存的到底是什么东西呢?
>
> - 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
> - 如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)
## 每个区域在什么情况下会发生 OOM?
1)**堆**:如果在堆中没有内存来完成对象实例的分配,并且堆也无法再扩展时,JVM 就会抛出 OutOfMemoryError 异常
2)**方法区**:
- JDK 8 之前的实现 - **永久代**:我们可以通过 `‑XX:MaxPermSize` 设置永久代的大小,一旦类的元数据超过了设定的大小,程序就会耗尽内存,并出现内存溢出错误 (`java.lang.OutOfMemoryError: PermGen space`)。
- JDK 8 及之后的实现 - **元空间**:<u>元空间存在于本地内存,意味着只要本地内存足够,它就不会 OOM</u>
3)**虚拟机栈 / 本地方法栈**:栈这个内存区域有两种异常状况:
- 如果<u>线程请求的栈深度大于虚拟机所允许的深度</u>,将抛出 `StackOverflowError` 异常(栈溢出)
- 如果使用的 JVM 支持动态扩展虚拟机栈容量的话,<u>当栈扩展时无法申请到足够的内存</u>就会抛出 `OutOfMemoryError` 异常(内存溢出)
4)**程序计数器**:此内存区域是唯一一个在《Java虚拟机规范》中【<u>没有</u>】规定任何 OutOfMemoryError(内存溢出)情况的区域(这个问题也算是一个比较常见的面试题了)
## 并不是所有的对象都分配到堆上,还可以分配到哪里
**栈上分配(Stack Allocations)**是 JIT 即时编译器的一项优化技术:如果确定一个对象不会逃逸出线程之外(不发生逃逸或逃逸程度较低 - 方法逃逸),那让这个对象在栈(线程私有)上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,**那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。**
