编辑
阿华代码,不是逆风,就是我疯
你们的点赞收藏是我前进最大的动力!!
希望本文内容能够帮助到你!!
目录
本文GC的垃圾回收机制是重点
一:引入
在C语言中。malloc可以申请内存,free释放内存,申请到的内存的生命周期,会跟随整个进程。这一点对服务器程序非常的不好,如果服务器对每个请求都要去申请一块内存,申请的越来越多,最后导致没有空闲内存了。就会发生内存泄露问题。
像之前我们写的TCP回显服务器中,就存在accept接受一个请求后,就一定要在finally中去.close关闭文件。
那么我们能否实现让程序自动释放内存,而不是让程序员来手动释放呢?JVM就做到了这一点。
二:垃圾回收
GC垃圾回收中有一个很重要的问题:“STW”(stop the world)问题,触发垃圾回收的时候,很有可能造成当前程序的其它业务逻辑被暂停,Java发展到现在,已经可以把“STW”控制在1ms以内了
通过上一篇文章的学习,我们知道JVM中的内存有好几块——程序计数器,栈,堆,元数据区,这里面“堆”就是GC垃圾回收的主要战场
垃圾回收,准确的说可以是“回收对象”,而非回收内存
编辑
1:识别出垃圾
编辑(1)引用计数机制
试想如果有多个引用指向同一个对象,那么这个对象什么时候被判定为垃圾就是一件麻烦的事情了,所以我们引入计数机制,这种思想方法并没有在JVM中进行使用,但是广泛应用于其它主流语言的垃圾回收机制中——给对象安排一个额外的空间,空间里保存当前这个对象有几个引用。
编辑
专门有一个扫描线程,去获取到当前有多少个引用指向这个对象,如果计数为0,这个对象就会被当成垃圾,被回收掉。但是这种机制也是有一些问题的
编辑
问题一:计数器占用内存空间
试想每一个对象都要有一个计数器进行计数,如果对象非常多的话,计数器占用的内存空间就非常大了
问题二:循环引用的问题
编辑
编辑
此时要使用第一个对象,那就需要获得第一个对象的引用,第一个对象的引用又在第二个对象中;那么就需要获得第二个对象的引用,第二个对象的引用又在第一个对象中,那么就需要获取第一个对象的引用。闭环了(裂开!!有点像死锁)
(2)可达性分析
JVM中使用的就是可达性分析
在代码中,会定义许多变量,JVM中存在扫描线程,会对已有的这些变量进行遍历,尽可能的去访问到更多的对象—— 能被访问到的对象,就不会被当做垃圾,访问不到的对象,自然就是垃圾。
注:不可达是有传递性这种效果的
编辑
2:内存释放机制
把标记为垃圾的对象所占的内存空间进行回收,这里我们介绍4种释放方式
(1)标记——清除
编辑
这种标记——清除的策略,也会带来一些问题:内存碎片。
在回收完黑色方块所处的内存空间后(假设为4M),剩余的白色方块空闲内存空间为6M,但是这6M的内存空间并不是连续的一段空间,而是一段离散的、小的空间。
这就造成了一个问题——剩下的实际能使用的内存空间是小于6M的
再通俗一点的举例:此时有一个6M大小的对象需要存进内存空间,肯定不能放进去嘛,因为这不是连续的一段内存空间
(2)复制算法
编辑
优点
①规避了内存碎片的问题
缺点
①申请的内存空间只有利用率太低,只有一半左右
②如果需要复制的对象太多,那么成本就太高了
(3)标记—整理
编辑
优点
①有效解决内存碎片的问题
②不像复制算法那样浪费太多空间
缺点
①搬运的成本较高
(4)分代回收
分代回收机制:依据不同种类的对象,采取不同的方式
JVM中有专门的扫描线程,来扫描标记对象,根据对象的年龄差异把整个内存分为新生代和老年代两个区域部分
其中新生代区域又分为两个部分——伊甸区,生存区
对象年龄:如果一个对象被扫描一次后,判定不是垃圾,就年龄+1(初始年龄=0)
编辑
①代码中new出来的对象都放到伊甸区,当扫描线程第一遍扫描时,标记为垃圾的对象就进行回收,剩余的对象通过复制算法,复制到生存区1中
注:绝大多数对象都活不过伊甸区,只有少数能够撑住第一轮,被复制到生存区1中,此时这些对象年龄+1
②扫描线程第二遍扫描新生代,重复①,而生存区1中的对象大部分又被标记为垃圾进行GC回收了,只有一丢丢存活下来的对象继续被复制到生存区2中,此时这些对象年龄再+1,变为2
注:第二遍扫描包含伊甸区和生存区,老年代区被扫描的频率很低<后面会解释>
③经过若干轮,发现有年龄非常大的对象,那么就会认为这个对象的生命周期很长,就会把这个对象从生存区拷贝到老年代区中
④扫描线程继续工作,只不过老年代区扫描的频率非常低,大大节省了开销
⑤老年代区的对象挂了的话,就会按照“标记—整理”的方式进行回收,释放内存
上述JVM分代回收是GC中的核心思想,实际还有很多细节,感兴趣的铁铁可以深入研究一下~~