垃圾回收机制GC
当对象被创建时,就会在Java虚拟机的堆区中拥有一块内存,在Java虚拟机的生命周期中,Java程序会陆续创建无数个对象,假如所有的对象都永久占有内存,那么内存有可能很快被消耗光,最后引发内存空间不足的错误。因此必须采用一种措施来及时回收那些无用对象的内存,以保存内存可以被重复利用。
java GC 机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,堆内存泄露和溢出的问题。
1.需要GC的内存区域
jvm中,程序计数器,虚拟机栈,本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理。因此,我们的内存垃圾回收主要集中于Java堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。
2. 判断GC对象是否存活
判断一个对象是否存活常用的有两种方法:引用计数和可达分析
2.1 引用计数:
每个对象都有一个引用计数属性,新增一个引用时计数加一,引用释放时计数减一,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
2.2可达性分析:
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
在java语言中,GC Roots包括:
- 虚拟机栈中引用的对象
- 方法区中类静态属性实体引用的对象
- 方法区中常量引用的对象
- 本地方栈中引用的对象
通过可达性分析可以对需要回收的对象进行标记,那么标记的对象是否一定会回收呢?
3.标记死亡对象
要真正宣告一个对象的死亡,至少要经历两次标记的过程:
3.1 第一次标记
在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记。并且判断此对象是否必要执行finalize()方法。
- 如果对象没有覆盖finalize()方法或者finalize()已经被jvm调用过,则这个对象就会认为是垃圾,可以回收。
- 对于覆盖了finalize()方法,且finalize()方法没有被jvm调用过时,对象进入一个队列中,等待着被触发调用finalize()方法
3.2 第二次标记
执行完第一次的标记后,GC将对队列中的对象进行第二次小规模标记。也就是执行对象的finalize()方法
- 如果对象在其finalize()方法中重新与引用链上任何一个对象建立联系,第二次标记时会将其移除“即将回收”的集合。
- 如果对象没有,也可以认为对象已死,可以回收。
4. 什么时候触发GC
- 系统调用System.gc()时可以触发
- 系统自身来决定GC触发的时机
5.GC分类
GC又分为minor GC 和 Full GC(也称为Major GC)
6. GC常用算法
GC收集过程中用过的算法有:
- 标记-清除算法
- 标记-压缩算法
- 复制算法
- 分代收集算法
6.1 标记-清除算法
6.1.1 标记
GC通过遍历内存区辨别那些内存在使用,那些内容没有使用,并做好标记。
引用的对象以蓝色显示。未引用的对象以金色显示。在标记阶段扫描所有的对象以进行此确定。如果必须扫描系统中的所有对象,这可能时一个非常耗时的过程。
6.1.2 清除
清除阶段移除掉垃圾对象,并且用一个链表维护空闲的区域。
内存分配器持有空间内存区的引用,以便分配给新的对象。
6.1.3优缺点
优点:
- 每个活着的对象的引用只需要找到即可,找到一个就可以判断其为活的
- 不移动对象的位置
缺点:
- 效率比较低,每个活着的对象都要在标记阶段遍历一遍
- 所有的对象都要在清除阶段扫描一遍,因此算法复杂度较高
- 没有移动对象,导致可能踹下你很多碎片化空间无法利用的情况
6.2 标记-压缩算法
标记-压缩算法时标记-清除算法的一个改进版。在标记阶段,该算法也将所有的对象标记为活着和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清除,而是将所有存活的对象进行整理一下,放到同一区域的另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。
通过一起移动引用的对象,这使得新的内存分配更容易和更快。
优点:该算法不会像标记-清除算法那样产生大量的碎片空间
缺点:如果存活的对象过多,整理阶段将会执行较多的复制操作,导致算法效率较低
6.3 复制算法
复制算法与标记-整理算法的区别在于:该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。
该算法将内存平均分成两个部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
优点:实现简单,不产生内存碎片
缺点:每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半
6.4 分代收集算法
6.4.1 标记和压缩算法存在的问题
标记和压缩算法对于Java虚拟机而言会比较耗时。当java虚拟机分配了越来越多的对象后,GC所花费的时间将会更长。然而根据经验分析,绝大多数对象存在的时间都比较短。这样我们可以把存活时间长的对象和存活时间短的对象隔离开,这样GC会更加高效。
现在的虚拟机垃圾收集大多采用分代收集算法,它根据对象的生存周期,将堆分为新生代和老年代。
6.4.2分代收集算法思想
- 在新生代中,由于对象生存期较短,每次回收都会有大量对象死去,那么这时候就可以采用复制算法。
- 老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以采用标记-清除或者标记-压缩的算法。
6.4.3 分代收集过程
1.新生代(Young)分为Eden区,From区与To区
2.几乎任何新的对象都会在eden空间中分配内存,两个survior空间中一开始是空的
3.随着对象的不断创建,eden区空间逐渐被填满。
4.当eden区满了,那么就会触发一次Minor GC,也就是新生代的垃圾回收,所采用的就是复制算法。
5.下一次minor GC发生时,eden空间的存活对象也将被复制到空闲你的survior空间(s1,年龄加一),另外在前一次minor GC s0空间的存活对象也会被复制到s1(年龄加一),垃圾对象会被清除。
6. 下一次minor GC发生时,还是重复第四条的内容,只是两个survivor空间对调了,这次是从s1复制到s0空间。
7.随着minor gc不断发生,幸存对象在两个幸存区不断交换储存,年龄也在不断递增。当幸存对象的年龄达到指定的阈值后,他们将被移动到老年代。
8. 当年老代的内存被填满的时候,将会触发将触发Major GC(Full GC)进行老年代的内存清理。Major GC在老年代用的是标记清除算法。同时新生代的对象将被清除。
6.5 对象被放到老年代的条件
- 该对象的年龄达到阈值
- 创建对象后,无法放置到eden区(比如eden区的大小是10m,新对象的大小是11m)
- 如果survivor区相同年龄的所有对象大小大于survivor区空概念的一般,年龄大于或等于这些对象的可以直接进入老年代
7. 方法区垃圾回收
方法区也存在垃圾回收,方法区的垃圾回收主要回收两部分内容
- 常量池中废弃的常量
- 不在使用的类型
8.finalize()方法
finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法