垃圾回收机制,是java提供的对于内存自动回收的机制。java不需要像C/C++那样手动free()释放内存空间,而是在JVM中封装好了。垃圾回收机制,不是java独创的,现在应该是主流编程语言的标配。GC需要消耗额外的系统资源,而且存在非常影响执行效率的“STW”问题(触发GC的时候,可能一瞬间把系统负载拉满,导致服务器无法响应其他的请求)。
GC回收的“内存”,更准确说,是“对象”,回收的是“堆上的内存”。
内存区域有四块:
1)程序计数器(不需要额外回收,线程销毁,自然回收了)
2)栈(不需要额外回收,线程销毁,自然回收了)
3)元数据区(一般也不需要,都是加载类,很少“卸载类”)
4)堆 (GC的主力部分)
GC一定是一次回收一个完整的对象,不能回收半个对象(一个对象有10个成员,肯定是把10个成员的内存都回收了,而不是只回收一部分)
GC的流程
GC的流程,主要是两个步骤。1)找到谁是垃圾 2)释放对应的内存
找到谁是垃圾
一个对象,什么时候创建,时机往往是明确的。但是什么时候不再使用,时机往往是模糊的。在编程中,一定要确保,代码中使用的每个对象,都得是有效的,不能出现“提前释放”的情况。
因此判定一个对象是否是垃圾,判定方式是比较保守的。
此处引入了非常“保守”的,一定不会误判的做法(可能回释放的不及时)。判定某个对象,是否存在引用指向它。
在java中,使用对象,都是通过引用的方式来使用的。如果没有引用指向这个对象,意味着这个对象注定无法在代码中被使用。就可以视为是垃圾了。
如何判定,某个对象是否有引用指向呢?
1)引用计数(不是JVM采取的方案,而是Python/PHP的方案)
会在为new 对象开辟内存空间时,额外开辟一个计数器,每当对象多一个引用,计数器+1。当计数器为0时,即可回收对象。
这种方法存在两个缺陷:
1、消耗额外的存储空间
如果对象比较大,浪费的空间还好,对象比较小并且对象数目多,空间浪费就多了。
2、存在“循环引用”的问题
当执行 a = null b = null 时,此时这两对象相互指向对方,导致两个对象的引用计数,都为1(不为0,不是垃圾)但是你外部代码,也无法访问到这两对象。
2)可达性分析(是JVM采取的方案)
可达性分析是java采用的做法,解决了空间和循环引用的问题,但是付出了时间上的代价。核心思想是“遍历”,JVM把对象之间的引用关系,理解成了一个“树形结构”。JVM就会不停的遍历这样的结构,把所有能够遍历访问到的对象标记成“可达”,剩下就是“不可达”。
这些树的根结点是怎么确定的?
Java代码中,你所有的
1)栈上的局部变量,引用类型的,都是GC roots
2)常量池中,引用的对象
3)方法区中的静态成员
都是一棵树的根结点。JVM就会周期性的对这所有的树进行遍历,不停的标记可达,也不停的把不可达的对象干掉。
具体树是否复杂,都取决于实际代码的实现。
由于可达性分析,需要消耗一定的时间,因此,java的垃圾回收,没法做到“实时性”。只能周期性进行扫描(JVM提供了一组专门的负责GC的线程,不停的进行扫描工作)
释放垃圾的策略
1、标记-清除
直接把标记为垃圾的对象对应的内存释放掉 (简单粗暴)
这样的做法会存在“内存碎片”问题。指空闲内存被分成一个个的碎片了,后续很难申请到连续的大的内存。并不实用。
2、复制算法
将内存空间分成两块,要释放某一块内存空间时,将无需删除的数据提前复制到另一块内存中。
这种做法空间浪费太多了。如下图删除1、3、5:
3、标记-整理
将无需删除的部分向前搬运,覆盖掉要删除的数据。如删除2、4、6
这种方法能解决空间利用率问题,但是时间开销更大。
JVM中实际采取的方案是综合上述方案,更复杂的策略。分代回收。也就是分情况讨论,根据不同的场景和特点选择合适的方案。根据对象的年龄(经历GC周期性扫描的轮次)来讨论,GC有一组线程,回对内存进行周期性扫描。某个对象经历了一轮GC之后,还是存在,没有成为垃圾,年龄就+1。
JVM堆区的结构如下:
分代回收的流程:1)把新创建的对象,放到伊甸区中。
2)伊甸区中,大部分的对象,生命周期都是比较短的,第一轮GC到达的时候,就会成为垃圾。只有少数对象能活过第一轮GC。
3)伊甸区 -> 生存区 通过复制算法。(由于存活对象很少,复制开销也很低,生存空间也不必很大)
4)生存区 -> 另一个生存区 通过复制算法。每经过一轮GC,生存区中都会淘汰掉一批对象,剩下的通过复制算法,进入到另一个生存区(进入另一个生存区的还有从伊甸区里进来的对象),存活下来的对象,年龄+1.
5)生存区 -> 老年代 某些对象,经历了很多轮GC,都没有成为垃圾,就会复制到老年代。
老年代的对象,也是需要进行GC的,但是老年代的对象生命周期都比较长,就可以降低GC的扫描频率。
以上,关于JVM的垃圾回收机制,希望对你有所帮助。