.Net平台GC VS JVM垃圾回收

前言

不知道你平时是否关注程序内存使用状况,我是关注的比较少,正好借着优化本地一个程序的空对比了一下.Net平台垃圾回收和jvm垃圾回收,顺便用dotMemory看了程序运行后的内存快照,生成内存快照后,妈妈不再担忧我优化程序找不到方向了。
java

.Net平台垃圾回收

内存优化

凭空想象这些概念多少会索然无味,下图是我我基于本地的一个程序生成的内存快照,使用jetbrains推出的dotMemory工具生成。
算法

生成内存快照

QQ截图20200422180141.png
程序运行时能够经过右上角的Get SnapShot按钮生成内存快照,内存快照里能够看到具体的对象、消耗内存的状况,好比说一些大的字符串对象,重复的大量的字符串对象, 那么从上面这张图上都能看到哪些关键字呢?
什么是Heap generation1和Heap greneration2呢?
什么是Allocated呢?
数据库

什么是GC

GC (Garbage Collection)如其名,就是垃圾收集,固然这里仅就内存而言。Garbage Collector(垃圾收集器,在不至于混淆的状况下也成为GC)以应用程序的root为基础,遍历应用程序在托管堆(Managed Heap)上动态分配的全部对象,经过识别它们是否被引用来肯定哪些对象是已经死亡的、哪些仍须要被使用。已经再也不被应用程序的root或者别的对象所引用的对象就是已经死亡对象,即所谓的垃圾,须要被回收。这就是GC工做的原理。为了实现这个原理,GC有多种算法。比较常见的算法有Reference Counting,Mark Sweep,Copy Collection等等。目前主流的虚拟系统.NET CLR,JVM都是采用的Mark Sweep算法。
安全

Mark-Compact 标记压缩算法

简单地把.NET的GC算法看做Mark-Compact算法。阶段1: Mark-Sweep 标记清除阶段,先假设heap中全部对象均可以回收,而后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是能够被回收的;阶段2: Compact 压缩阶段,对象回收以后heap内存空间变得不连续,在heap中移动这些对象,使他们从新从heap基地址开始连续排列,相似于磁盘空间的碎片整理。
     


        Heap内存通过回收、压缩以后,能够继续采用前面的heap内存分配方法,即仅用一个指针记录heap分配的起始地址就能够。主要处理步骤:将线程挂起→肯定roots→建立reachable objects graph→对象回收→heap压缩→指针修复。能够这样理解roots:heap中对象的引用关系错综复杂(交叉引用、循环引用),造成复杂的graph,roots是CLR在heap以外能够找到的各类入口点。
        GC搜索roots的地方包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针(还有finalization queue)等。主要能够归为2种类型:已经初始化了的静态变量、线程仍在使用的对象(stack+CPU register) 。 Reachable objects:指根据对象引用关系,从roots出发能够到达的对象。例如当前执行函数的局部变量对象A是一个root object,他的成员变量引用了对象B,则B是一个reachable object。从roots出发能够建立reachable objects graph,剩余对象即为unreachable,能够被回收。
    
           指针修复是由于compact过程移动了heap对象,对象地址发生变化,须要修复全部引用指针,包括stack、CPU register中的指针以及heap中其余对象的引用指针。Debug和release执行模式之间稍有区别,release模式下后续代码没有引用的对象是unreachable的,而debug模式下须要等到当前函数执行完毕,这些对象才会成为unreachable,目的是为了调试时跟踪局部对象的内容。传给了COM+的托管对象也会成为root,而且具备一个引用计数器以兼容COM+的内存管理机制,引用计数器为0时,这些对象才可能成为被回收对象。Pinned objects指分配以后不能移动位置的对象,例如传递给非托管代码的对象(或者使用了fixed关键字),GC在指针修复时没法修改非托管代码中的引用指针,所以将这些对象移动将发生异常。pinned objects会致使heap出现碎片,但大部分状况来讲传给非托管代码的对象应当在GC时可以被回收掉。


网络

垃圾回收之三个阶段

PhaseInGarbageCollection.png

  • Marking Phase:在标记阶段会建立全部活动对象的列表。 这是经过遵循全部根对象的引用来完成的。 不在活动对象列表中的全部对象均可能从堆内存中删除。
  • Relocating Phase:全部活动对象列表中全部对象的引用在重定位阶段进行更新,以便它们指向在压缩阶段将对象重定位到的新位置。
  • Compacting Phase:随着释放死亡对象占用的空间并移动剩余的活动对象,堆会在压缩阶段被压缩。 垃圾回收后剩余的全部活动对象均按其原始顺序移至堆内存的较旧端。

垃圾回收之Genearation - 分代

堆内存在回收过程当中不是一次性回收全部,而是分为3代,目前也支持3代,根据上面的截图能够看出来。所以能够在垃圾回收期间适当地处理具备不一样生存期的各类对象。 取决于项目的大小,每一代的内存将由公共语言运行时(CLR)给出。 在内部,Optimization Engine将调用Collection Means方法来选择哪些对象将进入第1代或第2代。


