笔者就Java的垃圾回收机制进行一番复习和探讨
两种情况
先来看创建对象的详细过程
简略版的过程
符合逃逸分析的对象就会被分配在栈中
垃圾收集器会对Java堆里面的对象进行判断,判断里面的某一个对象是存活还是死亡
判断对象为死亡才会进行回收
方式描述
1.给 Java 对象添加一个引用计数器
2.每当有一个地方引用它时,计数器 +1;引用失效则 -1;
判断对象存活准则
当计数器不为 0 时,判断该对象存活;否则判断为死亡(计数器 = 0)。
优点
缺点
作者:Carson带你学Android
链接:https://juejin.cn/post/6844903684036362254
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
很多主流商用语言(如Java、C#)都采用 引用链法 判断 Java对象是否存活。
含3个步骤:
将一系列的 GC Roots 对象作为起点,从这些起点开始向下搜索。
如图,当一个对象到 GC Roots 没有任何引用链相连时,则判断该对象不可达
没有任何引用链相连 = GC Root到对象不可达 = 对象不可用
注意:
不可达对象会被放在”即将回收“的集合里。
要判断一个对象真正死亡,还需要经历两个阶段:
a. 不筛选:继续留在 ”即将回收“的集合里,等待回收;
b. 筛选:从 ”即将回收“的集合取出
筛选的标准:该对象是否有必要执行 finalize()方法
若有必要执行(人为设置),则筛选出来,进入下一阶段(第二次标记 & 筛选);
若没必要执行,判断该对象死亡,不筛选 并等待回收
当对象无 finalize()方法 或 finalize()已被虚拟机调用过,则视为“没必要执行”
a. 方式描述
该对象会被放到一个 F-Queue 队列中,并由 虚拟机自动建立、优先级低的Finalizer 线程去执行 队列中该对象的finalize()
注:
1.finalize()只会被执行一次
2.但并不承诺等待finalize()运行结束。这是为了防止 finalize()执行缓慢 / 停止 使得 F-Queue队列其他对象永久等待。
b. 筛选标准
在执行finalize()过程中,若对象依然没与引用链上的GC Roots 直接关联 或 间接关联(即关联上与GC Roots 关联的对象),那么该对象将被判断死亡,不筛选(留在”即将回收“集合里)并等待回收
实际虚拟机更多使用可达性分析法
一般认为有四种
介绍:
按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。
优点:
不容易产生内存碎片
缺点:
内存使用率不高,只有原来的一半。
介绍:
分为俩阶段
标记阶段:标记出可以回收的对象。
清除阶段:回收被标记的对象所占用的空间。
优点:
实现简单,不需要对象进行移动。
缺点:
效率不高,无法清除垃圾碎片。容易产生内存碎片
介绍:
标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
优点:
解决了标记-清理算法存在的内存碎片问题。
缺点:
仍需要进行局部对象移动,一定程度上降低了效率。且会暂停用户线程,效率比较低
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思
义是根据对象的存活周期将内存划分为几块。
一般包括年轻代、老年代和永久代,如图所示:
新生代基本采用复制算法,老年代采用标记整理算法。
垃圾收集算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现
以下是关系图,其中不同收集器的连线表示他们可以搭配使用
对以上垃圾回收器的具体介绍
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来
获得最短回收停顿时间的垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内
存碎片,当剩余内存不能满足程序运行要求时,系统将会出现
Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾
清除,此时的性能将会被降低
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺
点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃
圾回收。
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间
的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、
From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,
当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进
入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记
整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执
行流程。
内存管理,包含内存分配和内存回收两个方向。以上介绍了内存回收的方程,下面重点阐述
内存分配过程。
对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,
某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代
的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分
配。少数情况下也会直接在老年代上分配。
多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空
间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没
有足够的空间,则将启用分配担保机制在老年代中分配内存。
这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从
日志中发现 Major GC/Full 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) 就会被晋升到老年代。
Java在部分场合下也会在栈中分配内存,这里就涉及到逃逸分析,这是比较前沿的虚拟机管理技术
逃逸分析的主要作用就是分析对象作用域。
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种行为就叫做 方法逃逸。甚至该对象还可能被外部线程访问到,例如赋值被类变量或可以在其他线程中访问的实例变量,称为 线程逃逸。
通过逃逸分析技术可以判断一个对象不会逃逸到方法或者线程之外。根据这一特点,就可以让这个对象在栈上分配内存,对象所占用的内存空间就可以随帧栈出栈而销毁。在一般应用中,不会逃逸的局部对象所占比例很大,如果能使用栈上分配,那么大量的对象就会随着方法的结束而自动销毁了,垃圾收集系统的压力就会小很多。
除此之外,逃逸分析的作用还包括 标量替换 和 同步消除 ;
标量替换 指:若一个对象被证明不会被外部访问,并且这个对象可以被拆解成若干个基本类型的形式,那么当程序真正执行的时候可以不创建这个对象,而是采用直接创建它的若干个被这个方法所使用到的成员变量来代替,将对象拆分后,除了可以让对象的成员变量在栈上分配和读写之外,还可以为后续进一步的优化手段创造条件。
同步消除 指:若一个变量被证明不会逃逸出线程,那么这个变量的读写就肯定不会出现竞争的情况,那么对这个变量实施的同步措施也就可以消除掉。
但是目前的Java虚拟机并没有直接应用逃逸分析技术,因为其实现起来比较复杂,包括Hotspot虚拟机也同样未进行使用