Java和Go的内存管理学习
AI摘要: 本文介绍了Java和Go的内存管理学习,通过GC实现自动内存管理,有助于深入理解性能瓶颈。GC分为三种:Series GC、Parallel GC和Concurrent GC,各有其特点和适用场景。GC策略包括Copying GC、Mark-sweep GC和Mark-compact GC,每种策略都有其优缺点。引用计数是判断死亡对象的一种方法,但存在缺点如线程不安全和无法处理环形结构。Java内存管理技术经过多代发展,采用分代回收法,根据对象的存活时间长短选择不同的清理策略。Golang采用三色标记法来追踪死亡对象,通过白色、灰色和黑色分别表示可以回收、正在检查和不能回收的对象。
Java和Go都是通过GC来实现自动内存管理,提高了编码效率和安全性。通过学习自动内存管理,有助于深入理解Java和Go在某些场景下的性能瓶颈。
GC相关概念
从图中可以看出,业务线程申请对象,而GC线程回收未被使用的对象。
根据场景不同,GC分为三种:
-
Series GC:单线程GC,并且会造成停顿
-
Parallel GC:多线程GC,依然会造成停顿
-
Concurrent GC:混合并发GC,不会造成停顿
GC策略
为了提高内存利用率,GC 会整理内存,使存活对象集中排列,空闲内存形成一个大块。这种整理可以:
-
提高内存分配效率。
-
避免分配大对象时反复尝试。
-
减少碎片导致的性能问题。
当某个对象被确认为垃圾对象时,GC也会有几种不同的清理策略
:
-
Copying GC:将存活对象复制到另外的内存空间(额外的空间)
存活对象的原来存储空间就可以用来分配其他对象。当只有少量存活对象(比如新生代对象就只有少量存活),移动到额外空间效率很高,此时原来剩下的空间就是完整可分配的大块内存。当有大量存活对象(如老年代的对象存活时间很久),此时移动大量对象到新空间就很耗时。
-
Mark-sweep GC: 将死亡对象的内存标记为可分配
将死亡对象的存储空间使用free list链表存储起来,当下次需要分配对象的时候就从free list中找空间。但是,Mark-Sweep算法的缺点只标记和清除对象,而没有整理内存,内存碎片的问题是依旧存在的,可能无法为大型对象分配内存。
-
Mark-compact GC:移动并整理存活对象
将存活对象原地整理,移动到最开始的位置,也就是整理和压缩存活对象(不使用额外内存,没有多余空间,和Copying GC最大的差别) , 那么黑色块后面的大块空间就能继续分配其他对象。老年代的移动采用的便是这种方式,移动的时间停顿是无法避免的,但是能避免额外的内存空间占用。
引用计数
我们知道死亡对象的三种不同GC策略,但是还没有学习如何判断一个死亡对象。常见的方法即引用计数,当一个对象没有任何引用指向它,就意味着它被抛弃了,可以判定为一个死亡对象。当然,引用计数并不是万能的,也有很多的缺点。
-
优点:
- 简单便捷:从编程的角度,引用计数对程序员天然透明,开发更加便捷,减少了心智负担
-
缺点:
-
引用计数本身是通过原子操作来实现原子性和可见性 ,天然降低了性能。并发环境中,多个线程都会操作同一个对象,线程不安全都得都懂;
-
无法处理环形结构,也就是循环应用;
比如图中的红色对象形成了一个环形结构,环形里的对象已经不可达,完全应该回收掉,但是基于引用计数难以实现。
当然,不同语言中有应对策略,比如C++中采用weak_ptr来解决。
-
内存开销。每个对象都需要额外维护一个存储空间来存储引用数目
-
Java内存管理技术
Java的内存管理技术经过多代的发展,相对比较成熟。
分代回收法
基于“大部分对象存活时间都很短,少部分对象是长寿的,应该采用不同的清理策略”的假设。比如函数中生成的对象,函数执行时间很短,那么对象在生成之后也立刻被抛弃,这是短寿对象。
-
年轻代的GC策略
- 存活数量很少,所以适合采用
Copying GC
,全部都挪到一个固定区域
- 存活数量很少,所以适合采用
-
老年代的GC策略
- 对象趋向于一直活着,反复复制的开销很大,适合
mark-sweep GC
。通过free list把死亡对象串起来,下次要分配对象就从free list中取空间
- 对象趋向于一直活着,反复复制的开销很大,适合
Golang内存管理技术
三色标记法
Golang采用三色标记法来追踪死亡对象,具体过程如下:
-
假设所有的对象都是未使用的,是可以回收的,全部标记为白色
-
找到某个正在被引用的对象,以此查找这个对象引用的其他对象,并标记为灰色,然后将自己改成黑色
-
黑色代表已经被检查,不能回收的对象,白色表示可以回收的对象,清理掉全部的白色对象