【JVM】内存模型

JVM内存模型是JVM高效运行的基础,也是JVM核心部分,同样也是面试官手里的香饽饽,盘它不要犹豫(●・̆⍛・̆●)


概览

内存作为重要的系统资源,为硬盘和CPU的握手做了桥梁,JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行,不同的JVM对于内存的划分方式和管理机制存在着部分差异。为了直观感受,先上经典JVM内存模型的结构组成图

2UAF1.png

区域解释

程序计数器(PC)

便于记忆的说,PC就是一块内存区域,里面存放着下一条要执行的指令的地址,也就是当前所执行的字节码的行号指示区,它会告诉字节码解释器下一条要解释的字节码的行号,起到了一个定位作用,其中类似于分支、循环、跳转、异常处理、线程恢复等功能都是依赖于PC来完成的。由于PC是线程隔离的,所以它不仅可以实现代码的流程控制,而且在多线程的情况下,它还可以记录下当前线程的执行位置,以便于再次切换到线程时能够继续顺序执行

需要注意的是:程序计数器是唯不会出现 OutOfMemoryError 的内存区域,因为它的生命周期与线程保持同步,随着线程的创建而创建,随着线程的结束而死亡。

虚拟机栈

我们在JVM字节码执行机制中详细的讲述了关于线帧的内容,它包括了局部变量表、操作数栈、动态链接、方法出口信息的内容,这里虚拟机栈就是线帧作为组成成分的栈结构,所以在方法执行中,虚拟机栈就是运用压栈出栈的方式对方法的执行进行控制,对于具体的操作流程这里不再赘述,不清楚这一块内容的小伙伴可以看往期blog《【JVM】字节码执行机制》或直接点击传送门

虚拟机栈是JVM内存模型中非常重要的一个角色,关于它我们还需要注意的是虚拟机栈会出现的两种异常问题:

  1. StackOverFlowError:如果虚拟机栈的大小不允许动态扩展,那么如果当前线程请求超过了虚拟机栈的最大深度,那么将会抛出此异常;
  2. OutOfMemeoryError:如果虚拟机栈的大小允许动态扩展,那么如果当前线程请求栈时内存已经用尽,那么将会抛出此异常。

本地方法栈

本地方法栈不仅在名字上与虚拟机栈相似,他们的作用也非常的相似,本地方法栈与虚拟机栈的运作方式基本相同,包括可能抛出的两种异常。虚拟机栈主要是为了虚拟机执行字节码服务,而本地方法栈服务的是虚拟机使用native方法,在HotSpot虚拟机中与Java虚拟机相融合.

关于native方法:

简单来说native方法就是一个不是使用Java代码编写的但是Java需要使用的这么一个方法,这个方法的接口并非Java代码的接口,也就是说native是用作Java和其他语言协作时使用到的方法。这个特性在很多种语言中都支持,这种是Java底层的机制,实际上Java的平台无关性很大程度上就是在不同的平台上调用不同的native方法实现对操作系统的访问的。正是因为非Java接口所以native方法是由操作系统实现的,Java虚拟机只进行调用就可以了。

Java堆(heap)

  1. Java堆作为线程共享的数据区,所以它作为JVM所管理的最大的最大的一块内存,它可以供几乎所有类实例对象与数组对象分配内存,但是如果无节制的大量创建对象,那么将会消耗完内存空间,所以Java堆也是OOM异常出现的主要发源地;
  2. 由于Java堆的特点,Java堆也是垃圾收集器主要管理的区域,所以也被成错Garbage Collected Heap(GC堆);
  3. 堆内存既可以固定大小也可以动态调整,但是通常情况下,在服务器运行的过程中,堆内存不断的进行扩容或收缩形成了不必要的系统压力,所以一般在线上生产环境中会给堆内存设置固定大小,避免了在GC后调整堆内存大小所带来的不必要的压力
  4. 从GC的角度来说,现在的垃圾收集器一般均采用分代垃圾收集算法,所以java堆又分成了新生代和老年代的空间。

在Java堆中新生代和老年代之间的运作关系很有意思,这里着重的说一下:

新生代:1个Eden(伊甸园)区+2个Survivor(幸存者)区;其中绝大部分的对象的生成是在Eden区,当Eden区已满后将会触发GC垃圾回收机制,经过GC垃圾回收后只有少量对象可以存活(被引用的对象)然后被复制到其中的一个Survivor区,每次GC的时候,将存活的对象复制到未使用的那块Survivor空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态,如果YGC要移送的对象大于Survivor区容量上限,则直接移交给老年代。并且重要的是对象是不可以在两个Survuvir区一直交换的,因为每进行一次交换就要将对象的1个计数器的值+1,也就是“年龄”+1,当年龄到达某个阀值的时候就会自动进入老年代

老年代:在新生代经历了多次GC回收后仍然存活的对象都放在了此区域,这个区域存放的对象存活率非常高,所以对于老年代的GC机制又被称为Major GC(老年代的垃圾回收),MGC通常使用的算法是“标记-清理”或者“标记-整理”的算法,其中整堆回收的机制也被成为Full GC(包括新生代)

永久代(Perm区):故名思议这里主要存放的是例如Class、Method的元信息,由于GC主要回收的是Java对象所以这里相对于Young Generation和Old Generation来说受GC影响是很小的(JDK 1.8之后此区域已经被替换成了物理内存中的元空间区域

方法区

方法区用于存放已经被虚拟机加载的类的信息、常量、静态变量、JIT编译后的代码等等,虽然它在逻辑上也是堆的一部分,但是方法区还有一个别名叫做Non-Heap(非堆),目的就是为了与堆区分开来。对于它存放的这些数据的特点简单的虚拟机实现可以不在这个区域进行垃圾收集,方法区和堆一样在实际内存中的存储可以是不连续的,这个区域存放的数据一般都长期存在,在这个区域垃圾回收主要也就是对于常量池的回收和类型的卸载,也同堆一样可以根据需求动态扩充和收缩内存大小也可以固定大小,如果内存不足也将抛出OutOfMemory异常。

直接内存

虽然直接内存并不是实际JVM运行时内存区的一部分,他是利用了本地方法在Java堆之外申请的内存区域,但是这部分内存也将会被频繁的使用并且也会抛出OOM异常,JVM通过一个存储在Java堆中的 DirectByteBuffer对象作为这块内存的引用并进行操作,这样一来,就可以避免在Java堆和Native堆之间频繁的复制数据,就能在一些场景中显著提高性能。

运行时常量池

运行时常量池是JVM内存模型方法区中的一部分,java文件被编译之后生成的class文件中除了包含:类的版本、字段、方法、接口等信息外,还有一项就是常量池,在类和接口被加载到虚拟机中之后对应的运行时常量池也就被创建了出来,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放。运行期间也可能将新的常量放入池中。当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。它也有可能抛出OOM异常。

如果觉得还不错的话,把它分享给朋友们吧(ง •̀_•́)ง