第二章 JAVA 内存区域与内存溢出异常
2.2 运行时的数据区域
程序计数器
- 可以看做当前线程所执行的字节码的行号指示器。概念上,字节码解释器工作时通过改变这个计数器的值来选取下一条要执行的字节码指令。
- 每条线程都需要一个独立的程序计数器,各条线程计数器互不影响独立存储。
- 如果线程执行的是JAVA方法,则计数器记录的是虚拟机字节码指令地址;如果执行的是native方法,则计数器值为空(undefined)
- 此区域是唯一没有OOM(out of memory)错误的区域
JAVA虚拟机栈
- 虚拟机Stack也是线程私有的。生命周期与线程相同
- 描述的是JAVA方法执行的内存模型: 每个方法在执行的同时都会创建一个栈帧(stack frame)用于储存局部变量表,操作数栈,动态链接,方法出口等信息。方法执行的过程就是栈帧入栈到出栈的过程。
- 局部变量表:存储了primitive type, reference和return address(指向了一条字节码指令的地址)
- long 和 double是64位的,会占用两个slot, 其余的都是1个slot。局部变量表需要的内存是在编译期完成分配的。当进入一个方法时,这个方法需要在帧中分配多大局部变量空间是确定的。运行期间,局部变量表大小不会变的。
- 本区域会出StackOverFlow(栈深度过大),和OOM(扩展时内存不够)异常
本地方法栈
- 专门为native方法服务的,作用和虚拟机栈相似。
JAVA堆
- 被所有线程共享的一块内存。在JVM启动时创建。唯一的目的是存对象实例。
- 堆是垃圾收集的主要区域
- 堆可以处在物理上不连续的内存空间中,只要逻辑连续即可。扩展配置(-Xmx最大, -Xms最小)。堆无法再扩展的时候会抛OOM异常。
方法区
- 也是线程共享的,存储已被虚拟机加载的类的信息,常量,静态变量,即时编译期编译后的代码等数据。
- 除了和堆一样不需要连续的内存和可以固定大小或者可扩展外。还可以选择不实现垃圾收集。会报OOM异常
运行时常量池
- 是方法区的一部分。 Class文件中的一项信息是常量池。用于存放编译期生成的各种字面量和符号引用。这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 对于运行时常量池,虚拟机规范没做任何细节要求。
- 运行时常量池相比于class文件的常量池的另一个重要特征是动态性。运行期间也能将新的常量放入池中
- 受方法区内存限制。会OOM
直接内存
- 不是JVM运行时数据区的一部分。例如NIO用native方法直接分配堆外内存。然后用Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作
- 受总内存限制
2.3 HotSpot虚拟机对象探秘
对象的创建
- 虚拟机遇到一条new指令时,首先检查这个指令的参数是否能定位到一个类的符号引用。并检查这个类是否已经加载,解析和初始化过,如果没有,先执行类的加载过程
- 类加载检查通过后,JVM为新生对象分配内存。类加载完之后,对象的内存大小就定下来了。为对象分配空间等同于把一块确定大小的内存从JAVA堆中分出来。
- 假设堆内存是规整的,有用和空闲的内存中间放个指针当分界点。则挪动指针就可以为对象分配空间。这种方式教指针碰撞。
- 如果内存不是规整的,JVM需要维护一个空闲列表,来记录那块内存可用。
- 线程安全的解决方案: 1. 对于分配内存空间的操作进行同步 。2.每个线程在Java堆里预分配一小块内存,成为本地线程分配缓冲(TLAB)。当TLAB用完时,才需要同步锁。 配置方法: -XX:+/-UseTLAB
- 内存分配完之后,JVM将分配的内存空间初始化成零(不包括对象头),如果使用TLAB,这一步也可提前至TLAB分配是进行。这一步保证字段有初始值。
- 接下来,JVM对对象的进行必要的设置,如这个对象是哪个类的实例,HASHCODE等。这些信息放在对象头中。
- 执行new 之后会继续执行<init>方法, 按照代码进行初始化。
对象的内存布局
- 对象分三块, 头(header), 实例数据(instance data),对齐填充(padding)
- header包括两部分信息。一部分存储对象运行时数据如HashCode, GC分代年龄,锁的标志,线程持有的锁,偏向线程ID,偏向时间戳等。这部分是Mark Word。Mark Word数据结构不固定。另一部分是类型指针。如果是数组,还要记录数组长度。
- 实例数据存储顺序受虚拟机分配策略参数和字段定义的顺序的影响,并且相同宽度的字段会被排到一起。父类的变量会安排到子类之前。如果CompactFields设为true。则子类的较窄的变量会插入到父类变量的空隙中。
- 对齐填充是保证对象起始地址必须是8字节的整数倍。
对象的访问定位
- 通过栈上的reference来操作堆上的对象。
- 句柄访问, JAVA堆中会分出一块内存作为句柄池。然后在reference中存储对象的句柄地址。对象被移动时不用改reference
- 直接指针访问。reference直接存对象地址。节省一次指针定位开销。
2.4 OOM异常
Java堆溢出
- 不断创建对象,超过容量限制。
- 将堆的最小值 -Xms与最大值 -Xmx设置为一样可以避免自动扩展。
- 用参数 -XX:+HeapDumpOnOutOfMemoryError。可以让JVM在出现内存溢出时Dump出当前内存对转储快照以便分析。
- 错误OutOfMemoryError: Java heap space
- 如果是内存泄露。用工具查看GC Roots引用链。看看为什么无法回收。
- 如果内存中的对象确实都还活着。看看是否有些对象生命周期超长。
虚拟机栈和本地方法栈溢出
- HotSpot虚拟机不区分虚拟机栈和本地方法栈
- 栈容量是由 -Xss参数设定。
- 栈深度过大: StackOverFlow; 内存不够: OOM
- 单线程下,这两种情况一般都抛StackOverFlow
- 操作系统对每个进程的内存是有限制的, 2GB - Xmx - MaxPermSize = 虚拟机栈 + 本地方法栈
方法区和运行时常量池的溢出
- String.intern()是一个native方法,如果字符串常量池已经存在这个值,则返回池中的,否则将此值放入常量池,并返回此String对象的引用。Java1.7的intern()不会复制实例。
- 方法区存放class相关信息,如方法名,字段描述,类名等。生成太多类可能导致此区域溢出
本机内存直接溢出
- DirectMemory可以通过-XX:MaxDirectMemorySize指定,默认与 -Xmx一样。
- 用DirectByteBuffer类分配内存抛异常的时候并没有真的去申请分配内存。而是计算出来得知内存无法分配
- 真正申请分配内存的方法是 unsafe.allocateMemory
- DirectMemory导致内存溢出,明显特征是Heap Dump文件不会出明显异常。
网友评论