02、Java内存区域与内存溢出异常
斜线表示线程私有;其他表示线程共享
| 方法区 | 虚拟机栈 | 本地方法栈 |
|---|---|---|
| 堆 | 程序计数器 | |
| 执行引擎----> | 本地接口库----> | 本地方法库 |
程序计数器
是一块较小的内存空间,线程私有表示当前线程下指令执行的字节码行号指示器,告诉虚拟机要执行哪条指令(跳转、循环、异常处理、线程恢复等都需要这个)
每一个线程都有一个独立的程序计数器,各线程之间互不影响
Java虚拟机栈
线程私有,描述的是Java方法执行的线程的内存模型:每个方法执行,虚拟机同步创建一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等
方法的调用到执行完成意味着一个栈帧的入栈和出栈
局部变量表:存放编译期各种Java虚拟机基本数据类型(boolean、byte、short、int、float、long、double)和对象引用还有returnAddress(字节码指令的地址)。这些数据类型在局部变量表中以局部变量槽表示,64位(double、long)两个槽、32位一个槽
编译期间这些局部变量表中的数据分配的大小都是固定的,方法运行期间不会改变
本地方法栈
为了虚拟机能使用本地方法
Java堆
所有线程共享的一块内存区域,在虚拟机启动时创建。用来存放对象实例
垃圾收集器管理的一块内存区域
Java堆共享内存可以划分出线程私有的分配缓冲区,提高对象分配的效率
可以是不连续的内存空间,可以拓展(-Xms;-Xmx)无法拓展抛OOM异常
方法区
线程共享区域,存储虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
这部分内存回收主要针对常量池的回收和对类型的卸载。
运行时常量池
用于存放编译期生成的各种字面量与符号引用;在类加载后存放到方法区的运行时常量池
运行期间也可以把新的常量放到池子中:String类中的intern()方法
对象的创建
1.Java虚拟机遇到new指令时,首先检查这个指令的参数能不能在常量池中找到,并检查是否被类加载、解析和初始化。如果没有先执行相应的类加载过程
2.在类加载通过后,虚拟机为新生对象分配内存(大小在类加载的时候就确定了)
-
指针碰撞划分:将内存划分为使用和空闲两部分,中间用指针分隔开,当需要分配内存,指针就像空闲区移动
但是内存并不都是规整的,已经使用和空闲都是交错在一起
而规整与否取决于垃圾回收采用的算法是否带有空间压缩整理能力。
-
空闲列表划分:虚拟机维护一个表,记录哪些内存块是可用的,分配的时候选择一块足够大的空间划分给对象,并在表中更新记录
3.对象创建在虚拟机中是非常频繁的所以在并发的情况下线程不安全(对象A正在分配内存,还没有保存对象B就是用原来的指针)
- 对分配内存空间进行同步处理——虚拟机采用CAS配上失败尝试的方式保证更新的原子性
- 把分配的内存的动作按照线程划分在不同的空间之中进行,每一个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,哪个线程需要分配就去哪个分配缓冲中分配,当缓冲用完了才使用同步锁定
4.内存分配完后将内存空间初始化为零值,如果使用了本地线程缓冲这一步可以提前到此时一块进行。
5.对对象进行必要的设置,比如:对象是哪个类的实例、怎么找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中
对象的内存布局
对象头
对象头:包括两类信息
- 存储对象自身的运行时数据,如:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
- 类型指针,对象指向它的类型元数据的指针,通过这个确定这个对象是哪个类的实例。如果是数组还需要一个数据来记录数组的长度
实例数据
实例数据:是对象真正存储的有效信息,即我们在程序代码里面定义的各种类型的字段内容,无论是父类继承下来的还是子类中定义的。
默认分配顺序是doubles|longs、ints、shorts|charts、bytes|booleans、oops
对齐填充
对象填充:占位符作用
对象的访问定位
Java程序通过栈上面的reference数据来操作堆上面的具体对象
访问方式:
- 句柄访问:在堆中划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址(句柄中包含了实例数据和类型数据各自具体的地址信息)(稳定)
- 直接指针访问:reference中存储的直接就是对象地址(有点快)
内存溢出错误OOM
-
堆内存溢出,反复创建对象
-
虚拟机栈和本地方法栈:
- 线程请求的深度大于虚拟机所允许的最大深度抛出StackOverflowError
- 虚拟机栈内存允许动态拓展,当拓展栈容量无法申请足够内存排除OutOfMemoryError
-
方法区和运行时常量池溢出
-
本机直接内存溢出