Java内存区域与内存溢出异常
本文内容来源于深入理解JAVA虚拟机一书,为方便记忆做的一些整理:
- 运行时数据区域
- HotSpot虚拟机对象
- OOM异常
运行时数据区域
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- Java堆
- 方法区
程序计数器
占用很小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
一个内核在一个确定的时刻只会执行一条线程中的指令,每个线程都有一个独立的程序计数器,它是线程私有的。
如果线程正在执行的是一个Java方法,该计数器记录的是正在执行虚拟机字节码指令的地址;如果是Native方法,计数器值为空。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。
程序计数器不会出现OOM现象。
Java虚拟机栈
生命周期与线程相同,是线程私有的内存区域
每个方法在执行的同时都会创建一个栈帧,用来存放局部变量表、操作数栈、动态链接、方法出口等信息。一个方法的执行对应着该栈针在虚拟机栈中的入栈和出栈过程。
局部变量表存放基本数据类型、对象引用、returnAddress类型。
如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;对于可以动态扩展的虚拟机栈,当无法申请到内存时,抛出OOM异常。
本地方法栈
与Java虚拟机栈不同的是,它为Native方法服务
Java堆
所有线程共享的一块内存区域,是Java虚拟机所管理的内存中最大的一块,用来存放对象实例和数组。
内存回收角度:可以细分为新生代和老年代,更细分为Eden空间、From Survivor空间和To Survivor空间。
内存分配角度:可划分为多个线程私有的分配缓冲区(TLAB)。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。
方法区
所有线程共享的内存区域,存放常量、静态变量、即时编译器编译后的代码和已被虚拟机加载的类信息等
该区域的回收主要是针对常量池的回收和对类型的卸载。
运行时常量池是方法区的一部分,主要用来存放Class文件中描述的符号引用以及翻译出来的直接引用。
类信息包括类名、访问修饰符、常量池、字段描述、方法描述等。
补充
直接内存:非虚拟机运行时数据区域,JDK1.4中新加入NIO类,是一种基于通道与缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存。
Java堆中存储的DirectByteBuffer对象作为这块内存的引用。
HotSpot虚拟机对象
对象的创建
虚拟机遇到一条new指令时,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化,如果没有就必须先执行相应的类加载机制。
为新生对象分配内存,分配方式有指针碰撞和空闲列表,具体分配方式由Java堆是否规整决定,而Java堆是否规整由所采用的垃圾收集器是否带有压缩整理的功能决定。
保证内存分配时的线程安全有两种方法:一、采用CAS配上失败重试,保证更新操作的原子性;二、在TLAB(本地线程分配缓冲)上进行分配。
将分配到的内存空间都初始化为0值,对对象进行必要的设置(哪个类的实例、对象的哈希码、GC分代年龄等),将这些信息存放在对象的对象头之中。
执行Init()方法。
对象的内存布局
分3块区域:对象头、实例数据、对齐填充
对象头包括两部分:一、Mark Word,存储对象自身的运行时数据,包括哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;二、类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据是对象真正存储的有效信息。HotSpot虚拟机的分配策略为相同宽度的字段总是被分配到一起。
对其填充并不一定存在,仅仅起占位符的作用,对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍,当实例数据没有对齐时,就需要通过对其填充来补全。
对象的访问定位
通过句柄和直接指针两种方式
句柄方式:Java堆中需要划分一块内存作为句柄池,references中存放的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息,好处在于当对象被移动时,只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针方式:reference中存储的直接是对象的地址,访问速度快。
OOM异常
Java堆溢出、虚拟机栈和本地方法栈溢出、方法区溢出和运行时常量池溢出、本机直接内存溢出