首页 > 编程语言 > JVM类加载,垃圾回收
2021
10-12

JVM类加载,垃圾回收

类加载子系统

classLoader 只负责对字节码文件的加载,至于是否可以运行,还要看执行引擎。

在这里插入图片描述

  • 加载的类信息存放于方法区的内存空间,除了类信息之外,还会存放有运行时常量池的信息,还可能包含字符串字面量和数字常量。

loading加载:

通过一个类的全限定名获得定义此类的二进制字节流。也就是根据完整的路径找到对应类的二进制字节流。

将这个字节流所代表的静态的数据结构转化为方法区运行时的数据结构

  • 在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个方法数据的入口。

链接:验证、准备、解析

验证:目的在于确保二进制字节流包含的信息符合虚拟机的要求,保证被加载类的正确性,不会危害虚拟机的安全。

验证文件格式、字节码、元数据、符号引用的验证

准备:为类变量分配内存并设置默认的初始值,即零值。

不包含被final修饰的类变量

解析:将常量池的符号转化为直接引用的过程。

初始化:JVM将程序的执行权交给程序。

双亲委派模型

双亲委派模型的原理:如果一个类加载器收到了类加载的请求的话,它首先不会自己去尝试加载这个类,而是把这个请求委派给自己的父类加载器去完成,每一层的加载器都是如此,因此所有的加载请求都会传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法加载这个请求的时候,即父类搜索不到这个类的时候,子类才会自己尝试去加载。

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

在这里插入图片描述

使用双亲委派模型来组织类加载之间的关系,一个显而易见的好处就是Java中类随着它的类加载器一起具备了一种带有优先级的层级关系。因此Object类在程序中的各种类加载的环境中都能保证是同一个类。反之,如果没有双亲委派模型的话,都由各个的类加载器去加载的话,如果用户自定义了一个java.lang.Object的类,并放在ClassPath 中的话,就会出现多个Object类,Java中最基础的体系也就会无法保证。

破坏双亲委派模型:

1.双亲委派模型被破坏的第一次就是刚引入双亲委派模型的时候,是为了兼容JDK1.2之前的代码

2.双亲委派模型的第二次的破坏就是自生的缺陷导致的。但发生父类调用子类的时候。

3.第三次就是用户对于程序动态性的追求而导致的:代码热的替换、模块热的部署。

垃圾回收

什么是垃圾?

GC 中的垃圾就是特指在内存中的、不会在被使用的对象。

判断对象已死

引用计数器法:

每个对象添加一个引用计数器,每被引用一次,计数器加1,失去引用,计数器减1,当计数器在一段时间内保持为0时,该对象就认为是可以被回收得了。(在JDK1.2之前,使用的是该算法)

缺点:当两个对象A、B相互引用的时候,当其他所有的引用都消失之后,A和B还有一个相互引用,此时计数器各为1,而实际上这两个对象都已经没有额外的引用了,已经是垃圾了。但是却不会被回收,引用计数器法不能解决循环引用的问题。

JVM垃圾回收采用的是可达性分析算法:

在这里插入图片描述

从GC set中的GC roots 作为起点,从这些节点向下搜索,搜索的路径被称为引用链,如果一个对象不存在引用链的话,那么说明这个对象已死。就会被GC回收器回收。

GCroots 是:

1.来自于JVM栈中引用的对象。比如各个线程中被调用的方法堆栈中使用到的参数、局部变量、临时变量等。

2.方法区中静态属性引用的对象。比如Java中的引用类型静态变量。

3.方法区中常量引用的对象。比如字符串常量池(String Table)中的引用。

4.本地方法栈中引用的对象。

5.Java虚拟机内部的引用。比如基本数据类型对应的Class对象,一些常驻的异常类等,还有系统的类加载器。

6.所有被同步锁持有的对象。 …

并不是所有的引用对象一定就会被GC 的时候回收掉。

JDK1.2之后的四种引用类型:

1.强引用:

就是程序中一般使用的引用类型,即使发生OOM也不会回收的对象。

就是为刚被new出来的对象所加的引用,它的特点就是,永远不会被GC,除非显示的设置null,才会GC。

比如:

package Reference;
/**
 * user:ypc;
 * date:2021-06-19;
 * time: 9:40;
 */
