类的生命周期

概述

类加载是一个将.class字节码文件实例化成Class对象,并进行相关初始化的过程。

1)类的加载

  类的加载是指把类的.class文件中的二进制数据读入到内存中,把它存放在运行时数据区的方法区内,然后在堆区创建一个java.Lang.class对象,用来封装类在方法区内的数据结构。

  1)首先我们有class文件,这些class文件会被ClassLoader(类加载器) Load到内存中CodeSegment,站在ClassLoader的角度来讲,这里面的一个个的class就是一个个的对象。

  2)运行环境找到main方法开始执行

  3)运行过程中会有更多的其他class被load到内存。

可见加载的过程是动态加载,不是从头到尾扫描我们的整个文件,把所有用到的.class加载到内存,而是用到的时候在加载,需要的时候在加载(如 new关键字创建实例),运行期间动态加载。

  类的加载的最终产品是位于运行时数据区的堆区的Class对象,Class对象封装了类在方法区内的数据结构,并且向java程序提供了访问类在方法区类的数据结构的接口。

类的加载是由类加载器完成。类加载器可分为两种

1) java虚拟机自带的类加载器:包括启动类加载器,扩展类加载器和系统类加载器。

2)用户自定义的类加载器:是java.long.ClassLoader类的子类的实列,用户可以通过它来制定类的加载方式。 

类加载器并不需要等某个类"首次主动使用"时再加载它,java虚拟机规范允许类的加载器在预料某个类将要被使用时就预先加载它。如果预先加载过程中遇到.class文件缺失等错误,类加载器必须等到程序主动使用时才抛错。

2)类的连接

当类被加载之后,就进入连接阶段。连接就是把已经读入到内存的类的二进制数据合并到虚拟机的运行环境中去。

1)类的验证:连接的第一步是类的验证,保证被加载的类有正确的内部结构,并且与其他类协调一致。如果虚拟机检查出错误就会抛出相应的Error对象。

类的验证主要包括以下内容:

1.1)类文件结构的检查:确保类文件遵从java类文件的固定格式。

1.2)语义检查:确保类本身符合java语言的语法规定,比如验证final类型的类没有子类

1.3)字节码验证:确保字节码流可以被java虚拟机安全的执行

1.4)二进制兼容的验证:确保相互引用的类之间协调一致。例如 在A类的a()方法中会调用B类的b()方法。java虚拟机在验证A类时会检查在方法区内是否存在B类的b()方法。

  也许你会问:由java编译器生成的二进制数据肯定是正确的,为什么还要进行类的验证呢,因为 这个.class文件可能是黑客特制的,类的验证能提高类的健壮性,确保程序被安全的执行。

2)类的准备:在准备阶段java虚拟机为类的静态变量分配内存,并设置默认的初始值

public static int a=1;

在准备阶段,将为int类型的静态变量a分配四个字节的内存空间,并且赋予默认值0

3)类的解析:在解析阶段,java虚拟机会把类的二进制数据中的符号引用替换为直接引用

列如 Worker类的goWork()方法会引用Car类的run()方法:

public void goWork(){
    car.run();
}

在Worker类的二进制数据中,包含了一个队Car类run()方法的符号引用,它由run()方法的全名和相关描述组成。在解析阶段,java虚拟机会把这个符号的引用替换成一个指针,该指针指向Car类run()方法在方法区内的内存位置,这个指针就是直接引用。

3)类的初始化

  类的准备阶段是为静态变量设置默认初始值,在初始化阶段,java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。

  其中静态变量的声明语句以及类的静态代码块都可以看做类的初始化语句。

  在以下代码中,静态变量 a和b都被显示的初始化  a是1  b是2.

    public static int a=1;
    public static int b;
    static{
        b=2;
    }

  java虚拟机初始化一个类包括以下步骤:

  1)如果类还没有加载和连接就先进行加载和连接。

  2)如果类存在直接的父类,并且这个父类还没有被初始化,那么就先初始化直接的父类,

  2)如果父类存在初始化语句,那么就依次执行这些初始化语句

类的初始化时机

  在类或接口被加载和连接的时机上,java虚拟机规范给实现提供了一定灵活性,但是他严格定义了类的初始化时机,所有的java虚拟机实现必须在某个类或接口被java程序首次主动使用才初始化他们。

