资源泄漏异常
1.1内存溢出
程序申请内存的时候,没有足够大的连续内存空间,导致异常
1.2内存泄漏
程序申请到内存后,使用完成没有释放掉内存空间,导致GC的可达性可以被监测到,所以不会被GC回收
内存泄漏最终会导致内存溢出
- 内存泄漏原因通过跟踪GC Roots的引用链,找到泄漏对象如果与GC Roots关联却不被释放。
- 是否对象生命存在周期过长
2. 线程栈和本地方法栈溢出
线程数量 = 分配给线程栈的内存是扣除堆内存剩下的内存 / 每个栈容量
如果每个线程过大,扩展线程时会导致溢出,这时候解决方案是"减少堆内存"
- 栈深度大于虚拟机所允许的最大深度
- 扩展栈时无法申请到足够内存
3. 运行常量池溢出
String.intern()导致,此方法是Native方法。若池中存在此String对象,则返回池中的String对象;否则,将此对象字符串添加到常量池中。
回收条件
判断一个对象是否死亡,有两次标记
- 是否与GC Roots有引用链,是否有finalize()方法被覆盖,或者被执行,会执行此方法(此方法可以让对象逃脱被回收),将对象放入F-Queue队列中
- 如果F-Queue队列中还存在此对象,则回收
finalize()方法只会被系统调用一次
请忘记这个方法,这是C++的析构函数,已经没人用了
Thread Stack
PC Register
Native Thread Stack
都是使用StackFrame棧帧随着出入栈实现自动内存清理
Heap
Method Area
分配是动态的,内存的回收需要使用GC来实现
Method Area 讨论
主要回收的是废弃常量和无用类:
- 废弃常量是当前系统没有任何一处引用该常量
-
无用类需要满足3个条件:
- 该类所有实例都已经被回收,也就是说Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
- 为了避免内存溢出,在大量使用反射、动态代理的场景都需要虚拟机具备类卸载功能
Heap GC讨论
Garbage Collect(GC)在动态的回收内存是通过对象是否存活来判断:
- 引用计数:每个对象有一个引用计数器,新增引用+1,释放-1,计数器为0可以回收(无法解决循环引用问题)
- 可达性分析/根搜索算法:从GC Roots往下查询,所走过的路径为引用链。如果一个对象到GC Roots没有任何引用链接,则为不可达,可回收(进行GC Roots枚举时,必须stop the world来保证引用关系不再改变。所以这也要求GC Root的枚举不能太耗时)
HotSpot
快速查找到GC Roots
使用一组叫做OopMap的数据结构。
(1) 记录每个类的每个数据位置(通过偏移量来确定)的类型(即是否是引用)。这一步在类加载后即可确定。
(2) 在特定的位置上记录栈和寄存器中哪些位置是引用。
这样我们在GC Root枚举时,可以快速找到所有引用。
设置安全点(只有在安全点上才有全部的OopMap信息,安全点由程序结构决定)
抢占式:在GC发生时,中断所有线程,如果发现有的线程不在安全点,
则重新唤醒知道跑到最近的安全点。--这种方式几乎没人使用
主动式:线程主动发起。在GC发生时,在安全点位置上设置标记位,
线程执行到安全点时轮询这个标志位,如果发现为true,则中断。
安全区(解决就绪线程态无限等待CPU导致无法GC进入安全点)
一段代码内无引用关系改变。
线程进入安全区代码后,标记自己“安全区”标记,GC发生时就不用管标记“安全区”的线程了。
当线程想要离开安全区时,判断GC是否结束,如果没结束要等GC结束后才能继续执行
GC Roots的对象
- Thread Stack(StackFrame局部变量表)中 引用的对象
- Method Area 类静态属性引用的对象
- Method Area 常量引用的对象
- Native Method Stack JNI中引用的对象
垃圾收集算法/方法论
并行(Parallel):用户工作线程停止,多条GC线程并行工作,可以同时处理多件事情(A B同时开始处理从0%->100% )
并发(Concurrent):用户工作线程与GC线程并发工作,GC集中在另一个CPU上,可以处理多个事情,不需要同时(先处理A 50%,再处理B 50%,再处理A 50%,再处理B 50%)
1. 标记-清除(Mark-Sweep)算法
标记出需要回收的对象,在标记完成后统一回收掉所有的被标记对象。
issues:
- 标记和清除的效率不高
- 标记清除后的会产生大量的不连续内存,碎片太多导致需要连续的大对象无法找到连续内存,再次提前促发垃圾回收
2. 复制(Copy)算法
将内存空间分成大小相等的两块,每次只使用一块。当一块用完类,将存活的对象copy到另一块上面,然后把使用的内存一次清空,在对象存活率低时回收的效率高
issues:
- 内存只有一半
- 长生存期的对象被持续复制效率低
3. 标记-整理(Mark-Compact)算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,在对象存活率高时回收效率高
4. 分代收集算法
基本假设是:绝大多数的对象生命周期都非常短暂,频繁回收,但是有部分对象生命周期比较长
将Java堆分为
Young Generation:每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法
,只需要付出少量存活对象的复制成本就可以完成收集
Old Generation:对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”
或“标记-整理”算法
来进行回收
垃圾收集器/方法论实现
1. Serial收集器
串行收集器,只使用一个线程去回收,当回收的时候会Stop The World。
缺点:工作时会停止用户线程
算法:新生代,复制算法
关注点:响应时间优先
2. ParNew收集器
多线程串行收集器,当新生代占用达到一定比例时开始收集
缺点:工作时会停止用户线程
算法:新生代,复制算法
关注点:响应时间优先
3. Parallel Scavenge
关注点:吞吐量优先,各代比例自适应调整
算法:复制算法
4. Parallel Old收集器
5. CMS(Concurrent Mark Sweep)收集器
采用标记-清除算法,使用并发
的方式
分为四个阶段:初始标记(stop-the-world)->并发标记->重新标记(stop-the-world)->并发清除
存在浮动垃圾,需要预留内存空间(默认68%),如果内存不足,会使用Serial Old收集器来执行。
优点:并发收集、停顿低,可以与用户线程并发工作,最短用户线程停顿的时间
缺点:会产生大量的空间碎片,导致提前促发full gc(预留给标记中用户修改的空间),并发阶段会降低吞吐量(解决方案是在N次full gc后提供碎片整理)
关注点:响应时间优先
分为四个步骤:三标记一清除
-
初始标记:
mark
GC Root直接关联的对象 -
并发标记:进行GC Root的
trace
过程,进行可达性分析 - 重新标记:修正(2)时,由于用户线程也在运行导致的引用关系变化
- 并发清除:就是清除的过程,并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾(浮动垃圾),新的垃圾在此次GC无法清除,只能等到下次清理
(1)和(3)需要stop the world,但(1)和(3)的消耗时间非常短。
(2)和(4)为并发,即可以和用户线程同时运行。消耗时间相较于1和3较长
但由于是并发,所以整个收集器看来就是和用户线程并行运行。
6. G1收集器
Young/Old Generation 不再是物理隔离,将Heap 划分成N个相等大小独立的Region,Young/Old Generation是一部分Region的合集,后台维护一个优先列表
,优先回收垃圾最多的区域
跨
region和跨代之间的堆引用全局扫描问题
,采用Remembered Set记录每次赋值引用时是否引用其它region,是则记录。(保证不会全堆扫描)
关注点吞吐量优先
优点:
- 整体来看采用标记-整理算法,局部(region)来看采用复制算法,不会产生大量碎片而导致无法分配大对象
- 可预测停顿,在指定N毫秒内的时间片段内,GC时间不超过N毫秒
- GC线程与用户线程可以并发执行
分为四个步骤:三标记一清除
- 初始标记
- 并发标记
- 最终标记
- 筛选回收:在运行的GC停顿时间内,选择回收价值最高的Region回收。这个阶段虽然可以和用户线程并行,但实际上由于所花费时间短,且停顿用户线程这个时间会更短,所以采用停顿用户线程的方式
常用的收集器组合
YoungGeneration: ParNew
OldGenration: CMS
YoungGeneration: G1
OldGenration: G1
Minor GC和Full GC
- Minor GC
新生代的垃圾收集动作,由于新生代的Java对象存活率都很低,Minor GC会非常频繁,并且回收速度快 - Full GC
老年代的垃圾回收动作,出现Full GC的时候通常会执行一次Minor GC。并且时间较长,会产生STW(stop the world)
GC促发条件
-
对象
优先
分配在Eden区 如果Eden区没有足够的空间
时,虚拟机执行一次Minor GC
。 -
大对象
直接
进入老年代 (大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝
(新生代采用复制算法收集内存)。 -
长期存活
的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。 -
动态判断对象的
年龄
。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。 -
空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置(这个意思为是否担保失败),如果true则只进行Monitor GC,如果false则进行Full GC
- 手动调用System.gc()方法,通常这样会触发一次的Full GC以及至少一次的Minor GC