自动内存管理之垃圾回收-Java虚拟机(二)

1 哪些内存需要回收?

回顾Java虚拟机运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而死。
方法开始分配一个栈帧大小的内存,每个栈帧的大小基本上在类结构确定下来时就已知了。方法结束或者线程结束,内存自然就随着回收了。所以这些区域内存分配和回收都具备确定性。

Java堆和方法区就有着显著的不确定性,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分的内存分配和回收是动态的。

2 什么时候回收?

2.1 引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一。
当引用失效时,计数器值减一。
任何时刻计数器为零的对象就是不可能再被使用的。

主流Java虚拟机里都没有选用引用计数算法来管理内存,因为单纯的引用计数很难解决对象之间相互循环引用的问题

引用计数算法无法解决这样的部分连通问题

public static void main() {
	MyObject referenceCountingGCA = new MyObject();
	MyObject referenceCountingGCB = new MyObject();
	referenceCountingGCA.child = referenceCountingGCB;
	referenceCountingGCB.child = referenceCountingGCA;
}

class MyObject{
	MyObject child;
}

2.2 可达性分析算法

通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到“GC Roots”间没有任何“引用链”相连(或者用图论的话来说就是从“GC Roots”到这个对象不可达)时,证明此对象不可能再被使用。

固定可作为“GC Roots”的对象:

  1. 虚拟机栈本地变量表中引用的对象
  2. 方法区类静态属性引用的对象
  3. 本地方法栈中JNI(Native)引用的对象
  4. 虚拟机内部的引用(如,基本数据类型对应的Class对象,常驻的异常对象,类加载器等)
  5. 被同步锁持有的对象
  6. 在JMXBean、JVMTI中注册的回调、本地代码缓存等

至少要经过两次标记,对象才能被判定为“死亡”。
第一次被标记为不可达之后,虚拟机将为覆盖finalize()且重未执行过finalize()方法的对象执行一次该方法(低调度优先级的Finalizer线程执行),因此finalize()方法可以进行对象第一次被标记后的自我拯救。
不过这个方法运行代价高昂,不确定性大,不推荐使用

2.3 方法区的回收

方法区的垃圾收集主要回收:废弃的常量不再使用的类型
方法区的回收判定条件苛刻,因此性价比通常是较低的。

常量的废弃判定条件:该常量不被任何对象引用

不再被使用的类型判定条件:

  1. 该类所有实例都已经被回收
  2. 加载该类的类加载器已经被回收(如OSGi、JSP重加载类的场景)
  3. 该类对象的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过放射访问该类的方法

在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

3 怎样进行回收?垃圾收集算法

3.1 分代收集理论

1)绝大多数对象都是朝生夕灭的--弱分代假说
2)熬过越多次垃圾收集过程的对象就越难以消亡——强分代假说

根据假说(1)和(2)我们可以将回收对象依据年龄(对象熬过垃圾收集过程的次数)分配到不同的区域存储。
每次可以只回收其中某一个或者某些部分的区域,因此可以划分出不同的回收类型(如,“Minor GC/Young GC”“Major GC/Old GC”“Full GC”等)
针对不同的区域,安排与里面存储对象存亡特征相匹配的垃圾收集算法(如,“标记-复制算法”“标记-清除算法”“标记-整理算法”等)

实际情况中,设计者一般会把Java堆划分为新生代(Yong Generation)和老年代(Old Generation)两个区域,每次回收后存活的少量对象将会逐步晋升到老年代中存放。

3)跨代引用相对于同代引用来说仅占极少数--跨代引用假说

然而,对象不是孤立的,对象之间会存在跨代引用。所以在部分区域回收过程中,还需要保证被其他区域引用的对象也需要存活。好在,存在互相引用关系的两个对象倾向于同时生存或者同时消亡,根据假说(3),我们可以只关注少数跨代引用。
建立以及维护一个记忆集,标记存在跨代引用的老年代,当发生Minor GC时,将包含跨代引用的老年代对象添加到GC Roots集中

3.2 标记-清除

标记出所有需要回收的对象,在标记完成后,统一回收掉所以被标记的垃圾对象。

1)执行效率随着需要清除的对象数量增长而降低
2)标记-清除后会产生大量不连续的内存碎片,不利于后续分配大对象

3.3 标记-复制

