jvm垃圾收集器和内存分配策略
# 序言
今天讲的是jvm垃圾收集器是如何回收垃圾的,现在我们的生产环境动不动就是几十G,每时每刻产生的垃圾对象也会很多,所以回收他们也是JVM重中之重的任务。
想象下,如果你是环卫工人,你会怎么捡垃圾,首先站在大街上,环顾四周,看看哪些是垃圾,然后在不太忙的时候,先去垃圾比较多的地方收拾垃圾,最后以自己最优美的姿势捡起啦,哦了!jvm和这个捡垃圾过程也十分相像。
jvm回收垃圾的过程很简单,一共就三个步骤:
- 如何判断那些对象是垃圾
- 什么时候触发垃圾回收
- 采用什么算法回收垃圾
# 💠如何判断对象“已死”
因为垃圾回收器的作用是回收无效的对象实例,因此第一件事就是确定对象是否不被使用,即“已死”。
# 引用计数法
对象持有一个引用计数器,当对象被引用时+1,当引用失效时-1,任何时刻对象引用计数器的值为0时,对象被认定为不可使用状态。 优点:原理简单。判定效率高 缺点:
- 对象之间互相引用,会导致对象的引用计数器一直不为0,从而永远不被认定为不可使用状态。
- 性能问题,多线程环境下,需要维护每个对象的引用计数器,可能会有加锁操作,降低系统性能
- 无法处理跨代引用:引用计数法只能处理同一代对象之间的引用关系,无法处理跨代引用。
# 可达性分析算法
首先会有一个 称之为“GC ROOT”的根对象作为起点,我们创建的对象的引用都会链接到“GC ROOT”上,可以想象下,这就是一棵树,“GC ROOT”就是树顶,创建对象的引用根据引用关系在下面开枝散叶。当引用对象到“GC ROOT"之间的引用链断开时,说明该对象不可达,即认定为不可被使用状态。
# 🥡什么时候触发GC(垃圾回收)
先要介绍下,堆内存的分布情况,因为大多数对象在创建后都会很快不再被使用,只有少部分对象会被持续使用,所以也反映到了堆上,采用分代收集
成了大多数虚拟机的收集理论。
堆内存逻辑分布分为新生代(占1/3)和老年代(占2/3),新生代又有1/5的区域是survivor(幸存者分为from和to两个区域)区域
回归正题,由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC
和Full GC
。
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young
、Tenured
和Perm
。Full GC
因为需要对整个堆进行回收,所以比Scavenge GC
要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC
的调节。有如下原因可能导致Full GC
:
a) 年老代(Tenured)被写满;
b) 持久代(Perm)被写满;
c) System.gc()被显示调用;
d) 上一次GC之后Heap的各域分配策略动态变化;
Young
、Tenured
和Perm
解释:
Young区:也叫新生代,是JVM分配给新创建的对象的内存区域。Young区又分为Eden区、Survivor0区和Survivor1区。当一个对象被创建时,首先会在Eden区分配内存空间。当Eden区满时,JVM会执行Minor GC,将还存活的对象移动到Survivor区,如果Survivor区也满了,则会将存活的对象移动到另一个Survivor区,如果另一个Survivor区也满了,则将对象移动到Tenured区。因为Young区通常存活时间较短,因此使用复制算法来进行垃圾回收。 Tenured区:也叫老年代,是JVM分配给长时间存活的对象的内存区域。因为Tenured区存活时间较长,因此使用标记清除算法来进行垃圾回收。当Tenured区满时,JVM会执行Full GC,对整个堆进行垃圾回收,这个过程比Minor GC的开销要大。 Perm区:也叫永久代,是JVM用来存储类、方法等元数据的内存区域。永久代的大小是有限的,因此如果应用程序不断加载类、卸载类,则可能会导致Perm区内存溢出。从JDK8开始,Perm区被元空间(Metaspace)所替代,元空间的内存大小可以动态调整
# Garbage Collection (垃圾回收)算法
# mark-sweep(标记清除)
先标记不可用对象内存地址,然后清除掉
优点:速度快,不卡顿,效率高
缺点:会产生内存碎片,容易内存泄漏
# mark-copying(标记复制)
堆空间分为两块,每次只使用一块,当一块内存用完了,先将不可用对象标记,再移动活下来的对象到另一块区域,新生代会使用。
优点:分配内存方便,没有碎片
缺点:如果是大量对象存活,会增加移动成本,每次只有一半内存可以使用
# mark-compact(标记整理)
标记好不可适用对象地址后,针对活下来的对象往一块内存区域移动,标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题;老年代大多数对象仍然会被使用,适合老年代。
优点:分配内存方便,没有碎片,内存利用率高
缺点:移动对象特别耗时,效率最低
# 收集器
- Serial收集器
- Serial Old收集器
- ParNew收集器
- Parallel Scavenge收集器 关注吞吐量 ,是基于标记-整理算法的
- Parallel Old收集器
- CMS收集器 关注延迟 是基于标记-清除算法的
- Garbage First收集器
# 疑问
# 1.根可达算法如何解决循环引用的呢?
在根可达算法中,当JVM遍历堆中的对象时,如果发现一个对象A被引用了,那么JVM就会将对象A标记为存活对象,并继续遍历对象A所引用的其他对象。如果在遍历对象A所引用的其他对象时,发现某个对象B又引用了对象A,那么JVM就会暂时将对象A标记为“可疑对象”,并将对象B继续遍历,直到遍历完所有的对象后,再次遍历所有的对象,重新检查所有可疑对象,如果发现某个可疑对象仍然被引用了,那么就将其标记为存活对象,否则就将其标记为垃圾对象。
这种方法可以解决循环引用的问题,因为在第一次遍历时,对象A会被标记为可疑对象,而不是垃圾对象,因此JVM会继续遍历对象B,直到遍历完所有的对象后,再次检查可疑对象,如果对象A仍然被引用了,那么就将其标记为存活对象,否则就将其标记为垃圾对象。这样,即使对象A和对象B互相引用,也不会被错误地标记为垃圾对象。
# 参考资料:
- https://www.cnblogs.com/1024community/p/honery.html
- 《深入理解Java虚拟机3》
- 马士兵jvm
- https://segmentfault.com/a/1190000040742205 动图来源