JVM(二)

1.如何判断对象可以回收

  • 引用计数法
    给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”。
    但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因是引用计数法无法解决对象的循环引用问题。
    循环
  • 可达性分析法
    扫描堆中的对象,看是否能够沿着 GC Root对象为起点的引用链找到该对象,找不到,表示可以回收

GC Root包括:

  1. java虚拟机栈当前活动栈帧中的本地变量表中的引用的对象 (栈中当前正在使用的局部变量直接引用的对象)
  2. 方法区中的静态变量和常量引用的对象
  3. 正在使用的锁对象

GC Root对象通过引用链无法到达的对象就是垃圾对象

2.引用

A.强引用(Strong References)

我们使用的大部分引用实际上都是强引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

强引用的这种特点会导致出现内存泄漏的问题,所以我们可以手动切断这些对象与强引用之间的关联(设null),让JVM可以去回收它。

public static void f1() {
        List<Byte[]> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(new Byte[CAP]);
            System.out.println(i + "----------------------");
        }
    }
B.软引用(Soft References)

软引用是个对象,被软引用对象引用的(包装的)对象,即该对象被软引用

如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用,软引用可用来实现内存敏感的高速缓存

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

//软引用
    public static void f2() {
        //SoftReference软引用对象 是 被强引用的,但是Byte[]是被包装在软引用对象当中的,当内存不够的时候,Byte会被GC清除,但是软引用对象不会。
        List<SoftReference<Byte[]>> list = new ArrayList<>();
        //引用队列--泛型是软引用对象所引用的对象---实现当软引用对象内包装的对象被回收(那么软引用对象也就失去了意义,但是由于它是强引用,所以需要手动删除,主要是因为这时候还有引用指向它,非空。),软引用对象也回收的目的。
        ReferenceQueue<Byte[]> queue = new ReferenceQueue<>();
        for (int i = 0; i < 5; i++) {
            //当软引用对象内包装的对象被回收,就把软引用存进引用队列
            SoftReference<Byte[]> s = new SoftReference<>(new Byte[CAP], queue);//关联了一个引用队列
            list.add(s);
            print_(list);
        }
        //把引用队列的内容清掉,因为他们所包装的对象已经被清除了,队列里都是没意义的软引用对象。
        Reference ref = null;
        while ((ref = queue.poll()) != null) {
            list.remove(ref);
        }
        print_(list);
    }

    //打印
    public static void print_(List<SoftReference<Byte[]>> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(i + "----" + list.get(i) + "----" + list.get(i).get());
        }
    }
C.弱引用(Weak References)

如果一个对象只具有弱引用。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

//弱引用
    public static void f3() {
        //SoftReference软引用对象 是 被强引用的,但是Byte[]是被包装在软引用对象当中的,当内存不够的时候,Byte会被GC清除,但是软引用对象不会。
        List<WeakReference<Byte[]>> list = new ArrayList<>();
        //引用队列--泛型是软引用对象所引用的对象---实现当软引用对象内包装的对象被回收(那么软引用对象也就失去了意义,但是由于它是强引用,所以需要手动删除,主要是因为这时候还有引用指向它,非空。),软引用对象也回收的目的。
        ReferenceQueue<Byte[]> queue = new ReferenceQueue<>();
        for (int i = 0; i < 5; i++) {
            //当软引用对象内包装的对象被回收,就把软引用存进引用队列
            WeakReference<Byte[]> s = new WeakReference<>(new Byte[CAP], queue);//关联了一个引用队列
            list.add(s);
            print_1(list);
            if (i == 2) {
                System.gc();//手动gc
            }
        }
        //把引用队列的内容清掉,因为他们所包装的对象已经被清除了,队列里都是没意义的软引用对象。
        Reference ref = null;
        while ((ref = queue.poll()) != null) {
            list.remove(ref);
        }
        print_1(list);
    }

    //打印
    public static void print_1(List<WeakReference<Byte[]>> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(i + "----" + list.get(i) + "----" + list.get(i).get());
        }
    }
D.虚引用(Phantom References)

虚引用与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

特别注意,在程序程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢(OutOfMemory)等问题的产生。

public static void f4() {
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        PhantomReference<Byte[]> phantomReference = new PhantomReference<>(new Byte[CAP], queue);
        System.out.println(phantomReference.get());//null,他就是个垃圾,没必要拿到,如果拿到就违背了垃圾的意思
    }
E.终结器引用(Final References)

没有强引用引用对象时,虚拟机创建终结器引用,垃圾回收时将终结器引用加入引用队列,注意此时没有进行垃圾回收,一个优先级很低的线程Finalizer查看引用队列是否有终结器引用,找到这个对象调用finallize()方法就可以回收垃圾。不推荐使用,又慢又复杂。

3.垃圾回收算法

A.标记清除

标记清除

原理:将没有被GC Root直接或间接引用的对象进行标记,然后进行清除,清除时无需清理字节内容,只需要记录清除内容的起始和结束地址,放入空闲地址列表中即可。
优点:速度快
缺点:容易产生内存碎片

B.标记整理

标记整理

原理:将没有被GC Root直接或间接引用的对象进行标记,然后进行整理,将被引用的对象放在一起,这时需要更新引用对象
优点:有更多的连续内存空间
缺点:速度慢

C.复制

copy

原理:将内存空间分为为FROM和TO相等的两部分,将没有被GC Root直接或间接引用的对象进行标记,将FROM中的引用对象拷贝到TO,拷贝完成后,交换FROM和TO两个区域的名称。
优点:不会有内存碎片 , 效率较高
缺点:需要占用双倍内存空间

