JVM的垃圾收集器与内存分配策略
判定对象是不是死亡
-
引用计数法:给对象中添加一个计数器,每当有一个地方引用它时计数器加一,引用失效时计数器减一,任何时刻计数器为零时表示该对象不可能再被引用
特点:实现简单判定效率很高,在大多数时候是一个不错的算法,但是难以解决对象之间的互相循环引用问题
-
可达性分析:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径(引用链),当一个对象到GC Roots没有任何引用链相连接的时,此对象是不可用,判定为可回收对象
-
可作为GC Roots对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(Native方法)引用的对象
-
引用 强 软 弱 虚
强引用:代码中普遍存在的类似于(Object obj = new Object())这类引用,只要强引用还存在,垃圾收集器永远不会回收被引用的对象
软因引用:还有用但是非必须的对象,软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围只用进行第二次回收,如果回收之后的还没有足够的内存才会抛出内存溢出异常
弱引用:非必须的对象,比软引用还弱,所引用的对象只能生存到下一次垃圾收集发生之前。垃圾收集器工作时无论内存是否够用,都会被回收掉
虚引用:最弱的一种引用,在该对象被回收时收到一个系统通知
补充:
可达性分析算法中的不可达对象,要经过至少两次标记过程(第一次标记后进行筛选,条件为是否执行finalize()方法。当没有覆盖finalize()或finalize()已经被虚拟机调用过,视为没必要执行)
垃圾回收算法⭐
-
标记清除:分为标记和清除两个阶段:先标记出所有需要回收的对象,完成收统一回收被标记的对象。
特点:标记和清楚两个过程效率不高导致效率低,
标记清除后会产生大量不、连续的内存碎片,空间碎片太多会导致程序运行中分配较大对象时,无法找到足够的连续内存导致提前触发垃圾垃圾收集动作
-
复制
将内存按容量划分为大小相等的两块,每次只使用一块,当其中一块内存用完了,将还存活的对象复制到另一块上,再把已经使用过的内存空间一次清理掉,每次对整个半区进行内存回收,分配时也不用考虑内存碎片等复杂情况,移动堆指针按顺序分配内存即可,实现简单,运行高效,但这种算法将内存缩小为了原来的一半
-
分代
根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。(当前商业虚拟机的垃圾收集都采用“分代收集”算法)
垃圾回收器(新生代or老年代) 各个收集器的优点 和缺点
-
Serial收集器
单线程收集器单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(虚拟机后台自动发起,自动完成)
应用场景:适用于Client模式下的虚拟机
-
ParNew
多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数和Serial收集器一样存在Stop The World问题
场景::ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
-
Parallel Scavenge
属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)该收集器的目标是达到一个可控制的吞吐量,GC自适应调节策略(与ParNew收集器最重要的一个区别)
补充:
Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
Parallel Scavenge收集器使用两个参数控制吞吐量:
Parallel Scavenge收集器使用两个参数控制吞吐量:
-
XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
-
XX:GCRatio 直接设置吞吐量的大小。
-
-
Serial Old:Serial Old是Serial收集器的老年代版本
特点:同样是单线程收集器,采用标记-整理算法
主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用
-
Parallel Old:
特点:是Parallel Scavenge收集器的老年代版本 多线程,采用标记整理法
应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器
-
cms ⭐ 获取最短回收停顿时间为目标的收集器收集过程与用户线程一起并发执行
特点:基于标记-清除算法实现。并发收集、低停顿
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务
GMS运行过程:
初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
并发清除:对标记的对象进行清除回收。
-
缺点:对CPU资源非常敏感。无法处理浮动垃圾,空间碎片化问题;
-
G1⭐ 一款面向服务端应用的垃圾收集器。
特点:并行并发,分代收集,空间整合,可预期停顿
初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)
内存分配策略
对象优先进eden区:
多数情况下对象在新生代Eden区中分配(当Eden区没有足够空间进行分配时,虚拟机发起Minor GC)
-
大对象直接进入老年代
大对象是指需要大量连续内存的Java对象,(eg:很长的字符数及数组)
-
长期存活的进老年代
-
动态对象判定
-
空间分配担保
-