二十四、JVM垃圾回收

垃圾收集发生的区域

Java堆和方法区有显著的不确定性,只有在程序运行时才能知道程序究竟创建了哪些对象,创建了多少对象,所以这部分内存的分配和回收是动态的,垃圾收集器所关注的正是这部分内存该如何管理

判定需要被回收的对象

引用计数法

  • 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效,计数器减1;任何时刻计数器值都为0的对象就不能再被使用了
  • 优缺点:
    • 虽然会占用额外的内存空间用于计数,但原理简单,判定效率高,大多数情况下都是一个不错的算法
    • 需要考虑很多额外情况,否则无法保证其正确工作,例如单纯的引用计数法就很难解决对象之间相互循环引用的问题

可达性分析算法

  • 基本思路:通过一系列称为GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用,可以回收

在Java技术体系中,可以作为GC Roots的对象包括:

  • 在虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象
  • Java虚拟机内部的引用,如基本类型对应的Class对象
  • 所有被同步锁持有的对象

垃圾回收机制

  • 内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

  • 内存溢出:指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。 内存泄露量大到一定程度会导致内存溢出。但是内存溢出不一定是内存泄露引起的。

  • 内存泄漏的分类(按发生方式来分类)

    • 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
    • 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
    • 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
    • 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏

引用类型

  1. 强引用(不回收):
    • 当使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用;
    • 无论什么时候,只要强引用关系存在,对象就永远不会被回收;
    • 对于一个普通对象,若没有其他的引用关系,只要超过了引用的作用域或者显式地将引用赋值为 null,就可以被当作垃圾回收了;
    • 强引用时造成Java内存泄漏的主要原因。
-verbose:gc -Xms4m -Xmx4m -Xmn2m
    /**
     * 强引用
     * 1. 强引用可以直接访问目标对象。
     * 2. 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象。
     * 3. 强引用可能导致内存泄漏。
     */
    @Test
    public void strong_quote_test() {
        StringBuffer str = new StringBuffer("Hello");
        StringBuffer str1  = str;

        str = null;
        System.gc();

        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println(str1);  // Hello
    }
  1. 软引用(内存不足即回收):
    • 用来描述一些有用但非必要的对象,此类对象只有在进行一次垃圾收集仍然没有足够内存时,才会在第二次垃圾收集时被回收。
    • 应用:高速缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉;
    • JDK1.2之后提供了SoftReference类来实现软引用
        // 使用软引用
        Object object = new Object();
        SoftReference<Object> reference = new SoftReference<>(object);
  1. 弱引用(发现即回收):
    • 也是用来描述那些非必须对象,但它的强度比软引用更弱一些;
    • 被软引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象;
    • 软引用、弱引用都适合保存可有可无的缓存数据;
    • JDK1.2之后提供了WeakReference类来实现软引用
        // 使用弱引用
        WeakReference<Object> weakReference = new WeakReference<>(object);
  1. 虚引用(对象回收跟踪):
    • 最弱的一种引用关系,一个对象是否存在虚引用,丝毫不会对其生存时间造成任何影响,也无法通过虚引用来取得一个对象实例。
    • 设置虚引用关联的唯一目的就是让这个对象被回收时能收到一个系统通知。
    • JDK1.2之后提供了PhantomReference类来实现软引用
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;

public class PhantomReferenceTest {
    private static final List<Object> TEST_DATA = new LinkedList<>();
    // 引用队列
    private static final ReferenceQueue<TestClass> QUEUE = new ReferenceQueue<>();

    public static void main(String[] args) {
        TestClass obj = new TestClass("Test");
        // 创建一个虚引用,并将其注册到一个队列中。当obj对象被GC回收后,JVM会将虚引用对象加入到队列中,以便程序可以在需要的时候得到通知
        PhantomReference<TestClass> phantomReference = new PhantomReference<>(obj, QUEUE);

        // 该线程不断读取这个虚引用,并不断往列表里插入数据,以促使系统早点进行GC
        new Thread(() -> {
            while (true) {
                TEST_DATA.add(new byte[1024 * 100]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
                System.out.println("-------> " + phantomReference.get());
            }
        }).start();

        // 这个线程不断读取引用队列,当弱引用指向的对象被回收时,该引用就会被加入到引用队列中
        new Thread(() -> {
            while (true) {
                Reference<? extends TestClass> poll = QUEUE.poll();
                if (poll != null) {
                    System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
                    System.out.println("--- 回收对象 ---- " + poll.get());
                }
            }
        }).start();

        obj = null;

        try {
            Thread.currentThread().join();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    static class TestClass {
        private String name;

        public TestClass(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "TestClass - " + name;
        }
    }
}

回收方法区

方法区的垃圾收集主要回收两部分:废弃的常量和不再使用的类型。判定一个常量是否废弃相对简单,与对象类似,只要某个常量不再被引用,就会被清理。

而判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了,需要同时满足下面三个条件:

  • 该类的所有实例都已经被回收,即 Java堆中不存在该类及其任何派生子类的实例
  • 加载该类的类加载器已经被回收
  • 该类对应的 java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法

Java 虚拟机允许对满足上述三个条件的无用类进行回收,但并不是说必然被回收,仅仅是允许而已。关于是否要对类型进行回收,HotSpot 虚拟机提供了–Xnoclassgc参数进行控制

内存泄漏

程序中分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费(让JVM对象误以为此对象还在引用中,无法回收,造成内存泄漏)。

内存溢出

指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BORN(^-^)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值