概述
对于java程序来说,由虚拟机管理内存,在虚拟机自动内存管理的机制下,一旦出现内存泄漏和溢出方面的问题,如果不了解java虚拟机是怎么使用内存的,那么排查错误会非常困难。
1.运行时数据区域
Java虚拟机在执行Java过程中会把管理的内存划分为若干个不同的数据区域。
1.1 程序计数器
程序计数器(处于线程独)占区是一个非常小的内存空间,它可以看成是当前线程所执行的字节码的行号指示器。如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟字节码指令的地址。如果正在执行的是native方法,那么这个计数器的值为空(Undefined)。此区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
1.2 Java虚拟机栈
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的动态内存模型。
栈帧(Stack Frame):用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行过程都对应一个栈帧在虚拟机中入栈到出栈的过程。
局部变量表:存放编译期可知的各种基本数据类型,对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址),局部变量表的大小在编译期便已经可以确定,在运行时期不会发生改变。
两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常;如果扩展时无法申请到足够内存,会抛出OutOfMemoryError异常。
1.3 本地方法栈
本地方法栈为虚拟机执行native方法服务。
与虚拟机一样本地方栈也会抛出StackOverflowError和OutOfMemoryError异常
1.4 Java堆
Java堆(Java Heap)是Java虚拟机所管理内存中最大的一块。Java堆是被所有线程共享的一块区域,在虚拟机启动时创建。存放对象实例。Java堆是垃圾收集器管理的主要区域,采用分代收集算法,所以可以分为新生代(由Eden 与Survivor Space 组成)和老年代。Java堆可以固定大小,也可以扩展(通过**-Xmx** 和 -Xms控制),如果堆中没有内存完成实例分配分配,并且无法扩展时会抛出OutOfMemoryError异常。
1.5 方法区
方法区(Method Area),别名Non-Heap(非堆),各线程共享,用于存储一杯虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区与永久代实际并不等价,对于HotSpot中才有永久代的概念
1.6 运行时常量池
每一个运行时常量池(Constant Pool Table)都在java虚拟机的方法区中分配,用于存放编译生成的各种字面量和符号引用。
例如在Java中字符串的创建会在常量池(方法区中StringTable:HashSet)中进行。
1.7 直接内存
直接内存(Direct Memory)不是虚拟机运行时数据区中的,也不是Java虚拟机中的内存区域。
jdk1.4中增加了NIO,引入基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存(系统内存替代用户内存),提高了性能。
2.HotSpot虚拟机对象
HotSpot VM是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机
2.1 对象的创建
创建对象通常new关键字,而在虚拟机中,首先检查这个指令的参数是否在常量池中定位到一类的符号引用,并检测符号引用的类是否已被加载、解析和初始化过。如果没有,则必须先执行相应的类加载过程。在类检查通过后,虚拟机会为新的对象分配内存(位于堆中),所需内存大小在类加载完成后便可完全确定。
内存分配完成后,虚拟机会将分配到的内存空间都初始化为零值(不包括对象头),然后虚拟机要对对象进行必要设置,例如这个对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息(存在对象的对象头(Object Header))。然后会执行< init >方法,把对象初始化,就此一个对象才算完全产生。
2.2 对象的内存布局
对象头(Header):
- 自身运行时数据(32位~64位 MarkWord):哈希值、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
- 类型指针(什么类的实例)
实例数据(InstanceData):数据实例,即对象的有效信息,相同宽度(如long和double)的字段被分配在一起,父类属性在子类属性之前。
对齐填充(Padding):占位符填充内存
2.3 对象的访问定位
对象的访问定位有两种方式:句柄访问和直接指针访问
句柄访问:Java堆中会划分出一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。一个句柄又包含了两个地址,一个对象实例数据,一个是对象类型数据(这个在方法区中,因为类字节码文件就放在方法区中)。
直接指针访问:引用变量中存储的就直接是对象地址了,在堆中不会分句柄池,直接指向了对象的地址,对象中包含了对象类型数据的地址。HotSpot采用直接定位
2.4 OutOfMemoryError异常
在Java虚拟机规范中,出程序计数器外,虚拟机其他几个运行时区域都会发生OOM异常的可能。
- Java堆溢出:堆存储实例对象,当对象数量达到最大推容量限制后会内存溢出;(-Xms -Xmx)
- 虚拟机栈和本地方法栈溢出:单个线程下,无论是由于栈帧太大还是虚拟机栈内容太小,当内存无法分配时,虚拟机抛出的异常都是StackOverflowError异常;(虚拟机栈-xss 本地方法栈-Xoss)
- 方法区和运行时常量溢出:提示“PerGen space”;(-XX:PermSize -XX:MaxPermSize)
- 本机直接内存溢出: (-XX:MaxDirectMemorySize,不指定与堆大小一致(-Xmx))