java程序对类的使用方式可以分为两种:主动使用和被动使用

只有6种活动被看做是程序对类或接口的主动使用

1)创建类的实列(包括new、反射、克隆序、列化)

2)调用类的静态方法

3)访问某个类或接口的静态变量,或对该静态变量赋值

4)调用API中某些反射方法,如(Class.forName("")方法)

5)初始化一个类的子类

6)java虚拟机启动时被标明为启动的类

除了上述六种方式 其他使用java类的方式都被看作被动使用,都不会导致类的初始化

1)对final类型的静态变量,如果在编译期间就能够计算变量的取值,那么这种变量被看做编译时常量。java程序中对编译时常量的引用被看做是对类被动引用,不会导致类的初始化。

    public static final int a=2*3; //变量a是编译时常量
    public static final int b =(int) (Math.random()*5);//变量b不是编译时常量 

2)调用ClassLoader类的LoaderClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

双亲委派加载机制

  类加载器用来把类加载到java虚拟机中。从JDK1.2版本开始,类的加载过程采用父亲委托机制,这种机制更能保证java平台的安全性。在此委托机制中,除了java虚拟机自带的根类加载器以外,其余类加载器都有且只有一个父加载器。

  当java程序请求加载器loader1区加载A类时,loader1收下委托自己的父加载器去加载A类,若父加载器能够加载,则由父加载器完成任务,否则才由加载器loader1本身加载A类,在此机制下,用户自定义的类加载器不可能加载应该由父类加载器加载的可靠类。

  java虚拟机自带以下几种加载器

  1)根(Bootstrap)加载器:该加载器没有父类加载器。它负责加载虚拟机的核心类库,如java.lang.*等。

  2)扩展(Extension)类加载器:它的父类加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或者从jdk的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。

  3)系统(System)类加载器:也称为应用类加载器,它的父类加载器为扩展类加载器。它从classpath环境变量或者系统属性java.class.path所指定的目录中加载类。

  除了虚拟机自带类加载器,用户还可以定制自己的类加载器,java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器应该继承ClassLoader类

然后覆盖它的findClass(String name)方法。在JDK的java.net包中,提供了一个比较强大的URLClassLoader类,它扩展了ClassLoader类

 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;
        }
    }

首先往上查找有没有加载的过程parent.loadClass(),然后往回委托给自己的孩子真正加载的过程 findClass

自定义ClassLoader 就是重写findClass()方法(父类的findClass()方法直接抛出了个异常 就是让你用来重写的),类似模板方法,钩子函数设计模式

重写loadClass()方法可以打破双亲委派机制

类的卸载

  当A类被加载 连接和初始化后,它的声明周期就开始了。当代码A类的Class对象不再被引用,即不可触及时,那么Class对象就会结束声明周期。

由此可见,一个类何时结束声明周期,取决于代表它的Class对象何时结束声明周期。

参考博客:虚拟机如何判断对象是否需要回收 https://www.cnblogs.com/ssskkk/p/9339180.html#_label1

细说引用

从JDK1.2开始,把对象的引用细分为四种级别,从高到低依次为:强引用、软引用、弱引用、和虚引用。

在java.lang.ref包中提供了一个表示引用的抽象父类Reference,它由三个具体的子类:SoftReferenceWeakReference类和PhantomReference类 分别代表 软引用 弱引用和虚引用。ReferenceQueue类表示引用队列,它可以和这三种引用联合使用,以便跟踪java虚拟机回收所引用的对象的活动。

详细介绍强软弱虚四种引用

补充:对象的生命周期

不管采取哪种方式创建对象,Java虚拟机创建一个对象都包含以下步骤。

1)class loading

2)class linking

3)class initializing

4)给对象分配内存

5)将对象的实列变量自动初始化变为其变量类型的默认值。(成员变量赋默认值)

6)初始化对象。在初始化过程中主要负责给实列变量赋予正确的初始值。然后执行构造方法(执行构造方法语句)

posted @ 2018-11-11 17:58  palapala  阅读(314)  评论(0编辑  收藏  举报