文章导读
本篇文章适合对java或者go的内存结构有一定了解的人,由于涉及内容比较多不会每一点都详谈,遇到自己不懂的可以自行搜索,另外我可能也会有讲述错误的地方,欢迎大家指正和补充。
什么是GC,为什么要GC?
GC既垃圾回收(Garbage Collection),是一种自动内存管理的形式。我们都知道内存空间是有限的,面对没用的垃圾对象(不再被程序或其他对象引用的对象)就需要将其回收来释放内存
怎么判断对象是不是垃圾
这便涉及到两种流行的垃圾回收算法来实现:引用计数法 和 可达性分析算法。
- 1.引用计数法:简单来说引用计数法就是对象每被引用一次就将计数器+1,引用失效就-1,为0就进行回收,通过这样很简单的加减法就可以实现,同时也导致了循环引用(a中引用b,b再引用a)这种致命问题。
- 2.可达性分析算法:简单来说就是从根节点出发通过每一条引用链来判断对象是否可以达到,达不到的就被回收,这是目前主流使用的办法,同时三色标记法也是基于此。
这里简单讲解一下java和go的根结点都设置为什么
java:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象:正在执行的方法中的局部变量所引用的对象。
- 方法区中的类静态属性引用的对象:类的静态字段引用的对象。
- 方法区中的常量引用的对象:常量池中引用的对象,例如字符串常量池中的字符串对象。
- 本地方法栈中JNI(即通常所说的Native方法)引用的对象:即使用Java本地接口(JNI)引用的对象。
- 活跃线程:Java虚拟机中活跃的线程也是GC Roots。
- 启动类加载器、系统类加载器所加载的类对象:由系统类加载器或启动类加载器加载的类对象。
- Java虚拟机内部的引用:如基本数据类型对应的包装类对象、一些常驻的异常对象等
go:
- 全局变量:在程序中声明的全局变量是GC Roots,因为它们在程序运行期间始终是可达的。
- 活跃的goroutine栈:每个活跃的goroutine的栈上的变量都是GC Roots。goroutine栈上的变量包括局部变量、参数等。
- 运行时的数据结构:Go运行时维护的数据结构,如调度器中的goroutine列表、channel等,这些都是GC Roots。
- finalizer注册表:被注册了finalizer函数的对象在finalizer被执行之前都是GC Roots。
垃圾回收区域
java:
java的主要回收区域为堆,由于在java中栈是线程私有的,这样的话栈上的数据直接随着栈被回收而进行回收便可,不需要太多考虑,方法区的部分虽然也是线程共有的,但是由于其回收条件比较严格通常较少触发
go:
go的回收区域同样为堆,但是这里的堆和栈和java是有一定区别的,在go中栈在堆上(不直接依赖于gc和协程相绑定),java中则是一块独立的区域
垃圾收集算法(简单讲)
- 标记清除算法:就是对垃圾进行标记,然后统一进行清除,这样导致了大量的内存碎片,并且效率低下的问题
- 复制算法:舍弃一部分的内存空间,每次把一半内存中的存活对象转移到另一半内存中,这样解决了内存碎片的问题,但可以使用的内存直接小了大半同时有大量“老年代 ”时效率低
- 标记整理算法:在1的基础上对对象进行整理来解决碎片化问题
- 分代GC:分代就较为复杂了,将内存进行分块(新生代和老年代),新生代中使用类似复制算法的操作,而老年代中存放那些很少被回收的对象通过标记清除或整理的算法实现。如果想详细了解可以自己去学习一下
java:
java主要是基于分代GC来进行垃圾回收的,java中提供了很多不同的垃圾收集器(可以理解为通过算法来回收的具体实现),比如Serial,ParNew,CMS等,而目前流行的是G1(ZGC),这里讲一些其优点:通过分块使内存的使用更灵活,STW(stop the world程序中断来进行垃圾回收的时间)更短,停顿时间可控等等。
go:
go并没有像java一样提供了多种可供选择的垃圾收集器,而是同一使用了基于标记清除算法的三色标记法进行回收,将对象通过(黑,灰,白)三种颜色进行标记,其中白色为新创建的对象,根节点遍历到的变为灰色,然后遍历灰色节点将其遍历到的节点变灰同时把自己变为黑色,以此类推直到灰色节点消失,通过这种类似可达性算法来将白色节点(到达不了的)进行清除,那go是如何优化STW的呐?go通过三色标记法使垃圾回收可以并发执行,通过一个或多个协程在程序执行时进行回收,从而将STW尽可能的缩短,这得益于通过强弱三色不变式实现的混合写屏障来保证对象不丢失,而碎片化的问题也得益于go特殊的内存单元设计得到了一定解决。
最后
这是我第一次写这种技术文章可能会有许多不足或是错误之处,欢迎大家来指正,这篇文章虽然只有短短几千字花费的时间却比我预计长很多,想要真正讲清楚GC可能要几万字,希望大家可以去多多学习。