一、首先我们先简单了解下jvm的内存结构
(图来源于深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明)
如上图所示,Java程序在运行时会大致分为三个区,也就是我们经常说的堆,栈以及方法区
我先对各部分做一个解释
1.程序计数器
首先,程序计数器在jvm中只占有很小的一部分内存空间,它是用来记录当前线程所执行的字节码的行号的,也就是行号指示器
程序计数器是各线程之间独立存在的,互不影响,每一个线程中都会有一个程序技术器。
2.栈
栈在jvm中分为本地方法栈和虚拟机栈,其实这两个栈的用途基本相似,只不过虚拟机栈是用来描述的是Java方法执行的线程内存模型: 每个方法被执行的时候, Java虚拟机都会同步创建一个栈帧(Stack Frame) 用于存储局部变量表、 操作数栈、 动态连接、 方法出口等信息。而本地方法栈则是用来你描述本地服务的。
在《Java虚拟机规范》 中, 对这个内存区域规定了两类异常状况: 如果线程请求的栈深度大于虚
拟机所允许的深度, 将抛出StackOverflowError异常; 如果Java虚拟机栈容量可以动态扩展, 当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
3.堆
对于jvm来说,堆应该是在整个虚拟机中分量最大的一个区域了,它占据了jvm中大部分的内存空间,也是被各线程之间共享的一个区域。此内存区域的唯一目的就是存放对象实例, Java世界里“几乎”所有的对象实例都在这里分配内存。
而这个堆中也是我们熟知的GC堆(垃圾回收器)。
4.方法区
它和堆一样,在jvm中是被各线程共享的一部分,只不过他所存放的数据是已被虚拟机加载
的类型信息、 常量、 静态变量、 即时编译器编译后的代码缓存等数据。而且它有个别名叫“非堆”,目的也是为了和堆进行区分。
5.运行时常量池
这个部分在图中 并未展示,它属于方法区的一部分,在编译后的class文件中,除了类的版本字段,方法等信息以外,还存在着一个用于存放编译期生成的各种字面量和符号引用的表,就是常量信息表,而这个表就存放在运行时常量池中。
二、虚拟机对象的剖析
对于Java这个面向对象的语言来说,在Java的世界中,万物皆对象
在我们编写代码的时候创建对象无非就是一行代码new关键字,克隆,或者反射
而在我们创建对象的时候,虚拟机内部是怎恶样运行的,这里我们来进行一个简单剖析
当虚拟机在遇到new关键字时,首先会去常量池中找是否有和这个指令参数相对应的类,并且检查这个类是否已经被加载了,如果没有,就会先去加载这个类。
当上面的步骤都完成后,jvm就该给这个对象分配内存空间了,而对象的内存大小则是可以直接确定。在jvm虚拟机中(这里以HotSpot为例),对象的在内存中可以分为对象头,实例数据还有对齐填充三个部分
HotSpot虚拟机对象的对象头部分包括两类信息。 第一类是用于存储对象自身的运行时数据, 如哈希码(HashCode) 、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等, 这部分数据的长度在32位和64位的虚拟机(未开启压缩指针) 中分别为32个比特和64个比特, 官方称它为“Mark Word”。 对象需要存储的运行时数据很多, 其实已经超出了32、 64位Bitmap结构所能记录的最大限度, 但对象头里的信息是与对象自身定义的数据无关的额外存储成本, 考虑到虚拟机的空间效率, Mark Word被设计成一个有着动态定义的数据结构, 以便在极小的空间内存储尽量多的数据, 根据对象的状态复用自己的存储空间。 例如在32位的HotSpot虚拟机中, 如对象未被同步锁锁定的状态下, Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码, 4个比特用于存储对象分代年龄, 2个比特用于存储锁标志位, 1个比特固定为0。
第二部分就是我们需要真正存储的信息
第三部分就是对齐填充,这部分主要就是一些占位符,并没有其他含义
继续说内存分配空间的问题,jvm虚拟机在内存分配空间上有两种方式,一种是“指针碰撞”(适用于内存空间是连续的且绝对规整的情况下)即所有被使用过的内存都被放在一边, 空闲的内存被放在另一边, 中间放着一个指针作为分界点的指示器, 那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。另一种则是“空闲列表”(适用于内存空间不连续) 实现方式为虚拟机维护一个列表, 记录上哪些内存块是可用的, 在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录。
这两种分配方式由虚拟机本身决定。
虚拟机执行到这里,在虚拟机的视角下一个新的对象就已经创建完成了,下一步则是按程序员的意愿进行对对象的初始化(即构造函数)。
三、OutOfMemoryError异常
在jvm中除了程序计数器以外,其他各部分区域皆有可能发生OutOfMemoryError异常(内存不足已异常)也就是我们所说的内存溢出
1.堆溢出
在上面我们说过,对是用来存储对象的区域,所以我们只要不断的进行创建对象并保证对象不被GC回收,就会产生堆溢出异常。要解决这个内存区域的异常, 常规的处理方法是首先通过内存映像分析工具(如Eclipse MemoryAnalyzer) 对Dump出来的堆转储快照进行分析。 第一步首先确认内存中导致OOM的对象是否是必要的, 也就是要先分清楚到底是出现了内存泄漏(Memory Leak) 还是内存溢出(MemoryOverflow) 。
2.栈溢出
由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈, 因此对于HotSpot来说, -Xoss参数(设置本地方法栈大小) 虽然存在, 但实际上是没有任何效果的, 栈容量只能由-Xss参数来设定。 关于虚拟机栈和本地方法栈, 在《Java虚拟机规范》 中描述了两种异常:
1) 如果线程请求的栈深度大于虚拟机所允许的最大深度, 将抛出StackOverflowError异常。
2) 如果虚拟机的栈内存允许动态扩展, 当扩展栈容量无法申请到足够的内存时, 将抛出
OutOfMemoryError异常。
因为HotSpot是不支出栈动态扩展的,所以不会产生OutOfMemoryError异常,只会产生StackOverflowError异常
3.方法区和运行时常量池溢出
——————————————————————————————————————————
资料均来源于《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》