public class Test {
    public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder("I am FinalReference");
        System.gc();
        System.out.println(stringBuilder);
        //触发GC
        byte[] bytes = new byte[1024 * 940 * 7];
        System.gc();
        System.out.println(stringBuilder);
        try {
            byte[] bytes2 = new byte[1024 * 1024 * 7];
        } catch (Exception e) {
        } finally {
            System.out.println("发生了OOM:");
            System.out.println(stringBuilder);
        }
    }
}

在这里插入图片描述

2.软引用:

就是用来描述一些还有用,但是非必须的对象,只被软引用关联着的对象,在系统将要发生OOM前会回收的对象。如果这次回收还没有足够的内存,才会抛出OOM的异常。

package Reference;
/**
 * user:ypc;
 * date:2021-06-19;
 * time: 9:46;
 */
public class SoftReference {
    public static class User {
        private int id;
        private String name;
        User(int id, String name) {
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    public static void main(String[] args) {
        //user 是强引用
        User user = new User(1001, "小明");
        //softReference 是软引用
        java.lang.ref.SoftReference<User> softReference = new java.lang.ref.SoftReference<>(user);
        //  显示的将强引用置为null
        user = null;
        System.out.println(softReference.get());
        System.gc();
        System.out.println("After GC: ");
        System.out.println(softReference.get());
        //触发GC
        byte[] bytes = new byte[1024 * 940 * 7];
        System.gc();
        System.out.println(softReference.get());
    }
}

在这里插入图片描述

3.弱引用:

弱引用也是用来描述那些非必要的对象,它的强度比软引用还低一些。当垃圾回收器开始工作的时候,无论内存是否够用,都会回收掉只被弱引用关联着的对象。

    public static void main(String[] args) {
        User user = new User(1002,"小明");
        java.lang.ref.WeakReference<User> weakReference = new java.lang.ref.WeakReference<>(user);
        user = null;
        System.out.println(weakReference.get());
        System.gc();
        System.out.println("After GC");
        System.out.println(weakReference.get());
    }

在这里插入图片描述

4.虚引用:

它是最弱的一种引用关系。刚被创建就会被GC回收器回收。它的价值就是在GC 的时候触发一次方法的回调。

虚拟机中的并行与并发:

并行:

并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在同时的进行工作。

并发:

并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器的线程和用户线程之家同时在运行。

常见的垃圾回收算法:

1.标记?清除算法:(Mark?Sweep)

将死亡的对象标记,然后进行GC。

在这里插入图片描述

执行的效率不稳定。如果堆中有大量的对象,其中有大部分都是要被回收的话 ,那么必需要进行大量的标记?清除的步骤,导致执行效率的降低。

会造成内存碎片的问题,使空间的利用率降低。标记?清除之后会产生大量的不连续的内存碎片。

2.标记?复制算法:

将空间分为两部分,一部分始终是未使用的状态。当进行垃圾回收的时候

将存活的对象复制到未使用的空间上,然后将另一半的区域进行全GC。

在这里插入图片描述

但是标记?复制算法在对象存活率比较高的时候就要进行多次的复制操作,效率会降低。而且每次只有50%的内存空间被使用。

3.标记?整理算法:

将存活的对象进行移动,然后进行GC。

对象的移动需要STW

在这里插入图片描述

解决了垃圾碎片的问题

常见的垃圾回收器:

在这里插入图片描述

  • Serial/Serial Old收集器 是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
  • ParNew收集器 是Serial收集器的多线程版本,使用多个线程进行垃圾收集。
  • Parallel Scavenge收集器 是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。
  • Parallel Old收集器 是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。
  • CMS(Concurrent Mark Sweep)收集器 是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。
  • G1收集器 是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

新时代、老年代

Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、FullGC ( 或称为 Major GC )。 Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。

当一个对象被判定为 “死亡” 的时候,GC就有责任来回收掉这部分对象的内存空间。新生代是GC收集垃圾的频繁区域。当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 也就是经历15次GC之后还存活的对象),这些对象就会被移动到老年代。

但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。

另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

为什么大对象会直接存在老年代?

大对象的创建和销毁随需要消耗的时间比较多,所以性能也比较满,如果存到新生代的话,那么有可能频繁的创建和销毁大对象,导致JVM对的运行的效率变低,所以直接存放在老年代。

在这里插入图片描述

新生代的各个区域的占比分别是:8:1:1 新生代与老年代的占比是:1:2

在这里插入图片描述

总结

本篇文章就到里了,希望能帮到你,也希望您能多多关注自学编程网的更多内容!

编程技巧