JVM 垃圾回收
1.概述
JVM 会自动帮程序员进行垃圾回收,并不需要程序员手动的进行垃圾回收(C++等语言需要自己手动回收垃圾),了解 JVM 的垃圾回收,可以帮程序员写出占用内存更小、更高效的程序。
1.1 什么是垃圾?
垃圾是指运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
1.2 什么区域需要进行垃圾回收
JVM 的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
而Java 堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。(1)
垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法。
(1):方法区存在运行时常量池,可以进行动态的分配。
1.3 补充
1.3.1 内存泄露
只有对象不再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄露,实际情况有一些疏忽导致对象的生命周期变的很长甚至 OOM,宽泛意义上的内存泄露。
举例:
- 单例的生命周期和程序是一样长,如果单例程序中,持有对外部对象的引用的话,那么这个外部对象是不能被回收的,导致内存泄露
- 一些提供close的资源未关闭导致内存泄露,如数据库链接、网络链接和IO
强引用是造成java内存泄露的主要原因之一
1.3.2 安全点与安全区域
1.安全点
-
程序执行并非在所有地方都能停顿下来开始 GC,只有特定的位置才能停顿下来开始 GC,这些位置称为安全点
-
如果太少,导致 GC 等待时间长,如果太多导致运行时性能问题,大部分指令执行都比较短,通常会根据是否具有让程序长时间执行的特征为标准选择一些执行时间较长的指令作为安全点,比如方法调用、循环跳转和异常跳转等
-
抢先式中断
中断所有线程,如果还有线程不在安全点,就恢复线程,让线程跑到安全点,没有虚拟机采用
-
主动式中断
设置一个中断标志,各个线程运行到安全点的时候,主动轮询这个标志,如果标志为真,则将自己进行中断挂起
2.安全区域
- 如果线程处于sleep或者blocked状态,这时候线程无法响应jvm中断请求,走到安全点去中断挂起。对于这种情况,就需要安全区域来解决
- 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何位置开始GC都是安全的
- 当线程运行到安全区域代码时,首先标志已经进入了安全区域,如果GC,JVM会忽略标识为安全区域状态的线程
- 当线程即将离开安全区域时,会检查JVM是否已经完成GC,如果完成了,则继续运行。否则线程必须等待直到收到可以安全离开安全区域的信号为止
2.对象“存活”算法
2.1 引用计数算法
2.1.1 概述
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器 +1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。
任何引用计数器为 0 的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
如果存在对象引用这不会进行回收,没有对象引用了,就会被回收。
2.1.2 优缺点
1.优点
引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
实现简单,垃圾便于辨识,判断效率高,回收没有延迟性。
2.缺点
- 需要单独的字段存储计数器,增加了存储空间的开销
- 每次赋值需要更新计数器,伴随加减法操作,增加了时间开销
- 无法处理循环引用的情况,致命缺陷,导致 JAVA 的垃圾回收器中没有使用这类算法(1)
(1):如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0,下面实例演示:
public class abc_test {
public static void main(String[] args) {
MyObject object1=new MyObject();
MyObject object2=new