JVM内存模型
JVM内存模型又叫运行时数据区,根据JVM规范,JVM内存共分为堆,虚拟机栈,方法区,本地方法栈,和程序计数器五个部分。
1、线程私有:虚拟机栈,本地方法栈,程序计数器
2、线程共享:堆,方法区,直接内存
堆
堆是Java虚拟机所管理的内存最大的一块。堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一的目的存放对象实例。几乎所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,因此也被称为 GC堆(Garbage Collected Heap)。
- 从内存回收的角度看,由于现在垃圾收集器基本都采用分代垃圾收集算法,所以Java堆又分为:新生代和老年代。其中新生代又分为:Eden区,From Survivor,To Survivor区。进一步划分的目的是更好的回收内存,或更快的分配内存。分代回收是基于:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。
- 从内存分配的角度看,线程共享的Java堆中可能会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
PS:可以通过-Xms,-Xmx 分别控制堆初始化时最小堆内存和最大堆内存的大小。
Java虚拟机栈
Java虚拟机栈是线程私有的,它的生命周期和线程相同,描述的是Java方法执行的内存模型。Java虚拟机栈是由一个个栈帧组成,线程在执行一个方法时,便会向栈中放入一个栈帧,每个栈帧中都包括局部变量表,操作数栈,动态链接,方法出口等信息。方法的执行就对应着栈帧在虚拟机中入栈和出栈的过程;栈里存放着各种基本类型和对象的引用。
局部变量表主要存放了各种基本数据类型(boolean,byte,char,short,int,float,long,double)和对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
Java虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError
- StackOverFlowError :若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
- OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
方法区(JDK 1.8后该区域被抛弃)
方法区与Java堆一样,是各个线程所共享的,它用来存储已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。
方法区是JVM提出的规范,而永久代就是方法区的具体实现。Java虚拟机对方法区的限制非常宽松,可以像堆一样不需要连续的内存,还可以选择不实现垃圾收集,所以垃圾收集行为在方法区是比较少出现的。
在方法区会报出 永久代内存溢出的错误。JDK1.8为了解决这个问题,提出了meta space(元空间)的概念,就是为了解决永久代内存溢出的情况,一般情况下,在不指定元空间大小的情况下,虚拟机方法区内存大小就是宿主主机的内存大小。
本地方法栈
与虚拟机栈发挥的作用类似,它们之间的区别是:虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机使用到的native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverFlowError和OutOfMemoryError异常。
程序计数器
程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等功能都需要依赖程序计数器来完成。
Java虚拟机的多线程是通过线程轮流切换并分配CPU的时间片的方式实现的,因此在任何时刻一个处理器(如果是多核处理器,则只是一个核)都只会处理一个线程,为了线程切换后能恢复正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,因此这类内存区域为“线程私有”的内存。
程序计数器的作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行,选择,循环,异常处理
- 在多线程的情况下,程序计数器用于记录当前现场执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪了。
PS:程序计数器是不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)。当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
JDK1.7及之后的版本的JVM已经将运行时常量池从方法区中移了出来,在java堆中开辟了一块区域存放运行时常量池。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但这部分内存也被频繁的使用。而且也可能导致OutOfMemoryError异常出现。
JDK1.4中新加入的NIO类,引入了一种基于通道(Channel)与缓存区(Buffer)的IO方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样就能提高性能,因为避免了在java堆和Native堆之间来回复制数据。
本机直接内存的分配不会收到Java堆的限制,但是既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。