面试官喜欢问的GC那些事

最近在找工作,因为之前的主力语言是C#,所以每次在面试的时候无一例外的都被问到了.Net的GC相关的问题.同样的问题我几乎每次面试的时候都要回答一遍,也是让人哭笑不得(这里就不得不提一下在微软面试的时候,就没有问这些热门问题).但是换一个角度想一想,基本上带runtime的语言,其GC的实现都是一个比较核心的技术点,并且直接影响到这门语言的性能表现,如果面试者完全不了解或者丝毫不关心这一块的东西,那毫无疑问是不合格的.今天正好就借这个机会,把我所了解的.Net的GC的相关技术细节做一个比较全面的总结.

为什么我们需要一个gc

dotnet官方文档认为gc主要有以下几个优点

  • 让开发者无需手动释放内存
  • 高效的分配对象到托管堆上
  • 自动回收对象,整理内存碎片
  • 内存安全

总的来说,gc最主要的作用或者说gc被设计出来的主要目的就是解决程序长时间运行的内存使用问题.

gc的工作原理

一般来说,gc的主要工作流程就是在程序的堆上对象比较多的时候遍历一遍,找出那些没有引用或者说程序无法访问到的对象,并将其回收,释放掉其占用的内存.如何确定一个对象没有被引用,最简单的方法就是引用计数.即使在每个对象上加一个计数器,每当有别的地方引用到了它,计数器值加1,当计数器值为0的时候就表示没有引用了,可以释放.但是这种实现有一个明显的缺点,那就是没有办法解决循环引用问题.举个例子,现在有两个对象A和B,A中有指向B的引用,B中有指向A的引用,但是程序中再没有别的地方有指向这两个对象的任何引用,这时候单纯的引用计数是没有办法检测出A,B其实都是可以回收的对象的.所以现在的虚拟机语言如C#,Java等其实用的是另一种实现,通过定义一些root的对象,然后在gc时尝试让每个对象生成到root对象的可达图,如果没有可达路径,则表示这个对象是无法访问的,可以回收.通常这些root对象可以是一个栈,线程对象或者全局对象等.

1
2
3
4
5
6
7
8
9
10
11
Root            Root
---------------------
|
A
|\
B C E
| |\
D F-G


这种情况下虽然E,F,G三个对象都有被引用,但是他们和任何一个root对象都没有任何关系了,所以程序集中其余地方也不能访问到,所以是可以回收的

gen0, gen1和gen2

前面说的只是gc工作的基本原理,dotnet的实际实现远远比这要复杂.在CLR中,托管堆分为3代:gen0,gen1和gen2.其中gen0一般用于存储生命周期较短的对象比如临时变量,同时也是gc发生最频繁的地方.一般较小的新对象都是分配在gen0堆上,如果对象比较大,会直接分配到gen2堆上去.每次gc过后,gen0堆中剩余的没被释放的对象就会被移动到gen1堆中,gen1堆中没被释放的则会移动到gen2堆中.一般来说gen2堆中的主要就是一些生命周期比较长的对象诸如全局对象.

gc时发生了什么

一次典型的gc工作流应该是这样:

  • 标记可回收的对象
  • 更新指向堆上对象的引用
  • 回收死掉对象的占用的空间,将原来分配在这段内存之后的对象移动过来

第一步很简单,辨别一下敌我,弄清楚哪些要回收,哪些可以继续保留.第二步开始重新整理对象之间的引用关系.第三步会重新整理堆上的内存分配.

Large Object Heap

上面说到的gc工作流的第三步可能会涉及大量的内存拷贝,比如移动的对象本身比较大.为此,dotnet针对大于85000byte的大对象,采用了专门的处理方式(至于为什么是85000这个值,dotnet官方说这是一个经验值).其实就是不轻易移动大对象,较大对象放在专门的LOH(Large Object Heap)上,只有在达到LOH的阈值/手动调用GC.Collect方法/OS内存不足的时候才会去尝试整理大对象.可以参考这篇文章,里面详细介绍了在windows平台下dotnet framework和dotnet core针对较大对象的gc优化策略.

workstation和server

在dotnet中,提供了两种区别较大的gc模式:workstation和server,大体上可以认为是分别给开发环境和实际生产环境使用的两种gc模式.workstation mode下同时支持concurrent gc与non-concurrent gc,而server mode考虑到系统吞吐和稳定性需求,只有non-concurrent gc和background gc(在dotnet framework 4以后,background gc已经取代了concurrent gc).而这些模式都可以在配置文件中修改,
当开启server mode时,CLR会根据cpu的核心数量开启较多的gc线程,开启concurrent gc时会开启一些单独做gc的线程,在较新版本的dotnet中还会同时开启了background gc,不过background gc仅仅只会对gen2堆上的对象进行回收,并且backgaround gc的工作线程优先级和普通的用户线程是一样的.

gc真的是程序性能瓶颈吗

当我们开发应用的时候,gc往往是性能下降的第一背锅顺位.我无数次见到某些”有经验”程序员指着较为别人的代码说一些诸如”你这个地方会引发gc/会导致性能问题*&%#@”之类的话,仿佛随便写写代码就会让gc成为性能瓶颈.但事实真的如此吗?恐怕未必.想真正弄清程序的bottle neck在何处,还是要具体环境具体分析.windows下用visual studio,linux下用perf等工具都是可以很容易的看出某一段时刻程序执行真正耗时的地方在哪里.

面试官喜欢问的GC那些事

http://xulanting.net/2019/06/07/post7/

Author

John Doe

Posted on

2019-06-07

Updated on

2021-02-07

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.