文章目录
垃圾回收机制(GC)
当对象被创建时,就会在Java虚拟机的堆区中拥有一块内存,在Java虚拟机的生命周期中, Java程序会陆续地创建无数个对象,假如所有的对象都永久占有内存,那么内存有可能很快被消耗光,最后引发内存空间不足的错误。因此必须采取一种措施来及时回收那些无用对象的内存,以保证内存可以被重复利用。
Java GC (Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专编写内存回收和垃圾清理代码,对内存泄露和溢出的问题。
官方的解释:
官方文档
- 自动垃圾回收是查看堆内存、识别哪些对象正在使用、哪些未使用以及删除未使用对象的过程。一个使用中的对象,或一个引用的对象,意味着你的程序的某些部分仍然维护着一个指向那个对象的指针。未使用的对象或未引用的对象不再被程序的任何部分引用。因此可以回收未被引用的对象使用的内存。
- 在像 C 这样的编程语言中,分配和释放内存是一个手动过程。在 Java 中,释放内存的过程由垃圾收集器自动处理
1.需要GC的内存区域?
jvm中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理.
因此,我们的内存垃圾回收主要集中于java堆和方法区中(也包括本地方法栈的JNI引用的对象),在程序运行期间,这部分内存的分配和使用都是动态的。
2.判断GC对象是否存活?
判断一个对象是否存活常用的有两种办法:引用计数和可达性分析:
1)引用计数:
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
Java没有选择使用引用计数算法管理内存。
2)可达性分析(Reachability Analysis) :
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
在Java语言中,GC Roots包括: .
- 虚拟机栈中引用的对象。
- 方法区中类静态属性实体引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。
3.标记死亡对象
要真正宣告一个对象的死亡,至少要经历两次的标记过程:
1)第一次标记
在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记。并且判断此对象是否必要执行finalize ()方法!
- 如果对象没有覆盖finalize()方法或者finalize()已经被JVM调用过,则这个对象就会认为是垃圾,可以回收。
- 对于覆盖了finalize()方法, 且finalize()方法没有被JVM调用过时,对象会被放入一个成为F-Queue的队列中,等待着被触发调用对象的finalize() 方法。
finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法
2)第二次标记
执行完第一次的标记后, GC将对F-Queue队列中的对象进行第二次小规模标记。也就是执行对象的finalize()方法!
- 如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合。
- 如果对象没有,也可以认为对象已死,可以回收了。
4.什么时候触发GC?
- 程序调用System. gc() 时可以触发
- 系统自身来决定GC触发的时机(根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程)
5.GC分类
GC又分为Minor GC和Full GC(也称为Major GC)
Minor GC触发条件:
Minor GC的触发条件是,当Eden区满时
6.GC常用算法
GC收集.过程用到的算法有:
- 标记-清除算法
- 标记-压缩算法
- 复制算法
- 分代收集算法
1)标记-清除算法
Step 1:标记(Marking):
GC通过遍历内存区辨别哪些内存在使用,哪些内容没有使用。并做好标记。
- 引用的对象以蓝色显示。未引用的对象以金色显示。在标记阶段扫描所有对象以进行此确认。如果必须扫描系统中所有的对象,这可能是一个非常耗时的过程。
Step 2:清除(Normal Deletion):
清除阶段移除掉垃圾对象,并且用一个链表维护空闲的区域。
- 内存分配器持有空闲内存区的引用,以便于分配内存给新对象。
优点:
每个活着的对象的引用只需找到即可,找到一个就可以判断它为活。
不移动对象的空间
缺点:
效率较低(递归与全对象遍历),每个活着的对象都要在标记阶段遍历一遍;
所有的对象都要在清除阶段扫描一遍,因此算法复杂度比较高;
没有移动对象,可能导致很多碎片空间无法利用的情况
2)标记-压缩算法
标记-压缩法是标记-清除法的一个改进版。在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到同一区域的另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。
通过一起移动引用的对象,这使得新的内存分配更容易和更快。
优点: 该算法不会像标记-清除算法那样产生大量的碎片空间
缺点: 如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低
3)复制算法
复制算法与标记-整理算法的区别在于:该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
**优点:**实现简单;不产生内存碎片
**缺点:**每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半
4)分代收集算法
标记和压缩算法存在的问题 :
标记和压缩算法对java虚拟机而言会比较耗时。当java虚拟机分配了越来越多的对象后。GC所花费的时间将会更长。然而根据经验分析,绝大多数的对象存活时间都比较短。这样我们可以把存活长的对象和存活短的对象隔离开。这样GC会更加高效。
Y 轴显示分配的字节数,X 访问显示随时间分配的字节数。
随着时间的推移,分配的对象越来越少。事实上,大多数物体的寿命都很短,如图表左侧的较高值所示
分代收集算法思想:
在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。
老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除
分代收集过程:
1.新生代(Young)分为Eden区,From区与To区
2. 几乎任何新的对象都会在eden空间分配内存,两个survivor空间一开始都是空的
3. 随着对象的不断创建,Eden区空间逐渐被填满.
- 当Eden区满了,那么就会触发一次Minor GC(Young GC),也就是新生代的垃圾回收,所采用的是复制算法
5.下一次minor GC发生时,Eden空间的存活对象将被复制到空闲的survivor空间S1(年龄+1),另外在前一次minor GC S0空间的存活对象也会被复制到S1(年龄+1),垃圾对象会被清除掉
6.下一次minor GC发生时,还是重复第4条的内容,只是两个survivor空间对调了,这次是从S1复制到S0空间
7.随着Minor GC的不断发生,幸存对象在两个幸存区不断地交换存储,年龄也不断递增。当幸存对象的年龄达到指定的阈值(这个例子中是8,由JVM参数MaxTenuringThreshold决定)后,它们将被移动到老年代:
8.当年老代的内存被填满的时候,将会触发将触发Major GC(Full GC)进行老年代的内存清理。Major GC在老年代用的是标记清除算法。同时新生代的对象将被清除。
做一次 Full GC 要比进行一次 Minor GC 的时间更长,一般是Minor GC的 10倍以上。
7.对象被放置到老年代的条件
一个对象被放置到老年代除了它的年龄达到阈值外,以下几种情况也会使得该对象直接被放置到老年代:
- 对象创建后,无法放置到eden区(比如eden区的大小为10m,新的对象大小为11m,eden区不够放,触发YGC。YGC后eden区被清空,但还是无法容下11m的“超大对象”,所以直接放置到老年代。当然如果老年代放置不下则会触发Full GC,Full GC后处理依然放不下则OOM);
- YGC后,对象无法放置到survivor To代也会直接晋升到老年代;
- 如果survivor区中相同年龄的所有对象大小大于survivor区空间的一半,年龄大于或等于这些对象年龄的对象可以直接进入老年代,无需等到年龄阈值。
8. Full/Major GC触发条件
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
9.方法区垃圾回收
方法区也存在垃圾回收,方法区的垃圾收集主要回收两部分内容:
- 常量池中废弃的常量
- 不再使用的类型。
10.finalize()方法
finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法
在撤消一个对象时,有时需要完成一些操作。例如,如果一个对象正在持有某些非Java资源,如文件句柄或者是Windows字符字体,则要确保在对象被销毁之前释放这些资源。为处理这种情况,Java提供了一种称为结束的机制(finalization),使用finalization,可以定义特殊的动作,这种动作在一个对象要被垃圾回收器收回时执行。
要给一个类添加一个结束器,只需要定义finalize()方法。无论何时要收回该类的对象Java运行时就调用该方法。在finalize()方法内要指定一个对象在被撤消前必须执行的动作。垃圾回收器定期运行,检查不再被任何运行状态引用的对象或间接地通过其他对象引用的对象。在释放内存前,Java运行时调用该对象上的finalize()方法。
finalize()方法的一般形式如下:
protected void finalize() {
//finalization code here…
}