垃圾收集器和内存分配
Posted sun-haiyu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了垃圾收集器和内存分配相关的知识,希望对你有一定的参考价值。
垃圾收集器和内存分配
程序计数器、虚拟机栈、本地方法栈这三个区域和线程的生命周期一致,所以方法结束或者线程结束时,内存自然就跟着回收了。Java堆和方法区,只有在程序处于运行期间才能知道会创建哪些对象,即这部分的内存分配和回收都是动态的,垃圾回收主要关注的是堆内存。
对象存活判断
在进行垃圾回收之前,首先要判断哪些对象还存活,哪些已经死去去。判断对象存活的方法,有如下几种:
引用计数法
每个对象有一个引用计数器,每当有一个地方引用了它计数+1;引用失效计数器-1;当引用计数为0时,说明这个对象在任何地方都不被使用了,可以进行回收了。
引用计数法有缺点:对象之间的循环引用。当两个对象互相引用对方,除此之外它们都再无其他任何引用时,两个对象的引用计数都不为0,造成了GC收集器无法回收它们。
可达性分析法
Java中正是使用了这种算法来判断对象是否存活。这种算法使用了类似树形结构来搜索对象,作为根结点的称为GC Roots,是搜索的起点,搜索走过的路径叫做搜索链,可以作为GC Roots的对象有
- 虚拟机栈中引用的对象
- 方法区中类静态属性有引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(通常所说的Native方法)引用的对象
当某个对象到GC Roots的路径上没有引用,或者说从GC Roots开始搜索不到这个对象(GC Roots到这个对象是不可达的),那么该对象就可以被回收。
图中GC Roots到Object5、6、7都不可达。
可达性分析中不可达的对象并不是一定会被回收,对象真正被回收需要经过两次标记。可达性分析后发现GC Roots到某个对象不可达时,该对象会被第一次标记。接着判断,如果:
- 对象没有覆盖
finalize()
方法 finalize()
方法已被调用(只能被系统调用唯一一次)
满足以上条件的任一个,虚拟机则认为“没有必要执行finalize方法”,接着经过第二次标记后,对象被回收。否则,有必要执行finalize,对象进入F-Queue队列之中,finalize方法是对象存活的最后机会——只需和引用链上的任一个对象关联即可,那么在第二次标记时将被移除出“即将回收”的集合;如果还不能finalize中逃脱,该对象才真正被回收。
引用
引用有4种:
- 强引用。比如
Object obj = new Object()
,只要强引用还在,GC收集器不会将被引用的对象回收。 - 软引用。用于描述一些还有用但非必需的对象,和软引用关联的对象,系统将要发生内存溢出时,会将这些对象列入回收范围中进行第二次回收;若这次回收后还是没有足够的内存,才抛出内存溢出异常。
- 弱引用。也用于描述非必需对象,比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
- 虚引用。最弱的引用关系,对象的虚引用存在与否,不会对其生存时间造成影响,也不能通过引用取得对象。设置虚引用的作用是当对象被回收时能收到一个系统通知。
垃圾收集算法
标记-清除算法
- 标记要回收的对象
- 统一回收被标记的对象
缺点如下:
- 标记和清除两个阶段效率不高
- 清除后产生大量不连续的内存碎片,对之后范培大对象带来不便(不得不提前出发一次垃圾收集)
复制算法
将内存按比例分成两块,每次只使用其中一块。当这一块内存用完了,将存活对象全部复制到另一块中,接着将已使用的内存空间一次性清除。
优点:不会产生内存碎片;缺点:内存利用率低。
标记-整理算法
和标记-清除算法类似,不同的是标记后并不是直接清理,而是让所有存活对象向一端移动,然后直接清除掉端边界外的内存。
分代收集算法
根据对象的存活周期的不同将内存划分为几块,通常将Java堆分为新生代和老年代。根据各个年代的特点采用最适当的收集算法:
- 新生代。每次垃圾收集都发现只有少量对象存活,采用复制算法,因为所需复制操作次数少;
- 老年代。对象存活率高、没有额外的空间对它进行分配担保,必须使用标记-清除或标记-整理算法。
GC进行时必须停顿所有Java执行线程。程序执行时只有在到达安全点才能暂停。而安全区域则是安全点的扩展:指在一段代码中,引用关系不会发生变化,在这个区域的任何地方开始GC都是安全的。
垃圾收集器
Serial收集器:单线程的收集器,在它进行垃圾回收时必须暂停其他所有的工作线程,直到它收集完成为止。优点:简单高效,没有线程交互的开销;缺点:GC时候其他线程不能工作。
ParNew收集器:Serial的多线程版本,使用多条线程进行垃圾收集,其余和Serial收集器几乎一致。
Parallel Scavenge收集器:收集器使用复制算法的新生代收集器,且是并行的多线程收集器。该收集器的目的是达到一个可控制的吞吐量。
吞吐量 = 运行用户代码的时间 / (运行用户代码的时间 + GC收集时间)
Serial Old收集器:Serial收集器的老年代版本,使用标识-整理算法。
Parallel Old收集器:Paralell Scavenge的老年版本,使用多线程和标记-整理算法。
CMS收集器:老年代的收集器目的是尽可能缩短垃圾收集时用户线程的停顿时间。适合需要用户交互的场景,能获得较短的响应时间。
基于标记-清除算法实现,整个过程分为以下4步:
- 初始标记:标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程,即在堆中堆对象进行可达性分析,从GC Roots开始找出存活的对象
- 重新标记:修正并发标记期间因用户程序继续运作导致标志产生变动的那部分对象的标志记录
- 并发清除:并发清除要回收的对象
缺点:
- CMS无法处理浮动垃圾(浮动垃圾指CMS在并发清理的过程中用户线程还在继续运行,因此还会产生垃圾,这些新产生的垃圾在标记之后,故CMS无法在本次收集中清理掉它们)
- CMS基于标记-清除,故会产生大量不连续的内存碎片
- 对CPU资源很敏感
G1收集器:有如下特点:
- 并行与并发
- 分代收集
- 从整体上看是基于标记-整理算法实现,从局部(两个Region之间)来看是基于复制算法的收集器,以确保G1运行期间不会产生内存空间碎片
- 可预测的停顿,G1除了追求低停顿外,还能建立可预测的时间模型,主要原因是它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
在使用G1收集器时,Java堆的内存划分为多个大小相等的独立区域,新生代和老年代不再是物理隔离。G1跟踪各个区域的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域。
G1收集器的运作大概有以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
前三个步骤和CMS的前三个步骤类似。最后一步筛选回收会对各个区域的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
内存分配和回收策略
对象主要分配在新生代的Eden区上,少数情况下也可能直接分配在老年代。当Eden区没有足够的空间时,虚拟机将发起一次Minor GC。
- Minor GC:发生在新生代的垃圾收集,Java对象大多“朝生夕灭”,因此Minor GC比较频繁,回收速度快;
- Full GC / Major GC:发生在老年代的GC,出现Full GC一般会伴随至少一次的Minor GC,且速度相比Minor GC会慢很多。
大对象会直接进入老年代。大对象指的是需要大量连续内存空间的Java对象,比如长字符串和大数组。
长期存活的对象将进入老年代。虚拟机给每个对象设置了一个对象年龄计数器,如果对象在Eden区出生并经历过一次Minor GC后仍然存活,且能被Survivor区容纳的话,该对象将被移动到Survivor区,且对象年龄设为1。此后,该对象每在Survivor区“熬过”一次Minor GC,对象年龄就+1,当对象年龄增长到一定程度(默认15岁)就晋升到老年代。但这个准则并不是一定的,如果在Survivor区中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接晋升老年代。
Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否能容纳新生代所有对象空间,若满足,则此次Minor GC是安全的;若不满足,则虚拟机会查看是否设置了允许担保失败,若允许,判断老年代的最大可用连续空间是否大于历次晋升到老年代对象的平均大小,若大于,就尝试着进行一次Minor GC,如果小于或者设置了不允许担保失败或者,那么将进行一次Full GC。
by @sunhaiyu
2018.6.9
以上是关于垃圾收集器和内存分配的主要内容,如果未能解决你的问题,请参考以下文章