最近在公司遇到了系统的性能效率问题,其中有内存溢出导致的。分析内存溢出的问题需要对
JVM
内存模型有较为清晰的理解,之前自己看书《深入理解Java虚拟机:JVM高级特性与最佳实践》中有对JVM
内存模型详细介绍,不过已经很久记不太清了,趁这个机会自己再次总结下,分享出来共大家参考。
1.JVM的内存模型总体结构
JVM在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。下图是JVM内存模型的整体结构
1.1 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器,多线程时,当线程数超过CPU数量或CPU内核数量,线程之间就要根据时间片轮询抢夺CPU时间资源。因此每个线程有要有一个独立的程序计数器,记录下一条要运行的指令。线程私有的内存区域。如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。
关键点:
- 此内存区是线程私有内存区域
- 此内存区是唯一一个在Java虚拟机规范中没有规定任何
OutOfMemoryError
情况的区域。
1.2 虚拟机栈(VM Stack)
管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError。
使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:
关键点:
- 此内存区是线程私有内存区域
- 在JVM规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的JVM都可以动态扩展),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
1.3 本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言,使用方式与数据结构并没有强制规定,因此虚拟机可以自动的实现它。甚至有的虚拟机(如HotSpot虚拟机)直接就是把本地方法和虚拟机栈合二为一。
关键点:
- 本地方法栈区也是线程使用内存区域
- 本地方法栈区也会抛出StackOverflowError和OutOfMemoryError异常。
1.4 JAVA堆
JAVA堆(Java heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所以线程共享的一块内存区域。虚拟机进行启动时创建。存放所以对象实例和数组。
Java堆是垃圾收集器管理的主要区域,因此很多时候也称作GC堆
因为目前垃圾回收是分代回收的,所以可以将Java堆再细分为新生代和老年代,新生代又可细分为Eden空间,Survivor0(S0,from space)空间,Survivor1空间(S1,to space)。
新生代用于存放刚创建的对象以及年轻的对象,刚创建的对象都放入eden,S0和S1都至少经过一次GC并幸存,如果对象一直没有被回收,生存得足够长,老年对象就会被移入老年代。下图为堆内存结构
关键点:
- Java堆是线程共享的内存区域
- Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时将会抛出OutOfMemoryError异常。
1.5 方法区
方法区(Method Area)与java堆一样,是各个线程的共享区域。用于存放被虚拟机加载的类的元数据信息:如常量、静态变量、即时编译器编译后的代码。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与java堆区分开来
注: 在HotSpot虚拟机上 方法区还有一个别名“永久代(Permanent Generation)”,其实本质上两者并不等价,仅仅是因为HotSpot的垃圾收集器可以像管理java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他的虚拟机,如BEA JRockit,IBM J9来说是不存在永久代的概念的。
关键点:
- 方法区是线程共享的内存区域
- 方法区无法满足分配需求时,也会抛出OutOfMemoryError异常
- jdk1.7的HotSpot中已经把原本放在永久代中的字符串常量池移出。
- 方法区进行垃圾回收的条件是非常苛刻的:回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收
1.6 运行时常量时
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息wait,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,java语言并不要求常量一定只有编译期才能产生,也就是并非预置如Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。其中利用较多的是String类的intern()方法。
关键点:
- 运行时常量池是方法区的一部分
- 当常量池也无法再申请到内存时会抛出utOfMemoryError异常。
想了解更多技术文章信息,请继续关注wiliam.s Blog,谢谢,欢迎来访!
参考资料
《深入理解Java虚拟机:JVM高级特性与最佳实践》·周志明著·第二版
JVM内存模型及垃圾回收算法·CSDN