Java内存区域与内存溢出异常
1.1 运行时数据区域
1.1.1 程序计数器
- 程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 任意时刻,一个处理器都只会执行一条线程中的指令,即每条线程都需要一个独立的计数器,为了线程切换后能恢复到正确的执行位置,使得各线程之间互不影响,独立存储。
- 若正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节指令的地址;若执行的是Native方法,则这个计数器值为空
1.1.2 Java虚拟机栈
- 同程序计数器一样是线程私有的,生命周期与线程相同
- 描述的是java方法执行的内存模型,每个方法被执行时会创建一个栈帧,存储局部变量表,操作数栈,动态链接,方法出口等信息
- 局部变量表存放了编译器可知的各种基本数据类型,对象引用和returnAddress类型,其中64位长度的long和double类型的数据会占用2个局部变量空间,其余类型只占用1个。
- 局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小
- 线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常;扩展时无法申请到足够的内存,抛出OutOfMemoryError异常
1.1.3 本地方法栈
与虚拟机栈所发挥的作用非常相似,区别就是虚拟机栈为虚拟机执行java方法,而本地方法栈则为虚拟机使用到的Nativa方法服务,同虚拟机栈一样,也会抛出StackOverflowError异常和OutOfMemoryError异常
1.1.3 Java堆
- 是Java虚拟机所管理的内存中最大的一块,被所有线程共享,存放绝大部分的对象实例
- Java堆是垃圾收集器管理的主要区域,故被称为“GC堆”
- Java堆可以处于物理上不连续的内存空间中,只要逻辑连续就可以,可以选择固定大小,可拓展
1.1.4 方法区(非堆)
- 与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 也称为“永久代”,HotSpot为了简便回收方法区的内存,即垃圾收集器可以像管理Java堆一样管理这部分内存
- 不需要连续的内存,可以选择固定大小,可拓展,还可选择不实现垃圾收集
- 这个区域回收目标主要是针对常量池的的回收和对类型的卸载,但是效果不佳。
1.1.5 运行时常量池
- 是方法区的一部分,存放编译期生成的各种字面量和符号引用
- 具备动态性,不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,如String类的intern()方法
1.1.6 直接内存
- 不是虚拟机运行时数据区的一部分
- NIO中的一种基于通道与缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存,通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作
1.2 HotSpot虚拟机对象探秘
1.2.1 对象的创建
- 检查这个指令的参数是否能在常量池中定位到一个类的符号应用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。若没有则先执行相应的类加载过程
- 类加载检查通过后,为新生对象分配内存。对象所需内存的大小在类加载完成后便可确定
- 若Java堆中内存是绝对规整的,所有用过的内存放在一边,没有用过的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是吧那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式叫做“指针碰撞”
- 若不是规整的,已使用和未使用的内存相互交错,故虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种方式叫做“空闲列表”
- Java堆是否规整是由所采用的垃圾收集器是否带有压缩整理功能决定的。使用Serial、ParNew等带Compact过程的收集器时,采用的是指针碰撞;使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表
解决频繁分配内存的问题:
- 对分配内存空间的动作进行同步处理
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中先预先分配一小块内存,称为本地线程分配缓存(TLAB)
内存分配后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
- 接下来虚拟机就要对对象进行必要的设置,例如这个对象是那个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,存放在对象头之中
- 执行方法,把对象按照程序员的意愿进行初始化
1.2.2 对象的内存布局
- 对象在内存中存储的布局可以分为三块区域:对象头、实例数据、对齐填充
对象头包括两部分信息:
- 用于存储自身的运行时数据,哈希码、对象的GC分代年龄等信息,这部分数据长度在32位和64位虚拟机中分别为32bit和64bit
- 类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,并不是所有虚拟机实现都必须在对象数据上保留类型指针,即查找对象的元数据信息并不一定要经过对象本身
如果对象是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象得元数据信息确定Java对象的大小,但是从数组的元数据信息无法确定长度
- 实例数据:父类继承,子类定义的都需要记录
- HotSpot虚拟机默认分配策略为longs/double,ints,shorts/chars,bytes/booleans,oops。即相同宽度的字段总是分配到一起
- 若CompactFields参数值为true(默认为true),子类之中较窄的变量也可能会插入父类变量空隙中
- 对其填充并不是必然存在,仅仅起着占位符的作用,因为HotSpot的自动内存管理系统要求对象其实地址必须是8字节的倍数。
1.2.3 对象的访问定位
- 通过栈上的reference数据来操作对上的具体对象
- 使用句柄:Java堆中会划分出一块内存来作为句柄池,reference存储的是对象的句柄地址,句柄中包含了对象实例数据(堆中的对象信息)与类型数据(方法区中的类信息)各自的具体地址信息
- 直接指针访问:reference存储的直接就是对象地址,故堆对象中的布局中应该考虑如何放置访问类型数据的相关信息(方法区中的类信息)
- 句柄访问好处是reference存储的是稳定的句柄地址,对象移动时只会改变句柄中的实例数据指针,reference本身不需改变,使用句柄访问的情况也十分常见。
- 指针访问好处是速度更快,节省了一次指针定位的时间,HotSpot就是使用这种方式进行对象访问的
1.3 OutOfMemoryError异常
1.3.1 Java堆异常
- 实例对象数量到达最大堆的容量限制后就会产生内存溢出异常
- 参数:堆的最小值-Xms 堆的最大值-Xmx 两者设置为一样即可避免堆自动扩展
- 参数 -XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照
- 解决:先通过内存映像分析工具堆Dump出来的堆转储快照进行分析。分析是内存泄漏还是内存溢出
1.3.2 虚拟机栈和本地方法栈溢出
- HotSpot中并不区分虚拟机栈和本地方法栈,故-Xoss(设置本地方法栈大小)基本上无效
- 栈容量由-Xss参数设定
- 线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常;扩展时无法申请到足够的内存,抛出OutOfMemoryError异常
- 单线程下 无论是栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常
- 如果是建立过多线程导致内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程
1.3.3 方法区和运行时常量池溢出
String.intern()是一个Native方法
- JDK1.6中,会将首次遇到的字符串实例复制到永久代中,返回永久代中这个字符串实例的引用
- JDK1.7不在复制实例,只是在常量池中记录首次出现的实例引用
-XX:PermSize -XX:MaxPermSize 限制方法区的大小
1.3.4 本机直接内存溢出
- 可通过-XX:MaxDirectMemorySize指定,默认则与Java堆最大值一样
- 虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但是并没有向操作系统申请分配内存,而是通过计算得知内存无法分配,真正分配内存的方法时unsafe.allocateMemory()
- DirectMemory导致的内存溢出,特征是Heap Dump文件中不会看见明显的异常,Dump文件很小,程序中使用了NIO