本文已参与「新人创作礼」活动,一起开启掘金创作之路。
JVM的内存模式
JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。
线程共享的: 堆、方法区、
线程私有的: 程序计数器、虚拟机栈、本地方法栈
-
元空间(方法区) :
本质和永久代类似,都是对JVM规范中方法区的实现。
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。在jdk1.8以前,称为永久代,在1.8以后,称为元空间MateSpace,不由具名管理它的内存结构,而是交给操作系统内存,元空间使用的系统内存,元空间的串池StringTable被移到了堆内存中。线程共享,方法区逻辑上是堆的一部分,方法存储了更类结构相关的一些信息,比如常量,类变量,类的构造器,方法的信息,成员方法和构造方法,编译器编译后的代码等等,方法区如果内存不足也会报内存溢出。 -
虚拟机栈:
每个线程都有一个私有的栈,随着
线程的创建而创建,生命周期与线程相同。栈里面存着的是一种叫“栈帧”的东西,每个方法都会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息,栈帧与数据结构的栈类似,都是先进后出的数据结构,只支持出栈和入栈操作。栈的大小可以固定也可以动态扩展。存在栈内存溢出的问题;栈的大小是固定的,可能由于方法的递归调用没有设置正确的方法结束的调教导致,栈一直入栈没有出栈,使得最后栈帧的内存大小超过栈的内存;栈帧过大导致内存溢出;
-
本地方法栈:
与虚拟机栈类似,区别是虚拟机栈执行Java方法,本地方法执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。
-
程序计数器:
程序计数器是一块
较小的内存空间,可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。字节码解释器通过
改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为“
线程私有”内存。在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次的运行位置。程序计数器是唯一不会出现
OutOfMemoryError的内存区域,生命周期随着线程的创建而创建,随着线程的结束而死亡。仅仅只是一个运行指示器,存储下一个待执行的命令的地址。无论代码多少,在最坏情况下死循环也不会让这块内存超限。 -
堆内存:
堆内存是 JVM 所有
线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过GC(垃圾回收器)进行回收。当申请不到空间时会抛出OutOfMemoryError。堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。jdk1.8后,字符串常量池从永久代中剥离出来,存放在队中。 -
直接内存:
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了
NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。