自动内存管理
Java内存区域与内存溢出异常
运行时数据区域
- 程序计数器:记录正在执行的虚拟机字节码指令的地址;线程私有;
- 栈:栈中的单位是栈帧(存储局部变量表,操作数栈,动态连接,方法出口等信息),栈帧的入栈和出栈对应着一个方法从被调用到执行结束;线程私有;
- 堆:虚拟机所管理的内存中最大的一块;线程共享;
- 方法区:线程共享的;存放被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存
- 本地方法栈:类似栈,不过是服务于本地方法
对象的创建
在语言层面上,创建对象仅仅只是new一个关键字而已;
从虚拟机的层面上:
- 从常量池查询该类的符号引用,判断该类是否被加载过
- 为新增对象分配内存空间(空间压缩整理,指针碰撞)
- 在创建对象时在创建对象时存在线程安全的问题(采用CAS,本地分配线程缓冲(TLAB))
- 将分配的空间初始化为零值
- 必要的设置(主要是对对象头的设置)
- CLass的init方法
对象的内存布局
1. 对象头
对象头分为两个部分:用于存储自身运行时所需要的数据,hashCode,GC年龄,锁状态,偏向线程ID等;类型指针,指向类型元数据的指针,如果是Java数组,还有记录数组长度的数据
2. 实例数据
该类中定义的各个字段
3. 对齐填充
非必要,任何对象的大小必须是8字节的整数倍
对象的访问定位
通过栈上的refrence来操作具体的对象
- 句柄:在堆开辟一块空间作为句柄池,reference存储的是句柄的地址,句柄中存放对象实例数据的指针和对象类型数据的指针,一次访问对象时,需要经过两次指针定位,好处是在垃圾回收的时候只会改变对象实例数据的指针,reference的值不需要改变
- 直接指针:refrence存储的是对象实例数据,在对象实例数据中存储对象类型数据的指针,如果只访问对象数据的话,只需要一次指针定位
OutOfMemoryError异常实战
堆溢出
通过堆转储快照进行分析,首先确认导致OOM对象是否必要的,从而判断是出现了内存泄漏还是内存溢出
- 内存泄漏:查看GC Root到对象的引用链,通过该引用链一般可以准确地定位到这些对象创建的位置
- 内存溢出:检查堆参数设置
栈溢出
- 线程请求的栈深度超过虚拟机允许的最大深度,StackOverflowError
- 扩展栈容量无法申请到足够的内存 OutOfMemoryError
操作系统分给进程的空间是有限的,减去堆和方法区的空间,再减去直接内存和虚拟机进程本身消耗的内存,剩下的空间约等于虚拟机栈和本地方法栈的大小,当每个线程分配到的栈空间越大,可以建立的线程数量就会减少
在不能减少线程数量的情况下,可以考虑减少最大堆和栈容量来换取更多的线程
方法区和运行时常量池溢出
intern() 在JDK1.6中是将首次遇到的字符串复制到永久区并返回永久区的是对象引用
在JDK1.7之后,字符串常量池中放到了堆中,intern()不再需要复制字符串对象中,而是在常量池中保存首次出现的对象实例的引用即可
垃圾收集器与内存分配策略
判断对象为垃圾的算法
- 引用计数器(存在循环引用的问题)
- 可达性算法
并非被标记成垃圾的对象第一次就会被回收,被标记为垃圾的对象会进行一次筛选,筛选的标准是有没有必要执行
finalize()
如果没有重写该方法,或者该方法已经被调用过,则会认为是没有必要执行的
如果是有必要的执行的,会将该对象放置到一个队列中,JVM会启动一个低优先级的线程去执行finalize()
,但是并不保证一定等待该方法执行结束,避免某个对象的该方法执行缓慢,或者发生死循环,会导致队列中的其他对象一直处于等待的状态
回收类的条件
- 该类的所有实例都已经被回收
- 该类的类加载器已经被回收
- 该类对应的Class对象不再被引用
垃圾收集算法
分代收集理论
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾回收的对象越难以消亡
- 跨代引用假说:跨代引用相对于同代引用来说仅占少数 --> Remember Set(在新生代中建立的一块全局变量,记录老年代的哪些区域存在跨代引用,在新生代中进行GC时,只需要将这部分的区域进行可达性的扫描即可)
标记 - 清除算法
存在内存碎片的问题,在内存分配的方法比较困难,但是停顿时间更多,在CMS中使用该算法(多数时候),同时结合了标记-整理的算法
标记 - 复制算法
在新生代使用
标记 - 整理算法
在老年代使用
HotSpot的算法细节实现
根节点枚举
不从GC Roots集合中去查找引用链
在HotSpot的解决方案中,是使用一组称为OopMap的数据结构来保存对象引用
安全点
只会在安全点记录OopMap的信息
在进行垃圾回收的时候需要所有的线程都是跑到最近的安全点
- 抢占式中断:系统首先将所有用户线程全部中断,如果发现某个用户线程不处于安全点上,就让他恢复执行,跑到安全点重新中断
- 主动式中断:设置一个标志位,所有的线程都会去轮询(精简至一条汇编指令)该标志,如果发现需要中断则到最近的安全点挂起
安全区域
当程序没有处于分配处理器时间,线程是无法响应系统的中断的,这个时候需要引入安全区域来解决
安全区域是指能确保在某一段代码片段中,引用关系不会发生变化,因此在这个区域的任何地方开启垃圾收集都是安全的,可以当作扩展开来的安全点
记忆表和卡表
记忆表是记录从非收集区域指向收集区域的指针集合的抽象数据结构
卡表是记忆表的一种实现方式,是一个字节数组
该字节数组中每一个元素对应非收集区域的一个特定大小的内存,称为卡页;在卡页中存在一个或多个对象存在跨代引用的时候,将这个元素标识为1,在gc时,将这些标识为1的元素加入到GC Roots中进行扫描即可
使用写屏障进行卡表的写入,写屏障可以看作是引用类型赋值的AOP切面,在引用类型赋值的会生成一个环绕通知
可达性分析(三色标记法)
- 白色:未访问过;在开始阶段,所有的对象都是白色的,在结束阶段,表示为不可达
- 黑色:表示对象已经被访问过,且该对象的所有引用都已经被扫描过
- 灰色:表示对象已经被访问过,但是该对象至少存在一个对象未被扫描过
当且满足两个条件时,会发生对象消失的现象
- 多了一个黑色到白色的引用
- 全部灰色对象到该白色对象的引用全部被删除
解决方式是破环这两个条件
- 当黑色对象新增一个白色对象的引用,将该对象记录下来,并发扫描结束之后,再将这些对象重新扫描一遍(增量更新)
- 将要删除的引用记录下来,在并发扫描结束之后,按照一开始的对象图进行搜索(原始快照)