一 Java内存模型
Java虚拟机在执行程序的过程中会把它管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
根据《Java虚拟机规范(Java SE 7版)》分为如下几个运行时数据区域。
其中"绿色"部分为线程共享数据区域,"黄色"部分为线程隔离的数据区,也即为线程私有内存。
二 线程私有内存
Java虚拟机的多线程是通过线程轮流切换并分配cpu(处理器)执行时间的方式实现的,在任何一个确定时刻,一个cpu(对多核处理器来说是一个内核)都只会执行一条线程中的指令。cpu切换的速度非常之快,让我们觉的它是在进行多线程运行,本质上它还是单一运行的。
在java线程中,每一个线程都有自己独立的内存,主要包括序计数器、虚拟机栈、本地方法栈,也即是线程私有内存,就是内存模型图中"黄色"部分,主要作用是维持线程的安全、稳定、高效的运转。
注意:
线程私有不存在多线程并发的资源竞争问题,因为其享有的内存是互不影响的,不存在并发问题。
与线程私有相对的是线程公有内存,也叫主内存,这一部分内存是线程共享的,也就是图中"绿色"部分,
这一部分内存会出现多线程并发"资源竞争"问题。
三 程序计数器
程序计数器为线程私有的一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
在虚拟机的概念模型里,字节码解释器工作时就是改变程序计数器的值来获取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成。比如,线程切换后能够恢复到正确的执行位置,是因为每一条线程都有独自的程序计数器,各线程之间计数器又是互不影响,独立存储的,所以线程切换才能恢复到切换前的正确执行位置,因为不会有别的线程对上次执行的位置做修改。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值则为空(undefined)。
该区域是Java虚拟机中唯一一块不会出现内存溢出异常(OutOfMemoryError)的区域。
总结:
1)程序计数器是线程私有较小内存空间,是程序运行的指示灯,没有它,程序就没法运行了。
2)该区域是java虚拟机中唯一一块不会出现内存溢出异常的区域。
四 Java虚拟机栈
Java虚拟机栈与程序计数器一样属于线程私有内存,其生命周期与线程生命周期相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的时候会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的8大基本类型(byte,short,int,long,float,double,char,boolean)、
对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表的句柄或其他与此对象相关的位置)和returnAdress类型(指向了一条字节码指令的地址)。
其中64位长度的long和double类型数据占用2个局部变量空间(Slot),其余数据类型占用1个。
局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法所需要在栈帧中分配多大的局部变量空间是确定的,在方法运行期间不会改变局部变量表大小。
注意,该区域可能会出现两种异常情况:
1)如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常。
2)如果虚拟机可以动态扩展,当扩展时无法申请到内存,抛出OutOfMemoryError异常。
总结:
1)虚拟机栈生命周期与线程生命周期相同。
2)每个方法在执行的时候会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表所需要的内存空间在编译期间完成分配,存储基本类型、reference、returnAddress。
3)会抛出StackOverflowError和OutOfMemoryError异常。
五 本地方法栈
本地方法栈与虚拟机栈十分相似,两者的区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用本地方法服务。
注意:本地方法栈同样也会抛出StackOverflowError和OutOfMemoryError异常。
六 Java堆
Java堆是Java虚拟机所管理的内存中最大的一块,所有线程共享区域,在虚拟机启动时创建。
该区域用于存放对象,当new一个对象或数组都在这里分配内存。基本上所有的对象实例都存在这里,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些,微妙的变化发生,所有对象都分配在堆上变得不是哪么绝对。
Java堆是收集器的主要管理区域。由于现在收集器基本都采用分代收集算法,Java堆可以细分为新生代、老年代;新生代再细一点分为Eden空间、From Survivor空间、To Survivor区域。无论我们如何划分,都与存放内容无关,无论什么区域还是存储的对象实例,划分这么细的根本目的是为了更好回收内存或者更快的分配内存。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。
在实现中,可以实现为固定大小的,也可以是可以扩展的,现在主流的虚拟机都是按照可以扩展来实现的,可以通过-Xmx和-Xms控制堆的最大内存或最小内存。需要注意的是,空间是有限的,当在堆中没有内存完成实例分配,也就是内存不够使了,并且堆也无法扩展时,抛出OutOfMemoryError异常。
总结:
1)堆为虚拟机中内存最大的一块,线程共享,在虚拟机启动时创建。
2)堆内存唯一的目的就是存储对象实例,即new对象和数组都在堆上分配内存。
3)堆内存分为新生代和老年代,新生代又可以分为Eden空间、From survivor空间、To Survivor区域。
4)堆内存可扩展,可以通过参数控制大小。-Xmx 堆最大内存、-Xms 堆最小内存。
5)当在堆中没有内存完成实例分配,也就是内存不够使了,并且堆也无法扩展时,抛出OutOfMemoryError异常。
七 方法区
方法区与堆一样,也是线程共享区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,比如我们在代码中定义的Constant常量就会在这个区域存储。
为了与Java堆区分开,也叫做Non-Heap(非堆)。在java虚拟机规范中,它是属于堆的逻辑部分。
同时在这个区域中,它也会有垃圾回收器工作,这个区域叫做“永久代”,之所以叫做永久代,因为它比新生代和老年代拥有更长的生命周期,但是并不是在这个区域它就会万事大吉了,永久代依然会存在垃圾回收的情况,只不过相对来说较少。该区域可能会抛出OutOfMemoryError异常。
总结:
1)方法区属于线程共享区域,用于存储虚拟机加载的类信息、常量、静态变量、 即时编译器编译后的代码等数据。
2)该区域会抛出OutOfMemoryError异常。
八 运行时常量池
运行时常量池主要是方法区的一部分,class文件除了有类的版本、字段、方法接口等描述信息外,还有一项信息是常量池,用于存储编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区运行时常量池中存放。也可能抛出OutOfMemoryError异常。
九 直接内存
直接内存并不是JVM运行时数据区的一部分, 但也会被频繁的使用: 在JDK 1.4引入的NIO提供了基于Channel与Buffer的IO方式, 它可以使用Native函数库直接分配堆外内存, 然后使用DirectByteBuffer对象作为这块内存的引用进行操作(详见: Java I/O 扩展),这样就避免了在Java堆和Native堆中来回复制数据,
因此在一些场景中可以显著提高性能。显然, 本机直接内存的分配不会受到Java堆大小的限制,但既然是内存,则肯定还是会受到本机总内存大小及处理器寻址空间的限制,因此动态扩展时也会出现OutOfMemoryError异常。
本文来自 CSDN 非原创,只是记录下学习