半区复制:
将内存按容量大小划分为大小相等的两块,当其中一块的内存用忘了,就将还存活的对象复制到另一块上,再将原本的块内存空间一次清理掉。
代价是,可用内存缩小为了原本的一半。

一块较大的Eden空间和两块较小的Survivor空间:
每次分配内存只使用Eden和其中一块Survivor,当发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另一块Survivor上,最后直接清理掉Eden和原本的Survivor空间。
HotSpot中默认Eden和Survivor大小比为8:1,即新生代可用容量为90%。然而,Survivor空间可能存在不足以容纳一次Minor GC存活对象的情况,所以需要依赖其他内存区域(如,老年代)留出一定的空间作为担保

1)执行效率随着对象存活率的增高而降低
2)能够使用的内存空间降低,或者需要额外的空间进行分配担保

3.4 标记-整理

标记所有需要回收区域对象的存活情况,让所有存活对象都向内存空间一端移动,最后清洗掉存活边界以外的内存。

1)移动对象时必须暂停用户应用程序(Stop The World)
2)移动对象可以解决空间碎片化的问题(不然就只能通过分页管理内存分配等方式来解决这个问题,而对象访问操作频繁,地址转换会明显影响程序性能),但会增加垃圾收集的停顿时间,是否移动对象各有利弊

4 实际应用:垃圾收集器

4.1 经典垃圾收集器

经典7种作用于不同分代的收集器。两个收集器之间存在连线,就说明他们可以搭配使用
|325

4.1.1 Serial/Serial Old收集器


它们都是单线程收集器
优点:简单而高效,它是所有收集器里额外内存消耗最小的,没有线程交互的开销。
场景:对于单核处理器或者核心数较少的处理器,Serial收集器用作垃圾收集可以获得最高的单线程收集效率;服务端模式下也可以作为后备预案使用
主要用途:客户端模式下的HotSpot虚拟机使用

4.1.2 ParNew收集器


ParNew收集器是Serial收集器的多线程并发版本
场景:多核处理机上运行,还可以与CMS收集器搭配使用
主要用途:运行在服务端模式下的HotSpot虚拟机上

4.1.3 Parallel Scavenge/Parallel Old收集器


Parallel Scavenge收集器基于标记-复制算法,Parallel Old收集器基于标记-整理算法实现。它们也是能够并行收集的多线程收集器。
这两款垃圾收集器的特别之处是,它们的目标是达到一个可控制的吞吐量,所谓吞吐量:

=+

通过参数-XX: MaxGCPauseMillis最大垃圾收集停顿时间、-XX: GCTimeRatio吞吐量大小,可以精确控制吞吐量

4.1.4 CMS(Concurrent Mark Sweep)收集器


CMS收集器是以获取最短回收停顿时间为目标的收集器,CMS收集器是基于标记-清除实现的。
场景:许多服务端(如,B/S系统中的服务器)上,比较关注服务的响应速度,以给用户带来良好的交互体验

运行过程四个步骤:

  1. 初始标记(Stop The World)
    标记GC Roots能直接关联到的对象,速度很快。

  2. 并发标记
    从GC Roots的直接关联对象开始遍历整个对象图,耗时较长,但是可以与用户线程并发运行。

  3. 重新标记(Stop The World)
    修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,停顿时间稍长。

  4. 并发清除
    清理删除标记阶段“死亡”对象,可以与用户线程并发运行。

缺点:
1)垃圾收集线程会占用部分处理器计算资源,导致应用程序变慢,降低总吞吐量。当处理器核心数量不足四个时,CMS对用户程序的影响就可能很大
2)垃圾收集线程与用户线程并发执行,当次收集无法清理正在产生的“浮动垃圾”,一旦空间不足,就会触发一次Full GC。同时为了保证用户线程的持续运行,还需要预留老年代内存空间给用户线程使用,一旦空间不足,就会临时启用Serial Old进行Old GC。
3)标记-清除算法也意味着大量空间碎片的产生,当无法分配大对象时,就会触发Full GC。

4.1.5 G1(Garbage First)收集器

4.2 低延迟垃圾收集器

4.2.1 Shenandoah收集器

4.2.2 ZGC(Z Garbage Collector)收集器

4.3 垃圾收集器的选择

5 内存自动回收后,如何自动分配

posted @   狎客  阅读(50)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示