HeapGenerationInGarbageCollection.pngjvm

  • Generation 0:全部短时间对象(例如临时变量)都包含在堆内存的第0代中。 除非它们是大对象,不然全部新分配的对象也是隐式的第0代对象。 一般,垃圾回收的频率在第0代中最高。
  • Generation 1:若是运行在垃圾回收中未释放的第0代对象占用的空间,则这些对象将移至第1代。这一代中的对象是第0代中的短时间对象和第2代中的长期对象之间的一种缓冲区对象。
  • Generation 2:若是某个第1代对象占用的空间未在下一次垃圾回收运行中释放,则这些对象将移至第2代。第2代对象的生存期很长,例如静态对象,由于它们整个都保留在堆内存中 处理持续时间。

GC给咱们带来的优点

  • 垃圾回收使用3个代的概念成功的在托管堆上有效的分配对象内存。
  • 再也不须要手动释放内存,GC会在不须要时自动释放内存空间。
  • 垃圾回收能够安全地处理内存分配,所以没有对象会错误地使用另外一个对象的内容。
  • 新建立的对象的构造函数没必要初始化全部数据字段,由于垃圾回收会清除之前释放的对象的内存。

非托管堆

说了半天都在说托管堆,那么非托管堆呢?垃圾回收是不知道何时去处理非托管堆资源,好比文件句柄,网络链接、数据库链接。如下两种方式用来处理非托管堆垃圾回收。函数

  1. 在定义类时声明析构函数。
  2. 在定义类时实现IDisposable接口并实现Dispose函数, 实现接口有在程序中有两种处理方法,使用using关键字,推荐使用, 再就是在finally中显式调用Dispose函数。

附录GC经常使用函数

返回指定对象的当前代数
public static int GetGeneration(Object);

检索当前认为要分配的字节数。 一个参数,指示此方法是否能够等待较短间隔再返回,以便系统回收垃圾和终结对象
public static long GetTotalMemory (bool forceFullCollection);

返回已经对对象的指定代进行的垃圾回收次数。
public static int CollectionCount (int generation);

获取垃圾回收的内存信息
public static GCMemoryInfo GetGCMemoryInfo ();

强制对全部代进行即时垃圾回收。
public static void Collect ();

jvm垃圾回收

好吧,说到这里还没提出来jvm垃圾回收,若是你已经了解了jvm垃圾回收,从上面的垃圾回收算法和分代回收来看,.Net平台和jvm在垃圾回收这块设计思路是一致的,二者的垃圾回收算法都包含:标记清除算法、复制算法、标记整理算法、分代收集算法。 
       ** 当前商业虚拟机算法都使用分代收集算法,jvm根据对象的存活周期把内存划分为:
年轻代、老年代、永久代。

工具

新生代(Young generation)

绝大多数最新被建立的对象会被分配到这里,因为大部分对象在建立后会很快变得不可达,因此不少对象被建立在新生代,而后消失。对象从这个区域消失的过程咱们称之为 minor GC。
新生代 中存在一个Eden区和两个Survivor区.新对象会首先分配在Eden中(若是新对象过大,会直接分配在老年代中)。在GC中,Eden中的对象会被移动到Survivor中,直至对象知足必定的年纪(定义为熬过GC的次数),会被移动到老年代。
能够设置新生代和老年代的相对大小。这种方式的优势是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与新生代的比例。例如 -XX:NewRatio=8 指定 老年代/新生代 为8/1. 老年代 占堆大小的 7/8 ,新生代 占堆大小的 1/8(默认便是 1/8)。
例如:post

-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8

老年代(Old generation)

对象没有变得不可达,而且重新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正因为其相对较大的空间,发生在老年代上的GC要比新生代要少得多。对象从老年代中消失的过程,能够称之为major GC(或者full GC)。优化

永久代(permanent generation)

像一些类的层级信息,方法数据 和方法信息(如字节码,栈 和 变量大小),运行时常量池(JDK7以后移出永久代),已肯定的符号引用和虚方法表等等。它们几乎都是静态的而且不多被卸载和回收,在JDK8以前的HotSpot虚拟机中,类的这些"永久的" 数据存放在一个叫作永久代的区域。
永久代一段连续的内存空间,咱们在JVM启动以前能够经过设置-XX:MaxPermSize的值来控制永久代的大小。可是JDK8以后取消了永久代,这些元数据被移到了一个与堆不相连的称为元空间 (Metaspace) 的本地内存区域。

小结

JDK8堆内存通常是划分为年轻代老年代不一样年代 根据自身特性采用不一样的垃圾收集算法
对于新生代,每次GC时都有大量的对象死亡,只有少许对象存活。考虑到复制成本低,适合采用复制算法。所以有了From SurvivorTo Survivor区域。
对于老年代,由于对象存活率****高,没有额外的内存空间对它进行担保。于是适合采用标记-清理算法标记-整理算法进行回收。

总结

目前对比了.Net平台垃圾回收和jvm垃圾回收,对于垃圾回收算法和分代的概念,二者设计思路都相同,惟一的区别我我的觉的JDK8之后jvm的垃圾回收效率更高,根据不一样的代使用不一样的垃圾收集算法,这一点彷佛是.Net平台垃圾回收没有实现的地方。

参考连接

https://www.geeksforgeeks.org/garbage-collection-in-c-sharp-dot-net-framework/
http://www.javashuo.com/article/p-niycnocw-mk.html
https://kb.cnblogs.com/page/106720/
https://www.zhihu.com/question/31806845

相关文章
相关标签/搜索