垃圾回收
什么情况下JVM内存中的一个对象会被垃圾回收?
什么时候会触发垃圾回收?
新生代里的对象越来越多,快满了,就会触发垃圾回收,把新生代没有人引用的对象给回收掉,释放内存空间
垃圾回收哪些对象?
JVM中使用了一种可达性分析算法来判定哪些对象是可以被回收的,哪些对象是不可以被回收的。
在JVM规范中,局部变量就是可以作为GC Roots的只要一个对象被局部变量引用了,那么就说明他有一个GC Roots,此时就不能被回收了。
譬如:Person p = new Person();
new Person 这个对象 就被引用变量 p 引用 所以 这个对象不会被垃圾回收
一句话总结:象被方法的局部变量、类的静态变量给引用了,就不会回收他们。
Java中对象不同的引用类型
java里面有强引用、软引用、弱引用和虚引用。
强引用: Person p = new Person(); 这就是强引用, 垃圾回收回收不了强引用对象
软引用:SoftReference p = new SoftReference(new Person());
正常情况下垃圾回收是不会回收软引用对象的,但是如果你进行垃圾回收之后,发现内存空间还是不够存放新的对象,内存都快溢出了此时就会把这些软引用对象给回收掉,哪怕他被变量引用了,但是因为他是软引用,所以还是要回收。
弱引用:WeakReference p = new WeakReference(new Person());
弱引用就跟没引用是类似的,如果发生垃圾回收,就会把这个对象回收掉。典型的应用在 ThreadLocal,ThreadLocal 引起内存泄漏的原因之一就是设计的时候 用了弱引用。
GC Roots 算法搜寻根节点枚举 为什么会 STW?
迄今为止,所有的收集器在根节点枚举这一步骤时都必须暂停用户线程, GC Roots 可达性算法查找引用链这个过程是可以做到和用户线程一起并发的, 但根节点枚举始终必须保持在某个快照中才能保证一致性, 快照一致性是指 在某个时间节点,系统之间的所有对象都像是被冻结在某个时间点上,不会出现分析的过程中根节点还和对象的引用关系还在不断变化的情况。 这也是垃圾收集器过程中必须停顿用户线程的一个重要原因, 即使是号称停顿时间可控的 CMS、G1、ZGC 等收集器 枚举根节点的时候也必须是要停顿的!
垃圾回收!开始你的表演!
新生代如何回收
1.标记-清除算法:把需要回收的对象标记出来,再统一回收。 缺点:1、IBM统计 新生代有 98% 的对象都需要进行垃圾回收,执行时间太久效率太低, 2、会产生大量不连续的空间碎片,不利于空间的利用,造成内存的浪费(分配大对象的时候需要连续的内存空间) 2. 标记-复制算法:把新生代分成两块,一次只用一块,把不需要回收的对象标记出来,复制到另一块,然后将这一块的内存使用全部清理掉。 缺点:浪费了一半的内存空间,
主流的新生代设计是, 1个Eden区,2个Survivor区,其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说新生代有 1G 内存,Eden区有800MB内存,Survivor 区 又分为 Survivor1 和 Survivor2,每一块Survivor区就100MB内存。 每次只能使用 Eden 区 和 一块 2个Survivor 区
当Eden 区快满了就会进行垃圾回收,把 Eden 区 和 Survivor1 的存活下来的对象都放到 Survivor2, 所以 Survivor 区存放的对象都是上一次 YoungGC 后存活的对象。
这么做最大的好处,就是只有10%的内存空间是被闲置的,90%的内存都被使用上了。
无论是垃圾回收的性能,内存碎片的控制,还是说内存使用的效率,都非常的好。
老年代如何回收
标记-整理算法: 将所有存活对象标记出来,让这些对象移动到内存的一端,然后直接清理掉边界以外的内存。
缺点:老年代里都是大对象居多,移动并更新大对象的引用是一个很重的操作,所以要停止用户进程才行,也就是 STW 的原因。
如果不移动 采用标记-清除:又会有很多空间碎片的问题,,只能通过内存分配器和内存访问器来解决,但这就给内存分配增加了负担,就会影响应用程序的吞吐量。
基于以上: 1、移动对象则内存回收的时候会更复杂,不移动对象分配内存会更复杂。 2、Paraller Scavenge 收集器 基于标记整理算法 关注延迟的 CMS 收集器基于 标记 - 清除算法
老年代垃圾回收的效率,据统计比新生代的回收算法慢10倍 频繁的 Full GC 会造成系统卡顿,假死。
哪些对象会从新生代进入到老年代?
1. 躲过15次GC之后进入老年代
对象在新生代每经过一次GC,GC年龄就 +1 ,当年龄达到 15 岁的时候就会被转移到老年代去, 可以通过 JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁
2. 动态对象年龄判断
动态年龄判断的规则是:一批对象的总大小大于了这块Survivor区域的内存大小的50%,那么此时大 于等于这批对象年龄的对象,就可以直接进入老年代了。 举例理解: 某个Survivor区有如下年龄的对象: 年龄1+年龄2+ ... + 年龄n 其中 年龄n 的多个对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代。
原因是: 无论是15岁的那个规则,还是动态年龄判断的规则,都是希望那些可能是长期存活的对象,尽早进入老年代
既然你是长期存活的,那么老年代才是属于你的地盘,不要在新生代里占地方。
3. 大对象直接进入老年代
典型的大对象便是那种很长很长的字符串,或者元素很多很庞大的数组,大对象压根就不会经过新生代,而是直接进入老生代, 因为在移动这些大对象时需要高额的内存复制开销,在两个Survivor区域复制来复制去15次 显然是不小的开销,直接把它送到老年代,避免复制, 在 full GC 的时候直接干掉也可以, 所以我们平时写代码的时候要注意不要创建大对象, 那哪些是大对象呢? 有一个JVM参数,就是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节 就是 1MB 超过1M 的就是大对象
4. Minor GC后的对象太多,无法放入Survivor区怎么办?
Survivor区放不下,这个时候就必须得把这些对象直接转移到老年代去
5. 老年代空间分配担保机制
在每次youngGC 前都会检查 老年代的内存大小是不是大于新生代所有对象,如果大于,那么久可以放心大胆的进行 YoungGC, 如果小于呢? 就要用到老年代空间分配的担保机制了。
该机制 由 -XX:-HandlePromotionFailure 参数是否设置了为准
1、设置了该参数,就会判断老年代的剩余内存大小是否大于之前每一次 YoungGC 后进入老年代对象的平均大小。 如果老年代剩余内存的大小比 之前 YoungGC 后进入老年代对象的平均大小 要大,那么就可以冒一点风险进行 YoungGC. 但此时进行 YoungGC 有几种可能: 1、 YoungGC 后,存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor区域即可 2、 YoungGC后,剩余的存活对象的大小,是大于 Survivor区域的大小,但是是小于老年代可用内存大小的,此时就直接进入老年代即可。 3、 很不幸,Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触发一次“Full GC。 FullGC 会对老年代回收 也会对新生带回收,老年代被回收后,如果还要空间就会把 上次YoungGC 存活的对象放到老年代,如果空间不够了 就会导致 OOM。
2、没有设置这个参数: 直接 进行 Full GC 所谓担保就是认为这次 YoungGC 后Eden 区和 Survivor区的对象能转移到另外一个 Survivor 区,担保人是老年代。
JVM 调优究竟是干嘛?
所谓JVM优化,就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收