0、前言
在Java中,由JVM虚拟机自动内存管理机制下,把内存控制权利交给 Java 虚拟机,不需要程序员去手动释放资源。但是这样也存在一些问题,就是会出现内存泄漏和溢出方面的问题。如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
1、JVM内存区域
JDK 1.8 和之前的版本略有不同,主要在于方法区的实现上的不同。具体来说,JDK 1.8之前,方法区是由永久代实现的,它的位置是划分给JVM的内存区域。但是1.8之后,方法区的实现改为了元空间,元空间存在于本地内存中。下面具体说一下方法区,永久代,元空间三个概念的区别。
- 方法区是个逻辑上的概念,是JVM的规范。主要用来存放类元信息,类的方法代码,变量名,方法名,访问权限,返回值,运行时常量池等等。
- 永久代是JDK 1.8之前方法区的实现。在逻辑上,包含新生代和老年代的堆区和实现方法区的永久代是两个不同的区域。但是在物理存储上,老年代和永久代却是连续的一块内存。他们占用的内存都属于虚拟机内存。
- 元空间是JDK 1.8之后方法区的实现。与永久代最大的区别在于,元空间占用的是本地内存,而不是虚拟机内存
JVM内存区域分为线程私有的和线程共享的:
- 线程私有:
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 线程共享
- 堆
- 方法区
程序计数器
每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码:用来存储指向一条指令的地址, 也即将要执行的下一条指令代码。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
为什么使用程序计数器记录当前线程的执行地址
因为 CPU 需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。JVM 的字节码解释器就需要通过改变 PC 寄存器的值来明确下一条应该执行什么样的字节指令
程序计数器为什么会被设定为线程私有
多线程在一个特定的时间段内只会执行其中某一个线程的方法, CPU 会不停的做任务切换,这样必然导致经常中断或恢复。每个线程要知道自己程序执行到哪里了,这就需要为每一个线程都分配一个程序计数器,各个线程之间可以进行独立计算,不会相互干扰。
它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈
每个方法在执行的时候,Java虚拟机栈都会同步生成一个栈帧,然后再将这个栈帧压入Java虚拟机栈中,所以Java虚拟机栈主要保存的就是这个栈帧。
当一个方法从调用到执行完毕,就意味着一个栈帧从Java虚拟机栈中从入栈到出栈的过程。每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
栈帧
栈帧主要保存局部变量表、操作数栈、动态链接、方法返回值这几类数据。
局部变量表用于存放方法参数和方法内部定义的局部变量,主要存放了编译期可知的各种数据类型以及对象引用。
操作数栈,方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。用于存放方法执行过程中的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
动态链接,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。
方法返回值,当方法开始执行后,只有两种方式可以退出:方法返回指令和异常终止。具体地说,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。
虚拟机栈中可能会出现的异常:
StackOverFlowError: 如果采用固定大小的 Java 虚拟机栈,线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,就抛出StackOverFlowError错误。OutOfMemoryError: 如果虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,则抛出OutOfMemoryError异常。
总之,虚拟机栈的作用是:它保存方法的局部变量、部分结果,并参与方法的调用和返回。
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
那什么是本地方法?
在Java程序上,本地方法是非java语言实现的方法。一个Native Method是这样一个java方法:该方法的实现由非java语言实现。
使用native标识符修饰的方法就是本地方法
因为Java代码有一定的局限性,有时候不能和系统底层交互,或是追求程序的效率时。这时候就需要更加底层的语言和更快的运行效率。
除此之外,本地方法栈的特点和虚拟机栈基本相同。
堆
JVM堆区域的空间划分:
- JVM中堆空间可以分成三个大区,新生代、老年代、永久代
- 新生代可以划分为三个区,Eden区,S1、S2两个幸存区
- Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
- 在JDK1.8版本废弃了永久代
当Eden区空间用完的时候,程序还需要创建对象,JVM的垃圾回收器将对伊甸区进行垃圾回收(Minor GC),将Eden区中不再被其他对象引用的对象进行销毁,将剩余的对象移动到S1。
若S1区满了,对S1区进行垃圾回收,将剩余的对象移动到S2区。如果S2区满了,再移动到老年代。
如果老年代已经满了,就产生了Major GC(Full GC),进行老年代的内存清理。如果执行了Full GC后依然无法进行对象的保存,就会产生OOM异常,
OutOfMemoryError。
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆。
Java的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
方法区
方法区是个逻辑概念,方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、即时编译器编译后的代码缓存。
方法区在JDK1.8由元空间实现,元空间使用的是本地内存
**为什么要将永久代替换为元空间 **
- 为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。
- 使用元空间就不再会出现永久代OOM问题了
- 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
2、小结
JVM内存模型请各位同学务必牢记在心,这是面试中常见的问题,要知道有哪些区域,区域中都存了什么,结构是什么样的,为什么这么做。