JVM运行时的数据区域划分图如下,该图是JVM内存模型最主要的内容。
从图中可以看出来,JVM将内存主要划分为五个部分:程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区。这些被划分为用途不一的数据区域有着各自的特点,它们都有自己创建和销毁的时间,有的区域随着进程的启动而存在,有的是伴随着用户线程的启动而建立、随着线程的结束而销毁。
(一) 程序计数器(Program Counter Register)
1> 内存空间较小;
2> 当前线程所执行的字节码行号指示器;
功能:a、在JVM中,字节码解释器的工作进行就是通过改变计数器的值来选取下一条需要执行的字节码指令
b、在3>中有解释,是作为线程切换回来继续执行的标记点
3> 线程私有;
线程私有:通俗来讲就是每一个线程在执行时都有一个独立的程序计数器,各个线程被独立存储。目的是为了记录所执行的线程进行到了哪一步。这样设计的原因主要是由多线程的实现方式所决定的,在Java虚拟机中,多线程的实现是通过线程轮流切换并分配处理器执行时间的方式,也就是在任何一个具体的时刻下,一个处理器只能够执行一个线程下的代码指令,这样在不断的线程切换过程中,同一个线程想要继续往下执行,就必须知道它上一次执行的位置,这个工作就是通过程序计数器完成的。
4> 计数器的值;
若线程正在执行的是Java方法:正在执行的虚拟机字节码指令的地址;
若线程正在执行的是Native方法:空值(Undefined);
Native方法(本地方法):为了补充Java语言无法直接访问系统底层的一种方法,由C或C++语言完成,实际上Native Method就是Java调用非Java代码的接口。
5> 计数器这块内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
6> 生命周期与线程相同。
(二)Java虚拟机栈(Java Stack)
1> 定义:描述Java方法执行的内存模型;
a、详细解释:每个方法执行时都会创建一个栈帧,在这个栈帧中存放着“局部变量表、操作数栈、动态链接、方法出口等”。
以我的理解就是这一块内存就是一个栈,栈帧是Java虚拟机栈的诸多元素,每一个栈帧都代表这一个方法正在被执行,其中存放着一些有关该方法中信息。
每一个方法从调用一直到执行结束,也就对应着一个栈帧在Java虚拟机栈中入栈出栈的过程。
b、局部变量表:
基本数据类型、(64位的long和double类型的数据会占用2个局部变量空间,其余占1个)
对象引用、(reference类型??,不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能 是指向一个代表对象的句柄或其他与此对象相关的位置)
returnAddress类型 、(指向了一条字节码指令的地址)
局部变量表所需的内存空间在编译期间完成分配,所以在线程进行时调用了一个方法,为之创建的栈帧中局部变量空间是完全确定的,并且在方法执行过程中不会改变局部变量表的大小。
2> 线程私有;
线程私有: 也就是和计数器一样,每一个线程有独立的Java虚拟机栈,这个线程中的每个方法执行过程就会创建一个栈帧,方法执行的全过程就是栈帧在该线程对应的虚拟机栈中入栈和出栈的过程。
3> 生命周期与线程相同;
4> 该内存区域两种异常状况:
a、线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
b、若虚拟机支持动态扩展,如果扩展时无法申请到做够的内存,就会抛出OutOfMemoryError异常。
StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。
而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。
5> 不能简单地将Java内存区简单分为堆和栈。
这样区分太过粗糙,只不过是将对象和局部变量区分开,简单地认为对象存放在堆中,局部变量存放在栈中。其实从上面的整理中也可以看出来,Java虚拟机栈实际上是由这些栈帧组成的,而每个栈帧中的内容并不只有局部变量表。
(三)本地方法栈
1> 定义:与Java虚拟机栈的定义很相似,可以说是Native方法执行的内存模型;
2> 虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定。甚至有的虚拟机(Sun HotSpot)直接就把本地方法栈和虚拟机栈合二为一;
3> 抛出StackOverflowError和OutOfMemoryError异常的情况与Java虚拟机栈是一样的;
4> 其他特性参考Java虚拟机栈。
(四)Java堆
1> 系Java虚拟机所管理内存中最大的一块,虚拟机启动时创建;
2> 被所有线程共享;
所有地线程都可以访问不同地对象,从内存分配地角度来看,线程共享地Java堆中可能划分出多个线程私有地分配缓冲区(TLAB),不过无论如何划分,无论哪个区域,存放地都是对象实例,这样不同地线程将各自地对象实例放在了看似共享的Java堆的各自的缓冲区上,这样的好处是可以更好的回收内存,也可以更快分配内存。
3> 唯一目的就是存放对象实例,几乎所有实例对象都存在此处;
在Java虚拟机规范中描述为:所有的对象实例以及数组都要在这里分配内存。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所以这里会严谨地加上“几乎所有”而不是全部,至于什么是逃逸分析,栈上分配,标量替换,以后学习中应该会专门去了解学习一下吧。
4>可以处于物理上不连续的内存空间,但逻辑上需要连续;
5> 可以实现固定大小的,也可以是可扩展的。若在堆中没有内存完成实例分配,并且堆也无法再扩展时,将抛出OutOfMemoryError异常;
6> 又称“GC堆‘(Garbage Cpllected Heap)
内存回收用到的终极算法:分代收集算法,今后可以专门学习一下,该算法也是多个算法的整合。
(五) 方法区(Method Area)
1> 与Java堆类似,各线程共享的内存区域;
2> 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等;
3> 规范中描述为堆的一个逻辑部分,习惯称为Non-Heap(非堆),目的是为了与Java堆区分;
4> 被称作“永久代”;
本质上两者并不一样,这样的叫法是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样GC收集器就能像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。当然方法区也可以不进行GC收集。
5> 不需要连续的内存和可以选择固定大小或者可扩展外,也可以不实现GC收集;
该区域的内存回收目标主要是针对常量池的回收和对类型的卸载,但效果不好,条件苛刻,但是对于这部分的回收确实是有必要的。
6> 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
(六) 运行时常量池
1> 方法区的一部分;
Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译时生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
2> 受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常;
(七) 直接内存
1> 不属于虚拟机运行时数据区的一部分,也不是规范中的定义;
2> JDK1.4加入了NIO类,一种基于通道与缓冲区的新I/O方式,NIO可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为直接内存的引用来操作直接内存,这样可以避免在Java堆和Native堆来回复制数据,从而提高性能;
3> 受总内存影响,也会出现OutOfMemoryError异常。
以上总结的就是JVM内存模型的相关概念和知识点,前五项以及那张内存模型图需要牢记,其中有些许疑问,不过也都是某些概念上的问题,具体关于每一块内存区域的定义作用,抛出的异常类型,已经理解。
————————————————
版权声明:本文为CSDN博主「Adelaide_Guo」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Adelaide_Guo/article/details/75394973
原文地址:https://www.cnblogs.com/yss818824/p/12267689.html