4.分代垃圾回收

代

  • 对象首先分配在伊甸园区域
  • 新生代(只要新生代from和伊甸园中任何一个空间不足都称作新生代空间不足。)空间不足时,触发 minor gc(新生代垃圾回收),伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from 和 to两个区域的名称
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行,当对象寿命超过阈值(15次)时,会晋升至老年代。
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,stop the world的时间更长,对老年代进行垃圾回收,具体算法为标记整理算法。

5.垃圾回收器

STW:世界停止时间,即 用户线程停止。

1)串行

  • 单线程
  • 堆内存较小,适合个人电脑
    串行

2) 吞吐量优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 让单位时间内,STW 的时间最短 ,垃圾回收时间占比最低,这样就称吞吐量高
    吞吐量

3) 响应时间优先(CMS并发标记清理算法)

  • 多线程
  • 堆内存较大,多核 cpu
  • 尽可能让单次 STW 的时间最短
    响应时间优先
  • 初始标记需要STW,只标记GC Root对象,时间很短
  • 并发标记,可以和其它用户线程并发执行
  • 重新标记,需要STW,因为并发标记阶段用户线程会有新的对象引用产生,对整个内存区做标记
  • 并发清理,并发清理所有垃圾,在此期间,用户线程会产生新的浮动垃圾
注意:如果产生垃圾速度快于清理速度-->full清除

4)G1

适用场景:

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region
  • 整体上是 标记+整理 算法,两个区域之间是 复制 算法
    G1垃圾回收阶段

4.1 Young Collection

  • stop the world

4.2 Young Collection + CM

  • 在 Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW)

4.3 Mixed Collection
会对 E、S、O 进行全面垃圾回收

  • 最终标记(Remark)会 STW
  • 拷贝存活(Evacuation)会 STW

总结

  • SerialGC(串行垃圾回收)
  1. 新生代内存不足发生的垃圾收集 - minor gc
  2. 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC(并行垃圾回收,吞吐量优先)
  1. 新生代内存不足发生的垃圾收集 - minor gc
  2. 老年代内存不足发生的垃圾收集 - full gc
  • CMS(并发标记清理垃圾回收,响应时间优先)
  1. 新生代内存不足发生的垃圾收集 - minor gc
  2. 可以设置老年代内存占用阈值,进行垃圾回收
    垃圾产生速度高于垃圾回收速度,发生full gc
  • G1
  1. 新生代内存不足发生的垃圾收集 - minor gc
  2. 老年代内存不足:可以设置老年代内存占用阈值,进行垃圾回收
    老年代占比超过阈值,触发并发标记和混合收集;
    在垃圾收集过程中,回收垃圾速度大于产生垃圾的速度,处于并发垃圾收集阶段,不触发full gc;
    在垃圾收集过程中,回收垃圾的速度小于产生垃圾的速度,并发收集失败,退化为串行收集,触发full gc

6.类加载(过程)

类加载包括:加载、链接和初始化三个步骤,其中链接又包括验证、准备和解析三个阶段。主要由类加载器完成。

  • 加载:
    将类的字节码载入方法区中,生成大的Class实例。
  • 链接:
    链接分为三个阶段:验证,准备,解析。
  1. 验证:验证类是否符合 JVM规范,安全性检查,确保被加载类的正确性。
  2. 准备:为类的静态变量分配内存【并设置默认值 0 】。
    static变量分配空间和赋值是两个不同步骤,其中分配空间在准备阶段完成(即设置默认值),而赋值在初始化阶段完成。
  • 如果static变量是final修饰的基本类型或者字符串被赋值字面量,那么编译阶段值就确定了,赋值在准备阶段完成。
  • 如果 static 变量是 final 的,但属于引用类型(除String字面量外),那么赋值也会在初始化阶段完成。
  1. 解析:把类中的符号引用转换为直接引用。
  • 初始化:

注意:类的初始化仅有一次:在类第一次被使用时会初始化---如果有继承关系:父类的一切在先。

静态们:类属性—在类的初始化完成
实例们:对象属性—在对象的初始化完成

类加载:把静态的初始化
静态变量们先赋默认值0,实例变量们是对象属性,不是类属性,当作看不见,视为空气。然后从头到尾按顺序走一遍,该赋值赋值,该调方法调方法,忽略非静态的方法和属性。

对象加载:把实例的初始化
实例变量们先赋默认值0,静态变量们不变(由于类加载在对象的前边,所以静态的已经初始化过了。),视为空气。然后从头到尾按顺序走一遍,该赋值赋值,该调方法调方法,忽略静态的方法和属性。注意构造方法最后执行

7. 类加载器

A.启动类加载器
B.扩展类加载器
C.双亲委派模式
一个类加载器加载类时,若存在上级类加载器,则委托给上级类加载器,若上级类加载器依然有上级,就再向上委托,直到启动类加载器,若启动类加载器能够加载则完成加载。否则,按原路线返回,返回给下级加载,若下级能加载则加载,否则再向下返回加载,直到返回起始调用加载的类加载器,若没有合适的类加载器则报异常。
方法
注意:loadClass中包含了双亲委派机制的实现,而后面的加载是通过findClass去实现的。所以,如果我们自己实现一个类加载,而想保持双亲委派机制,就一般不重写loadClass,而去重写findClass;如果想破坏双亲委派机制,那么就重写loadClass。

为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改 避免类的重复加载:

  • 当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值