![KDPDTG0]0Q9Q5Z94`NJ$R4.jpg
Java虚拟机的内存空间分为五个部分 分别是:
1)堆
2)方法区
3)Java虚拟机栈
4)本地方法栈
5)程序计数器

1 堆(heap)

对于大多数应用来说 Java堆(Java Heap) 是Java虚拟机所管理的内存中最大的一块
堆是被所有线程共享的一块内存区域 在虚拟机启动的时候创建 用于存放对象实例
几乎所有的对象实例都在这里分配内存

堆的特点

1)线程共享
整个Java虚拟机只有一个堆 所有的线程都访问同一个堆

2)在虚拟机启动时创建
3)垃圾回收的主要场所
4)可以进一步细分为:新生代(Young/New)、老年代(Old/Tenure)
新生代又可被分为:生成区/伊甸园区(Eden)、幸存区(Survivor)
幸存区又可被分为:Survivor 0 space(S0 也叫from区)、Survivor 0 space(S1 也叫to区)
无论如何划分 无论哪个区域 存储的仍然是对象实例
进一步划分的目的是为了更好的回收内存 或者更快的分配内存
不同的区域存放具有不同生命周期的对象
这样可以根据不同的区域使用不同的垃圾回收算法 从而更具有针对性 效率更高
5)堆的大小既可以固定也可以扩展 但主流的虚拟机堆的大小是可扩展的(可设置-Xms 初始化堆;-Xmx 最大堆空间)
因此当线程请求分配内存 但堆已满 且内存已满无法再扩展时 就抛出OutOfMemoryError

2 方法区

2.1 什么是方法区?

Java虚拟机规范中定义方法区是堆的一个逻辑部分 但是它却有一个别名叫做Non-Heap非堆 目的应该是与Java Heap 区分开来
方法区中存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等

2.2 方法区的特点

1)线程共享
方法区是堆的一个逻辑部分 因此和堆一样 都是线程共享的
整个虚拟机中只有一个方法区
2)方法区演变史
a)在JDK1.7以前HotSpot虚拟机使用永久代来实现方法区
永久代的大小在启动JVM时可以设置一个固定值(-XX:MaxPermSize) 不可变
b)在JDK1.7中 存储在永久代的部分数据就已经转移到Java Heap或者Native memory(本地内存)
譬如符号引用(Symbols)转移到了native memory
原本存放在永久代的字符常量池移出
但永久代仍存在于JDK 1.7中 并没有完全移除
c)JDK1.8中进行了较大改动
移除了永久代(PermGen)替换为元空间(Metaspace)
元空间不在jvm中 而是在本地内存中
永久代中的 class metadata 转移到了 native memory(本地内存 而不是虚拟机)
永久代中的 interned Strings 和 class static variables 转移到了 Java heap
永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)
3)内存回收效率低
方法区中的信息一般需要长期存在 回收一遍内存之后可能只有少量信息无效
对方法区的内存回收的主要目标是:对常量池的回收 和 对 类型的卸载

2.3 什么是运行时常量池?

1)运行时常量池(Runtime Constant Pool) 是方法区的一部分(虚拟机规范)
2)Class文件中除了有类的版本、字段、方法、接口等描述信息外
还有一项信息是常量池(Constant Pool Table) 用于存放编译期生成的各种字面量和符号引用
这部分内容将在类加载后存放到方法区的运行时常量池中
比如 我们一般在一个类中通过public static final来声明一个常量
这个类被编译后便生成Class文件 当这个类被Java虚拟机加载后 Class文件中的常量就存放在方法区的运行时常量池中
3)此外Java语言并不要求常量只有在编译期才能产生
比如在运行期 String.intern也会把新的常量放入池中
4)当运行时常量池中的某些常量没有被对象引用 同时也没有被变量引用 那么就需要垃圾收集器回收

3 Java虚拟机栈

Java虚拟机栈是描述Java方法运行过程的内存模型
Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做**“栈帧”**的区域
这块区域用于存储该方法在运行过程中所需要的一些信息
这些信息包括:
1)局部变量表
存放基本数据类型变量、引用类型的变量、returnAddress类型的变量(指向了一条字节码指令的地址)
2)操作数栈
用来存储运算过程中的临时变量的
3)动态链接
将符号引用转换为调用方法的直接引用称为链接
a)当一个字节码文件被装载进JVM内部时 如果被调用的目标方法在编译期可知 且运行期保持不变
那么在这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
b)如果被调用的方法在编译期无法被确定下来 也就是说 只能够在程序运行期将调用方法的符号引用转换为直接引用
由于这种引用转换过程具备动态性 因此也就被称之为动态链接
比如 Java中的多态所引起的现象 即子类重写父类的方法后 调用者在编译期不明确将要调用的具体方法 只能动态链接
4)方法出口信息
方法执行完成后 需要将PC寄存器的值恢复到调用该方法的栈帧要执行的下一条指令的地址
5)附加信息
当一个方法即将被运行时 Java虚拟机栈首先会在Java虚拟机栈中为该方法创建一块“栈帧”
栈帧中包含局部变量表、操作数栈、动态链接、方法出口信息等
当方法在运行过程中需要创建局部变量时 就将局部变量的值存入栈帧的局部变量表中
当这个方法执行完毕后 这个方法所对应的栈帧将会出栈 并释放内存空间
注意:
人们常说 Java的内存空间分为“栈”和“堆” 栈中存放局部变量 这句话不完全正确
这里的“栈”只代表了Java虚拟机栈中的局部变量表部分
真正的Java虚拟机栈是由一个个栈帧组成
而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息

3.1 Java虚拟机栈的特点

1)Java虚拟机栈也是线程私有的 每个线程都有各自的Java虚拟机栈 而且随着线程的创建而创建 随着线程的死亡而死亡
2)局部变量表的创建是在方法被执行的时候 随着栈帧的创建而创建
而且局部变量表的大小在编译时期就确定下来了 在创建的时候只需分配事先规定好的大小即可
此外 在方法运行的过程中局部变量表的大小是不会发生改变的
3)Java虚拟机栈会出现两种错误:StackOverFlowError和OutOfMemoryError
a)StackOverFlowError
若Java虚拟机栈的内存大小不允许动态扩展
那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候 就抛出StackOverFlowError异常
比如 死递归
可以设置Java虚拟机栈的内存的大小 比如 -Xss256K
设置之后还是固定的 超过这个数值之后还是会发生栈内存溢出错误
b)OutOfMemoryError
若Java虚拟机栈的内存大小允许动态扩展
那么当扩展时无法申请到足够的内存 此时抛出OutOfMemoryError异常
注:StackOverFlowError和OutOfMemoryError的异同?
StackOverFlowError 表示当前线程申请的栈超过了事先定好的栈的最大深度 但内存空间可能还有很多
OutOfMemoryError 是指当线程申请栈时发现栈已经满了 而且内存也全都用光了

4 本地方法栈

4.1 什么是本地方法栈?

本地方法栈和Java虚拟机栈实现的功能类似 、同样是线程私有的
它们之间的区别不过是Java虚拟机栈为Java 方法服务 而本地方法栈为虚拟机使用到的Native 方法服务
本地方法被执行的时候 在本地方法栈也会创建一个栈帧 用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息
方法执行完毕后相应的栈帧也会出栈并释放内存空间
也会抛出StackOverFlowError和OutOfMemoryError错误

5 程序计数器(pc寄存器)

5.1 什么是程序计数器?

程序计数器是一块较小的内存空间 可以把它看作当前线程正在执行的字节码的行号指示器
也就是说 程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址
注:如果当前线程正在执行的是一个本地方法 那么此时程序计数器为空

5.2 程序计数器的作用

程序计数器有两个作用:
1)字节码解释器通过改变程序计数器来依次读取指令 从而实现代码的流程控制
如:顺序执行、选择、循环、异常处理
2)在多线程的情况下 程序计数器用于记录当前线程执行的位置
从而当线程被切换回来的时候能够知道该线程上次运行到哪了

5.3 程序计数器的特点

1)是一块较小的存储空间
2)线程私有 每条线程都有一个程序计数器
3)是唯一一个不会出现OutOfMemoryError的内存区域
4)生命周期随着线程的创建而创建 随着线程的结束而死亡

6 直接内存

1)在JDK1.4 中新加入的NIO 类 引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O 形式
它可以通过调用本地方法直接分配堆外内存 然后通过一个存储在Java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作
而无需先将外面内存中的数据复制到堆中再操作 从而提升了数据操作的效率

2)直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分 也不是Java虚拟机规范中定义的内存区域
但是这部分内存也可能会被频繁地使用
直接内存的大小不受Java虚拟机控制 但既然是内存 则肯定还是会受到本机总内存的大小及处理器寻址空间的限制
当内存不足时还是会出现OutOfMemoryError错误(OOM错误)

综上所述

1)Java虚拟机的内存模型中一共有两个“栈” 分别是:Java虚拟机栈和本地方法栈
两个“栈”的功能类似 都是方法运行过程的内存模型
并且两个“栈”内部构造相同 都是线程私有
只不过Java虚拟机栈描述的是Java方法运行过程的内存模型 而本地方法栈是描述Java本地方法运行过程的内存模型
2)Java虚拟机的内存模型中有两个区域是线程共享的 一个是堆 一个是方法区
方法区本质上是属于堆的一个逻辑部分
这两个区域都是在JVM启动的时候就创建 JVM停止才销毁
堆中存放对象 方法区中存放类信息、常量、静态变量、即时编译器编译的代码
3)堆是Java虚拟机中最大的一块内存区域 也是垃圾收集器主要的工作区域
4)程序计数器、Java虚拟机栈、本地方法栈是线程私有的
即每个线程都拥有各自的程序计数器、Java虚拟机栈、本地方法区。并且他们的生命周期和所属的线程一样