Java虚拟机垃圾收集器与内存分配策略
概述
- 那些内存需要回收,什么时候回收,如何回收是GC需要完成的3件事情。
- 程序计数器,虚拟机栈与本地方法栈这三个区域都是线程私有的,内存的分配与回收都具有确定性,内存随着方法结束或者线程结束就回收了。
- java堆与方法区在运行期才知道创建那些对象,这部分内存分配是动态的,本章笔记中分配与回收的内存指的就是:java堆与方法区。
判断对象已经死了
- 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它,计数器+1;引用失败,计数器-1.计数器为0则改判断该对象已死。 但是这种方式,很难解决java对象间相互循环引用的问题。所以主流的java虚拟机,都没有使用该方式判断对象是否存活。
- 可达性分析(Reachability Analysis):通过GC Roots作为起点,以这些节点通过引用链(Reference Chain)向下搜索,当GC Roots没有引用链能够到达某个对象A的时候,A可以认为是不可用对象。这类对象就可以判定为可回收对象
1.引用
- 引用的狭隘定义:当reference类型数据中存储的数值是另一块内存的起始地址,就称这块内存代表着一个引用。但是这种方式不能描述一些“食之无味,弃之可惜”的对象
- JKD1.2之后对引用概念做了扩充,对象分为强引用(Strong Regerence),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference)
- (1)强引用指new这类的引用,只要强引用还在,收集器永远不会回收掉被引用的对象
- (2)软引用用来描述一些还有用,但是非必须的对象,在系统要发生内存溢出的时候,会对这类对象二次回收,如果空间还不够,抛出OOM
- (3)弱引用的对象只能生存到下一次收集发生之前。无论内存是否足够。
- (4)虚引用是最弱的一种引用关系,为一个对象设置虚引用唯一目的只是在这个对象被回收的时候收到一个系统通知。
2.回收方法区
- Java虚拟机规范说过可以不要求回收方法区(永久代),在新生代中,垃圾收集一次可以回收70%以上内存。但是在方法区(永久代)的收集效率灰常低。主要收集的是废弃常量与无用的类。
- 废弃常量指常量池存在一个字符串abc.但是当前系统没有对象引用abc,则abc是可以废弃的常量,可以被回收
- 无用的类:(1)该类的对象已经被全部回收.(2)加载该类的ClassLoader已经被回收.(3)该类对应的Class对象没有在任何地方被引用,而且通过反射访问。HotSpot提供了 -Xnoclassgc参数控制是否回收无用的类
垃圾收集算法
- 经典常用的收集算法:标记-清除算法(Mark-Sweep),标记-整理(Mark-Compact),复制算法(Copying),主流虚拟机的”分代收集”
1.标记-清除算法
- 标记清除是最基础的收集算法:1.标记出所需要回收的对象。2.标记完成后统一回收掉所有被标记的对象。
- 标记清楚算法不足:1.效率不高。2.清除之后产生大量不连续的片段,如果这些不连续不够空间存放之后产生的对象,还会提前触发另外一次收集动作。降低内存效率。
2.复制算法:
- 复制算法将内存分为两等块A,B,每次使用一块内存比如A,当A内存满了之后,将剩下的存活的对象拷贝到B内存,一次性清除A。优点:实现简单运行高效,并且不用在考虑类似于标记清除方式留下的内存碎片。 缺点是:将原来的内存只使用一半,成本太高。
- 当前主流的商业虚拟机大多采用这种方式收集新生代。新生代对象98%都是朝生夕死,所以不必要按照1:1分配新生代内存。辟如hotspot的分配为:8(Eden):1(from survivor):1(to survivor)。同时使用老年代进行内存分配担保。
3.标记-整理(Mark-Compact)
- 复制收集算法在对象存活率较高的时候,会产生大量的复制工作,影响性能。因为老年代,一般对象比较大,并且存活率高,一般不能使用该算法。
- 标记-整理算法是根据老年代对象存活率高,对象比较大的特性,提出的。其基本原理与标记清除一致,但是后面步骤不是直接清除,而是让所有存活的对象向一端移动,然后清除掉端边界以外的内存。
4.分代收集算法
- 将虚拟机内存主要分成新生代与老年代,新生代一般使用复制算法,由老年代进行分配担保。老年代使用标记整理算法进行回收。
HotSpot主要算法实现
- GC-Root节点找引用链,GC-Root节点一般为常量,静态变量,本地变量表中数据。当应用的方法很多,找引用链这个操作,将会消耗很多时间
- 可达性分析,对时间的敏感还体现在GC停顿上,分析的时候,要求系统是停止的,不可以出现可达性分析过程中,对象引用关系还在不断的变化,即“stop the word”。
- 当前主流虚拟机都使用的准确式GC:在HotSpot实现中,使用一组称为OopMap的数据结构来达到这个目的。在类加载完成时候,在JIT(即使编译器)编译过程中,在特定的位置记录下栈中那些位置是引用。这样GC在扫描的时候可以直接得知这些信息了。
- 安全点:在OopMap协助下,HotSpot可以快速准确完成GC-Root枚举,但是OopMap的内容变化指令非常多。如果为每一条指令都生成对应的OopMap,需要消耗大量额外空间,因此,只有在特定位置(安全点:Safepoint)才能记录这些OopMap信息。方法调用,循环跳转,异常跳转等这些功能的指令才会产生SafePoint。
- 安全区(safe region):安全区域是引用关系不会变化的一个代码片,线程执行到安全区的时候,首先标记自己进入了安全区,这段时间JVM执行GC的时候,不用管这些状态的线程了;线程出安全区的时候,首先会检查系统是否已经完成了GC Root的选举,如果完成了继续执行,如果没有完成则等待直到收到可以安全离开的信号为止。
HotSpot主要的垃圾收集器
分代算法可以将收集器分为新生代收集器和老年代收集器。1.新生代收集器主要包含:Serial(串行),ParNew(并行),Parallel Scavenge(并行);2.老年代收集器主要包含:Serial Old,Parallel Old,CMS(Concurrent Mark Sweep)。
- Serial是最早的,单线程的新生代收集器,在收集动作的时候,必须暂停其它所有的工作线程,直到收集结束。该收集器几乎是单CPU中收集速度最快效率最高的收集器。serial对Client模式下的虚拟机来说依旧是很好的选择,回收新生代一两百兆基本在100毫秒以内,这些停顿完全可以接受
- ParNew是Serial的多线程版本,该收集器是Service模式下的虚拟机首选收集器,主要原因是它可以与主流的老年代收集器CMS配合使用。该收集器使用的线程数默认与CPU内核一致,
- Parallel Scavenge 收集器是关注点是吞吐量(吞吐量=运行用户代码时间/(运行代码时间+垃圾收集时间)),停顿时间越短,则用户体验越好,高吞吐量则可以高效利用CPU时间,尽快完成运行任务。该收集器适合大量后台运算而不需要太多的交互的系统。该收集器也叫做吞吐量优先收集器。
- Serial Old 采用的是标记整理算法。和Serial收集器一样,都是单线程收集器。一般使用于Client运行模式下。
- Parallel Old是Parallel Scavenge收集器配合使用的老年代收集器,使用的是标记整理算法,如果新生代使用了Paraller Scavenge算法,则老年代只能使用Parallel Old与Serial Old算法。在注重吞吐量系统,优先考虑Paraller Scavenge与Parallel Old算法。
CMS收集器
是当前老年代主流收集器,采用标记清除算法。CMS整个过程主要分为4个步骤:1.初始标记;2.并发标记;3.重新标记;4.并发清除.其中初始标记,重新标记仍然需要stop the world。
- 初始标记只是记录GC Root能够直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing,重新标记阶段则是为了修正并发标记期间用户继续操作变动的那一部分对象的标记记录。
- 耗时比较长的并发标记与并发清除步骤,可以与用户线程一起工作,不需要stop the world。CMS的优点体现在了并发收集,低停顿。
- CMS缺点1:对CPU资源非常敏感,在低于4核的时候不建议使用该收集器。CMS默认启动的回收线程数是(CPU数量+3)/4。
- CMS缺点2:CMS没有办法处理浮动垃圾,因为标记之后,用户线程还在执行,会产生其它垃圾,留在下次收集。在jdk1.6之后,CMS收集器启动阈值已经提升至92%,如果老年代剩下的8%不能满足浮动垃圾的分配,则会出现Concurrent Mode Failure.从而导致Full GC产生。也可以临时启用serial old 重新对老年代收集,但是这样停顿时间会很长。-XX:CMSInitiatingOccupancyFraction属性设置CMS的启动阈值,并且可以根据实际适当降低启动阈值。
- CMS缺点3: 使用的是标记清除算法,会产生大量碎片空间,如果无法找到足够大的连续空间分配当前对象,则触发一次full gc。
G1收集器
G1收集器被认为是HotSpot jdk1.7重要进化特征,是一款面向服务端的垃圾收集器。G1具有以下优势
- 并行与并发:G1能够更充分应用多CPU,多核环境的硬件优势,减少系统停顿时间。G1可以通过并行的方式,让java程序继续执行。
- 分代收集:G1不需要与其他收集器配合,可以独立管理整个GC堆。它能够自己采用不同的方式去处理新建的对象和已经熬过多次GC的老对象,以产生更好的收集效果。
- 空间整合: G1是基于标记整理算法实现的收集器,不会产生大量不连续的碎片空间,这一点比CMS要有很大提高。
- 可预测的停顿:降低停顿几乎是所有当前互联网企业应用的关注点。G1除了追求低停顿外,还建立了可预测停顿时间模型,能让使用这指定在一个长度为M的毫秒时间片段内,收集消耗的时间不长于N毫秒。已经有部分实时的垃圾收集器的特征了。
- 其他收集器回收范围都是整个新生代和整个老年代,G1将整个堆分成多个大小相等的独立区域(Region),新生代与老年代都是Region的不需要联系的集合。回收的时候,有一个优先列表,优先回收价值最大的Region,保证了G1收集器在有限时间内获取尽可能高的收集效率。
- Region:每一个Region都有一个对应的Remembered Set。每一次操作Reference时候如果检查到有引用对象在别的Region中。则记录Remembered Set。将Remembered Set作为一个GC-root。可以保证不会对全堆做扫描,也不会有回收遗漏。
- G1收集器如果不计算维护Remembered Set操作,可以分为以下步骤:1.初始标记;2.并发标记;3.最终标记;4.筛选回收
内存分配策略
- 对象优先在Eden分配
- 大对象直接进入老年代:代码中劲量避免短命的大对象。-XX:PretenureSizeThreshold=3145728 表示大于3M的对象直接在老年代分配。
- 长期存活的进入老年代:虚拟机个每个对象一个age计数器,对象每经历一次Minor GC存活下来,age+1.达到一定年龄值(默认15),则进入老年代。-XX:MaxTenuringThreshold=1表示每次Minor GC之后存活的对象就要进入老年代。
- 动态判断对象年龄:如果在survivor中相同年龄对象的大小总和大于survivor空间的一半,年龄大于或者等于该年龄的对象直接进入老年代。
- 空间分配担保:创建对象新生代如果没有足够空间时候,则由老年代分配内存,这就要求老年代有连续的能够存放该对象的空间,否则可能担保失败,从而触发Full GC
小结
该内容主要介绍了几种垃圾收集算法,几种主流虚拟机收集器以及内存分配的主要策略。根据具体场景选择关注停顿点还是关注吞吐量,串行还是并行收集器。是调优的重要部分。同时理解内存分配,对于写java代码也有一定的提高。