前言
主要介绍了Java内存区域与内存溢出影响。
一、Java运行时数据区域
-
线程独占
- 本地方法栈
- 为本地方法服务(虚拟机栈为Java方法(字节码)服务)
- 虚拟机栈
- 其单位栈帧储存内容为局部变量表、操作数栈、动态连接,返回地址和其余信息
- 局部变量表和操作数栈在编译期间就可确定内存大小,局部变量表存储基本数据类型、对象引用、返回地址
- 每个Java方法调用、退出时对应一个栈帧入栈、出栈
- 程序计数器
- 用于指令字节码指示作用(是虚拟机Java方法则显示字节码地址,若为本地方法则为NULL)
- 线程共享
- 虚拟机堆
- 存储对象实例数据与数组
- 是GC主要管理的区域,为了迎合GC分代管理可以分为新生代(Eden,Survival(From、To))、老年代
- 为方便分配内存可以为各个线程分配线程私有的分配缓存(TLAB)
- 逻辑连续,物理离散,大对象最好连续
- 方法区
- 对象类型数据
- 类型、父类、接口、方法
- 运行时常量池
- 符号引用(以及部分直接引用)+字面量
- 动态的(运行期间也可以加入新的元素(string的intern()方法=没遇到过加入常量池返回引用,遇到过直接返回引用)
- 原来在方法区的永久代中,后来存在元空间中(虚拟机外的本地内存)
- 还有已被虚拟机加载的静态变量,即时编译器编译的代码缓存等数据
- 对象类型数据
- 虚拟机堆
- 直接内存(不属于运行时数据区)
- NIO中引入的,通过堆中DirectByteBuffer对象引用通过Native函数库直接分配的堆外内存
- 在分配内存时不受堆大小限制,容易忽略直接内存的存在而导致报OOM
- 本地方法栈
二、虚拟机对象
- 对象创建
- 对象构造函数初始化
- 使对象按照程序员需要的方向初始化
- 对象赋零值初始化
- 每次分配完成后赋零值
- 在TLAB中就赋零值
- 对象更新、删除操作时的线程安全
- 同步锁
- 线程独占的TLAB用完再同步锁
- 为类分配堆中实例数据内存空间(加载完成的类所需空间已经确定)
- 标记-整理GC(serial、parNew)
- 指针碰撞
- 标记-清除GC(CMS)
- 空闲列表
- 标记-整理GC(serial、parNew)
- 检查常量池中定位到的符号引用对应的类是否已经加载,解析,初始化过(检查方法区中类型数据)
- 对象内存布局
- 对象头
- Mark word 对象运行时数据
- 因为是与自身定义数据无关的数据所以要通过动态定义数据结构复用自己的存储空间
- 类型指针 指向方法区中的类型元数据
- 数组还需要记录数组的大小(数组对象大小=元数据大小*数组大小)
- Mark word 对象运行时数据
- 对象实例数据
- 填充(8字节整数倍)
- 对象头
- 对象访问
- 句柄 (对象移动时只需要改变句柄引用内容)
- 栈中局部变量表里的对象引用指向堆中的句柄表,句柄中实例数据引用指向堆中实例数据、类型数据引用指向方法区中类型数据
- 直接指针(访问实例数据时无需经历指针转发)
- 栈中局部变量表里的对象引用指向堆中的实例数据,对象中的类型引数据指针指向方法区的对象类型数据
- 句柄 (对象移动时只需要改变句柄引用内容)
- 对象构造函数初始化
三、各个内存区域的OOM溢出
- 堆溢出(不断创建对象实例数据)
- 虚拟机栈、本地方法栈溢出
- 太多线程创建太多栈OutOfMemory(减小栈的内存大小)
- 单个线程太多方法调用导致的栈内存不够StackOverflow(增大栈大小)
- 方法区、运行时常量区溢出(创建太多类导致有很多类类型数据,类型卸载要求很严格)
- 直接内存溢出(找不到溢出原因时可以考虑直接内存溢出)