目录
1. Java内存区域和内存溢出异常
1.1 概述
Java和c++之间有一堵由内存动态分配和垃圾回收技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。
1.2 运行时数据区
1.2.1 程序计数器
- 为了线程切换后能够恢复到正确的位置,每个线程都有一个独立的程序计数器,相互不影响
1.2.2 Java虚拟机栈
- 线程私有。
- 每个方法时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等。
- 局部变量表:
64位的long和double数据会占用2个局部变量空间(Slot)。其余变量占一个。局部变量所需的内存空间在编译期完成分配。
1.2.3 本地方法栈
- 虚拟机栈为虚拟机执行Java方法,本地方法栈为虚拟机使用到的Native方法服务。
1.2.4 Java堆
- 垃圾回收的主要区域。GC堆。
- 分为:新生代、老年代
1.2.5 方法区
- 和堆一样,是线程共享的内存区域
- 存储被虚拟机加载的类型学、常量、静态变量、即时编译器编译后的代码等。
- 别名:Non-Heap(非堆)
1.2.6 运行时常量池
- 方法区的一部分
- Class文件的常量池用于存放编译期生成的各种字面量和符号引用。,这部分会放在运行时常量池中
- 运行时常量池相对于Class文件的常量池的一个重要特征:具备动态性。即运行期间可以有新的常量加入到运行时常量池中。(String 的intern())
1.2.7 直接内存
- 不属于运行时数据区
- 避免了在Java堆和native堆中来回复制数据。
1.3 HotSpot虚拟机对象
1.3.1 对象创建
- 内存分配方式:
指针碰撞:内存规整
空闲列表
- 将分配到的内存空间初始化为零值。
1.3.2 对象的内存布局
- 对象头
<1>自身的运行时数据(Mark Word)
<2>类型指针.
<>如果是数组,还必须在对象头中记录数组长度。
- 实例数据:真正的有效信息
- 对齐补充:不是必然的,占位符,保证对象的起止地址是8的整数倍
1.3.3 对象的访问定位
- 通过栈上的reference数据来操作堆上的具体对象。
- 主流的访问方式:
<1>句柄
优点:reference中存储的是稳定的句柄地址,对象移动是(GC中很常见)只会改变句柄中的实例数据指针,reference本身不改变。
堆中会划分出一块内存作为句柄池。句柄中包含对象实例数据与类型数据各自的具体地址信息。
<2>直接指针
优点:节省了一次指针定位的时间开销。
2. 垃圾收集器与内存分配策略
2.1 概述
2.2 对象是否回收
2.2.1 引用计数法
- 引用时计数器加一,引用失效时,计数器减一
- 有问题:很难解决对象之间循环引用的问题。
2.2.2 可达性分析
- 可以作为GC Roots 的对象包括:
<1>虚拟机栈(栈帧中的本地变量表)中引用的对象
<2>方法区中静态属性引用的对象
<3>方法区中常量引用的对象
<4>本地方法栈中JNI(Native方法)引用的对象
2.2.3 关于引用
- 强引用:永远不会被回收
- 软引用:内存不足时回收
- 软引用:下次GC一定回收
- 虚引用:对回收无影响。回收时会收到通知
2.2.4 finalize
- finalize()是对象逃脱死亡的最后一次机会。
2.2.5 回收方法区
- 回收废弃的常量和无用的类。
2.3 垃圾回收算法
2.3.1 标记-清除法
- 标记后统一回收所有被标记的对象
- 不足:
<1>效率,标记和清除的效率都不高
<2>产生大量内存碎片
2.3.2 复制算法
- 内存分两块,将还存活的堆量复制到另一块,在把已经用过的内存空间一次清理掉。
- 不足:
内存缩小一半,比例太高。
2.3.3 标记-整理算法
- 让存活的对象相一端移动,之后直接清除点端边界以外的内存
2.3.4 分代收集
- 新生代:复制
- 老年代:标记-清除、标记-整理
2.4 HotSpot 的算法实现
2.4.1 枚举根节点
- 枚举GC Roots。必须要停顿。
- HotSpot实现中,会使用OopMap来指导那些地方存在对象引用。
- JIT 编译期间也会在特定位置记录栈和寄存器中哪些位置是引用
2.4.2 安全点
- 程序执行时不是所有的地方都能够停顿下来进行GC,只有到达安全点时才可以进行GC
- 上述“特定位置”就是安全点
- 选定标准:
是否具有让程序长时间执行的特征。(方法调用、循环跳转等)
- 如何让GC 发生时所有的线程都跑到最近的安全点再停顿:
<1>抢先式中断。(没有虚拟机用)。直接中断,如果有的不再安全点,再恢复线程
<2>主动中断。设置标志位,线程执行时轮询,标志为真,线程中断挂起。
2.4.3 安全区
- 对于处于sleep和blocked状态的线程,因为无法响应JVM的中断请求,走到安全点,所以设置了安全区。
- 一段代码片段,引用关系不变化,任何地方GC都是安全的。
2.5 垃圾回收器
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
2.5.1 Serial 收集器
- 线程、串行(GC时需要暂停其他所有的工作线程)
- Client模式下的默认新生代收集器
- 简单高效
2.5.2 ParNew 收集器
- Serial 收集器的多线程版本、串行
- server模式的首选新生代收集器(因为,除了Serial 收集器,只有ParNew可以和CMS配合)
2.5.3 Parallel Scavenge 收集器
- 目标是:达到一个可控制的吞吐量
- 吞吐量表示运行用户代码的时间占到总运行时间的比例。
- 与ParNew 收集器的一个重要区别:自适应调节策略(目的是提供最合适的停顿时间或者最大的吞吐量)
2.5.4 Serial Old 收集器
- Serial 收集器的老年代版本
- 标记-整理算法
2.5.5 Parallel Old收集器
- Parallel Scavenge 收集器的老年代版本
- 多线程
- 标记-整理算法
2.5.6 CMS收集器
- 目标:获取最短回收停顿时间
- 标记-清除
- 4个步骤:
<1>初始标记:标记GC roots直接关联的对象
<2>并发标记:GC Roots Tracing
<3>重新标记:修正并发标记期间的标记变动
<4>并发清除
- 缺点:
<1>堆CPU资源敏感。默认启动的线程数:(CPU+3)/4
<2>无法处理浮动垃圾
<3>GC后有大量的空间碎片产生。
2.5.7 G1收集器
- 面向服务端应用
- 将Java堆分为大小相等的独立区域,新生代老年代没有物理隔离。后台维护一个有限列表,每次根据允许的停顿时间有限回收价值最大的的Region。
- 特点:
<1>并行和并发
<2>分代收集:不需要其他收集器配合.采用不同的方式去处理新建的对象和存活一段时间的对象。
<3>空间整合:基于整体标记-整理,,局部上来看基于复制算法。
<4>可预测的停顿
- 步骤:
<1>初始标记:标记GC roots直接关联的对象
<2>并发标记:GC Roots Tracing
<3>最终标记:修正并发标记期间的标记变动
<4>筛选回收:对region的回收价值和成本进行排序,根据期望的停顿时间来制定回收计划
2.6 内存分配与回收策略
2.6.1 对象优先在Eden分配
- 大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
2.6.2 大对象资金进入老年代
- 大对象
长字符串或者数组等
- 最糟糕情况:朝生夕灭的短命大对象
2.6.3长期存活的对象进入老年代
- 对象年龄计数器:经历过一次GC ,年龄加1。超过一定阈值进入老年代。
2.6.4 动态对象年龄判定
- 幸存区相同年龄所有对象的大小总和大于幸存区空间的一半,则年龄大于等于该年龄的对象竟然老年代。
2.6.5 空间担保
- 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
- 如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
参考:
https://cyc2018.github.io/CS-Notes/#/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA
《深入理解Java虚拟机》