垃圾回收
垃圾回收GC
GC的概念及意义?
概念:垃圾回收,释放jvm内存,可以在一定程度上避免OOM问题。
意义:每当我们新建对象时,jvm就会自动监控这个对象的地址大小与使用情况,通过可达性分析算法寻找不可达对象,判断是否需要进行GC释放内存。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
对象的引用类型?
强引用:发生gc时不会收回强引用所关联的对象,比如new
软引用:有用但非必须的对象,在OOM之前会把这些对象列进回收范围之中进行第二次回收,若第二次回收还没有足够的内存,则会抛出OOM。也就是第一次快要发生OOM的时候不会立马抛出OOM,而是会回收掉这些软引用,然后再看内存是否足够,若还不够才会抛出OOM。
弱引用:有用但不是必须的对象,在下一次GC时会被回收
虚引用:也叫幽灵引用/幻影引用,无法通过虚引用获得对象,他的意义在于能在这个对象被GC掉时收到一个系统通知
垃圾回收算法?
标记清除算法
所有的垃圾回收算法都是以它为基础的。
分为两步:
标记和清除:
首先需要标记出所有需要回收的对象,然后进行清除回收变为可用内存。
优点:
实现简单,不需要对象进行移动。
缺点:
算法效率低,会产生垃圾碎片,提高了垃圾回收的频率
复制算法
将可用堆内存按照容量分为大小相等的两块,每次只用一块,当这块内存快用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存一次清理掉。年轻代from/to(s1/s2)采取的就是此种算法。老年代一般不会采取此种算法,因为老年代都是大对象且存活的久的,空间压缩一半代价略高。
优点:运行高效,不会产生碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
标记整理算法
在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。回收后,已用和未用的内存都各自一边。
分代收集算法
根据对象存活周期的不同将内存划分为几块,一般是新生代、老年代,新生代基本采用复制算法,老年代采用标记整理算法。
分代原因
不同对象生命周期是不一样的,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间会相对较长,也有很多对象完全没必要遍历,比如大对象存活的时间更长,遍历下来发现不需要回收,造成资源性能的浪费。所以就有了分代收集算法,进行区域划分,把不同生命周期的对象放在不同的区域,不同的区域采取最适合他的垃圾回收方式进行回收。
分代工作流程
新生代一般采用复制算法,有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1
老年代一般采用标记整理算法,提高回收效率。
新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代:
把 Eden From Survivor 存活的对象放入 To Survivor 区;
清空 Eden 和 From Survivor 分区;
From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 1,当年龄到达 15(默认配置是 15)时,升级为老年代。大对象也会直接进入老年代。
老年代:老年代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
垃圾回收器
新生代回收器(3个)
Serial
采取复制算法,用于新生代,单线程收集器,所以在他工作时会产生StopTheWorld(GC卡顿)。单线程情况下效率更高,比如用于GUI小程序。
ParNew
采取复制算法,用于新生代,是Serial的多线程版本,多个GC线程同时工作,但是也会产生StopTheWorld(GC卡顿),因为不能和工作线程并行。
Parallel Scavenge
采取复制算法,用于新生代,和ParNew一样,所以也会产生STW,多线程收集器,他是吞吐量优先的收集器,提供了很多参数来调节吞吐量。
老年代回收器(3个)
Serial Old
Serial的老年版本,采取标记整理算法,用于老年代,单线程收集器,所以在他工作时会产生StopTheWorld(GC卡顿)。单线程情况下效率更高,比如用于GUI小程序
Parallel Old
采取标记整理算法,用于老年代,Parallel Scavenge收集器的老年代版本,吞吐量优先。
CMS
采取标记清除算法,老年代并行收集器,号称以最短STW(GC卡顿)时间为目标的收集器,并发高、停顿低、STW(GC卡顿)时间短的优点。主流垃圾收集器之一。
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX: UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。CMS 使用的是标记清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure(并发模式失败 老年代正在清理,从新生代晋升了新的对象,或者直接分配大对象新生代放不下导致直接在老年代生成,这时候老年代也放不下),临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
整堆回收器
G1
采取标记整理算法的并行收集器。对比CMS的好处之一就是不会产生内存碎片,此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。而且他的STW(GC卡顿)停顿时间是可以手动控制一个长度为M毫秒的时间片段(可以用JVM参数 -XX:MaxGCPauseMillis指定),设置完后垃圾收集的时长不得超过这个(近实时)。
详解CMS
采取标记清除算法,老年代并行收集器,号称以最短STW(GC卡顿)时间为目标的收集器,并发高、停顿低、STW(GC卡顿)时间短的优点。主流垃圾收集器之一。
工作流程:
初始标记:标记 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。所以此阶段会STW,但时间很短。
并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。不会STW。
重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。STW时间会比第一阶段稍微长点,但是远比并发标记短,效率也很高。
并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。
优点:
①并发高②停顿低③STW时间短
缺点:
对cpu资源非常敏感(并发阶段虽然不会影响用户线程,但是会一起占用CPU资源,竞争激烈的话会导致程序变慢)。
无法处理浮动垃圾,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,失败后而导致另一次Full GC的产生,由于CMS并发清除阶段用户线程还在运行,伴随程序的运行自然会有新的垃圾产生,这一部分垃圾是出现在标记过程之后的,CMS无法在本次去处理他们,所以只好留在下一次GC时候将其清理掉。
内存碎片问题(因为是标记清除算法)。当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure(并发模式失败),临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
详解G1
采取标记整理算法的并行收集器。
特点:
并行与并发执行:利用多CPU的优势来缩短STW时间,在GC工作的时候,用户线程可以并行执行。
分代收集:无需其他收集器配合,自己G1会进行分代收集。
空间整合:不会像CMS那样产生内存碎片。
可预测的停顿:可以手动控制一个长度为M毫秒的时间片段(可以用JVM参数 -XX:MaxGCPauseMillis指定),设置完后垃圾收集的时长不得超过这个(近实时)。
步骤:初始标记:仅仅标记GCRoots能直接关联到的对象,且修改TAMS的值让下一阶段用户程序并发运行时能正确可用的Region中创建的新对象。速度很快,会STW。
并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。不会STW。
最终标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。STW时间会比第一阶段稍微长点,但是远比并发标记短,效率也很高。
筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
原理:
G1并不是简单的把堆内存分为新生代和老年代两部分,而是把整个堆划分为多个大小相等的独立区域(Region),新生代和老年代也是一部分不需要连续Region的集合。G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
Region不是孤立的,也就是说一个对象分配在某个Region中,他并非只能被本Region中的其他对象引用,而是整个堆中任意的对象都可以相互引用,那么在【可达性分析法】来判断对象是否存活的时候也无需扫描整个堆,Region之间的对象引用以及其他手机其中新生代和老年代之间的对象引用虚拟机都是使用Remembered Set来避免全堆扫描的。
Minor GC与Full GC分别在什么时候发生
Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。发生原因:①老年代被写满(默认90%)②System.gc()被显示调用。
栈上分配
JVM允许将线程私有的对象分配在栈上,而不是分配在堆上。分配在栈上的好处是栈上分配不需要考虑垃圾回收,因为出栈的时候对象就顺带着一起出去了,没了,而不需要垃圾回收器的介入,从而提高系统性能。
对象逃逸
TLAB
对象分配规则
对象的内存分配通常是在 Java 堆上分配,对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在老年代上分配。遵循以下规则:
对象优先在 Eden 区分配
对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。
大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。
长期存活对象将进入老年代
虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。
动态判断对象的年龄
动态判断对象的年龄,如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。无需等到-XX:MaxTenuringThreshold参数要求的年龄。
空间分配担保
每次进行YGC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行YGC,如果false则进行Full GC。