Java内存模型
一. 程序计数器(线程私有)
程序计数器是一块较小的内存空间,主要代表当前线程所执行的字节码的行号指示器。
字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要这个计数器来完成。
主要作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而保证线程切换后能恢复到正确的执行位置。
注意:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
二. 虚拟机栈(线程私有)
虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始位置的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了1条字节码指向的地址)。
虚拟机栈会出现的两种错误
- StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError错误。
- OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError错误。
三. 本地方法栈(线程私有)
与虚拟机栈的作用相似,区别:虚拟机栈为虚拟机执行Java方法,而本地方法栈则为Native方法。
四. 堆(线程共享)
Java虚拟机所所管理的内存最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,因此也被成为GC堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本采用分代垃圾收集算法,所以Java堆还可以分为:新生代、老年代;再细致一点的有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
堆内存划分
在JDK1.7版本及之前的版本,堆内存通常分为以下三部分
- 新生代(Eden、From Survivor、To Survivor)
- 老年代
- 永久代(方法区)
JDK1.8版本后方法区(HotSpot的永久代)被彻底移除了,取而代之是元空间。
OutOfMemoryError错误表现
Java堆最容易出现的错误就是OutOfMemoryError,表现形式如下:
-
OutOfMemoryError: GC Overhead Limit Exceeded当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
-
java.lang.OutOfMemoryError: Java heap space假如在创建新的对象时,堆内存的空间不足以存放新创建的对象,就会引发此错误。
五. 方法区(元空间,线程共享)
方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做非堆(Non-Heap),目的应该是与Java堆区分开来。
注意:JDK1.8将方法区(HotSpot的永久代)被彻底移除(实际上JDK1.7已经开始了),取而代之是元空间,元空间使用的是直接内存。
运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译器生成的各种字面量和符号引用)。
注意:JDK1.7及之后的版本,JVM将运行时常量池从方法区中移了出来,在Java堆中开辟了一块区域存放运行时常量池。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError错误。
本机直接内存的分配不会收到Java堆的限制,但是会受到本机总内存大小以及处理器寻址空间的限制。
方法区和永久代的关系
《Java虚拟机规范》只是规定了方法区的概念以及作用,并没有规定如何实现,因此在不同的JVM中,方法区的实现则不同。永久代是HotSpot虚拟机对《Java虚拟机规范》中方法区的一种实现方式,其他的虚拟机则不存在永久代的概念,两者就像Java中接口和类的关系,类实现了接口。
常用参数
/* JDK1.8之前的版本 */
-XX:PermSize=N // 设置方法区初始空间
-XX:MaxPermSize=N // 设置方法区最大空间
/* JDK1.8及之后的版本 */
-XX:MetaspaceSize=N // 设置元空间初始(最小)空间
-XX:MaxMetaspaceSize=N // 设置元空间最大空间
参考文献
深入理解Java虚拟机