第二章 Java内存区域与内存溢出异常
一、运行时数据区域
1.程序计数器(线程私有)
- 定义:是一个逻辑概念,可以看做是当前线程所执行的字节码的行号指示器。
- 目的(作用):线程切换后能恢复到正确的执行位置(字节码解释器工作时就是用过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都需要依赖这个程序计数器)。
-
异常:
- Java方法:计数器记录的是虚拟机字节码指令地址。
- Native方法:这个计数器的值为空,这个内存区域是唯一一个Java虚拟机规范中没有规定任何OOM的区域。
2.Java虚拟机栈(线程私有)
- 定义:Java方法执行的内存模型,每个方法执行同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法调用至执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈出栈的过程。
- 局部变量表作用:存放了编译期可知三块内容①基本数据类型②对象引用(可能是直接指向对象的指针或者句柄)③returnAddress类型(执行一条字节码指令地址)。
- 注意:64位长度的long和double占用了2个局部变量空间,其余的占用1个。局部变量表所需的内存空间在编译器完成分配,运行期间不会改变大小。
- 异常:如果线程请求深度大于虚拟机所允许的深度,抛出StackOverflowError异常,虚拟机栈可以动态扩展时,若扩展时申请不到足够的内存,将抛出OOM异常。
3.本地方法栈(线程私有),HotSpot虚拟机将虚拟机栈和方法栈合二为一
顾名思义,本地方法栈为Native方法服务,抛出异常通虚拟机栈。
4.Java堆
- 定义:虚拟机规范中规定:所有的对象实例和数组都要在堆上面分配。
-
内存回收角度
-
新生代
- Eden
- From Survivor
- To Survivor
- 一般情况下上述三个的内存比为: 8:1:1
- 老年代
-
- 内存分配角度:可能有多个线程私有的分配缓冲区(TLAB:Thread Local Allocation Buffer)。
- 异常:虚拟机规范规定:Java堆可以处于物理不连续的内存空间,只要逻辑上连续就可以;当堆无法再扩展时,OOM。
5.方法区
- 作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
- 注:虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫Non-Heap(非堆)。
-
运行时常量池
- 作用:存储编译器生成的各种字面量和符号引用。
- 一个重要特征:动态性。运行期间也可以将新的常量放入常量池中,比如String的intern()方法。
6.执行引擎
7.本地库接口
- 注:直接内存并不是虚拟机运行时数据区域的一部分,也不是虚拟规范中定义的内存区域,但是这个地方也有需要关注,如:JDK1.4引入的NIO,可以使用Native函数库直接操作堆外内存,所以在配置虚拟机参数时,也需要注意直接内存。
二、HotSpot虚拟机对象探秘
1.对象的创建
- ①类加载检查:当虚拟机遇到一条new指令时,先检查这个指令的参数能否在常量池中定位到一个符号的引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过,没有则必须先执行类加载过程(第七章介绍)。
-
②为新生对象分配内存:等同于把一块确定大小的内存从Java堆中分离出来,对象所需内存大小在类加载完便可完全确定。
-
分配内存两种方式:
- ①指针碰撞:前提为Java堆中的内存是绝对规整的,占用的放在一边,未占用的放在另一边,中间放着一个做份分界点的指示器,这样分配内存就是把这个指针向空闲区域移动一个对象内内存大小的位置。如:Serial、ParNew等带有Compact(兼容)过程的收集器。
- ②空闲列表:如果堆中的内存不够规整,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配时找到足够大的一块空间分配给对象,并更新表。如:CMS这种基于Mark-Sweep算法的收集器。
- 解决分配和并发修改内存时可能遇到的问题:①采用CAS失败重试的方式保证更新操作的原子性②把分配的动作按照线程划分在不同的空间之中进行;即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB,Thread Local Allocation Buffer)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。是否使用TLAB,可用 -XX:+/-UseTLAB 参数设定。
-
- ③将分配到的内存空间都初始化为零值(不包括对象头):类的元数据信息、对象的实例数据、对象的GC分代年龄等信息都在对象头中。
- ④执行对象的init()方法(因为此时对象内的字段还没有值)。
2.对象的内存布局
-
①对象头
- 可分为两部分,第一部分:存储对象运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
- 第二部分:类型指针
即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例,当然不是所有的都如此。如果对象是Java数组,对象头中要有记录数组长度的数据,因为虚拟机通过POJO对象的元数据信息确定Java对象的大小,数组的元数据中无法确定数组大小。
-
②实例数据
- 记录父类继承和子类定义的数据,顺序受虚拟机分配策略参数,Java源码在类中定义的顺序影响。
- 默认分配策略longs/doubles,ints,shorts/chars,bytes/booleans,oops(Ordinary Object Pointers)。
-
③对齐填充
- 非必然存在。HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,即对象大小必须是8字节的整数倍。当对象实例部分数据没有对齐时,需要通过对其填充来补全。
3.对象的访问定位
Java需要通过栈上的reference来操作堆上的对象,取决于虚拟机实现,驻留访问方式有两种:
- 句柄:Java堆中分出一块内存作为句柄池,reference中存储对象的句柄地址,句柄中包含对象实例数据与类型数据各自的具体地址信息。
- 直接指针:reference中存储对象地址,此方式必须考虑如何防止访问类型数据的相关信息。
- 句柄的好处:对象被移动(垃圾收集时)指挥改变句柄中的实例数据指针,不会改变reference。
直接指针的好处:少了一次定位开销,速度更快。
两种方式都很常见,HotSpot使用直接指针。
三、实战:OutOfMemoryError异常
1.Java堆溢出
不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象。爆出异常后,会接着提示Java heap space。
2.虚拟机栈和本地方法栈溢出
- 虚拟机规范中描述了两种异常:①线程请求的栈深度大于虚拟机所允许的最大深度,StackOverflowError异常②虚拟机在扩展栈时无法申请到足够的内存空间,抛出OutOfMemoryError异常。本质上是对一件事情的两种描述。
- 单线程下,栈帧太大和虚拟机栈绒料太小都会报出StackOverflowError异常。
- 如果是建立过多线程导致内存溢出,可通过嫌少最大堆和减少栈容量来换取更多线程。
3.方法区和运行时常量池溢出
一个类要被垃圾回收掉,判定条件是比较苛刻的。在经常动态生成大量Class应用中,需要特别注意类的回收状况。
具体场景:动态代理。增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。
异常区域:PermGen space
4.本地直接内存溢出
- 可通过-XX:MaxDirectMemorySize指定,如果不指定,默认与Java堆最大值(-Xmx)一样。
- 异常:这里的内存溢出有一个明显的特征 是Heap dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或者间接使用了NIO,可以考虑下是不是这方面的原因。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。