类的加载和初始化

加载某个类指的是将该类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个该类的java.lang.Class对象(Class是java.lang包中的一个类),用来封装该类在方法区内的数据结构。类加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序提供了访问方法区内数据结构的接口。加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

类的加载是指将类的.class文件中的二进制数据读入到内存中的运行时数据区的方法区内,所以,首先要生成要加载类的.class文件(class文件是字节码格式文件),然后找到这个.class文件,加载到内存中。通常用文本编辑器或者是IDE编写的java程序是.java格式的文件,JVM不能直接运行.java文件,需要经过编译之后生成.class文件,才能被JVM运行,这时JVM将.class文件中的二进制数据读入到内存中,也就是将类加载到内存中。所以类的加载是发生在运行阶段,而不是编译阶段。是谁将类加载到内存中的呢?是何时加载的呢?又是如何加载的呢?

1、何时加载类以及如何加载类

类的加载是由类加载器(Classloader)完成的,即类加载器负责将类加载到内存中。既可以是饿汉式加载[eagerly load](只要有其它类引用了这个类,就将该类加载到内存中),也可以是懒加载[lazy load](等到初始化类的时候才加载,即按需加载)。一般情况下,为了提高系统性能,都是按需加载,也就是初始化类的时候才去加载类

如何加载类-类的加载机制

使用类加载器将类加载到内存中。类加载器其实也是Java类。有四大类:

启动类加载器(根加载器):Bootstrap Class Loader

扩展类加载器:Extension Class Loader

系统应用类加载器:APP Class Loader

用户自定义加载器:Customer Class Loader

Java程序在加载类之前,先要检查类是否已经被加载。

检查:检查类是否已经被加载,从底层往上层依次检查各个加载器已经加载的类,顺序是系统应用类加载器、扩展加载器、根加载器,一旦发现被某个加载器加载过,则马上使用该类。如果一直找到最顶层的根加载器,发现类还没有被加载进JVM运行数据区的方法区,则接下来就要加载该类。

加载:加载和检查顺序相反,从上层往下层的顺序进行加载。加载器检查自己的加载路径,从加载路径(包含jar包和class文件)中查找要加载的类的.class文件,一旦找到类就进行加载。

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

1)Bootstrap ClassLoader:负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

2)Extension ClassLoader:负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目录下的jar包

3)App ClassLoader:负责加载classpath中指定的jar包及目录中class

4)Custom ClassLoader:属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader,加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类在所有ClassLoader只加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

类加载器双亲委派机制

JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

由上图可知,类加载器均是继承自java.lang.ClassLoader抽象类(Bootstrap ClassLoader除外)。下面简要介绍一下java.lang.ClassLoader中几个最重要的方法。

//加载指定名称(包括包名)的二进制类型,供用户调用的接口
public Class<?> loadClass(String name) throws ClassNotFoundException{//…}
//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是,这里的resolve参数不一定真正能达到解析的效果),供继承用
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…}
//findClass方法一般被loadClass方法调用去加载指定名称类,供继承用
protected Class<?> findClass(String name) throws ClassNotFoundException {//…}
//定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{//…}

public Class<?> loadClass(String name) throws ClassNotFoundException {
  return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 首先,检测是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            //如果没有被加载,就委托给父加载器或启动类加载器加载
            try {
                if (parent != null) {
                    //父加载器不为空则调用父加载器的loadClass
                    c = parent.loadClass(name, false);
                } else {
                    //父加载器为空则调用Bootstrap Classloader
                    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();
                //父加载器没有找到,则调用findclass,使用自身的加载功能
                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()
            resolveClass(c);
        }
        return c;
    }
}

由上面源码可知,JVM在加载类时默认采用的是双亲委派机制。

2、何时初始化类

初始化是为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

 ①声明类变量时指定初始值

 ②使用静态代码块为类变量指定初始值

类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的

JVM初始化步骤:

 1、假如这个类还没有被加载和链接,则程序先加载并链接该类,然后再初始化该类。

 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类,然后再初始化该类。

 3、假如类中有初始化语句,则系统依次执行这些初始化语句。

类初始化时机:只有主动使用类时才会导致类的初始化,类的主动使用包括以下六种:

– 创建类的实例,也就是new的方式  (隐式初始化)

– 访问某个类或接口的静态变量,或者对该静态变量(不是常量)赋值

– 调用类的静态方法

– 反射(如Class.forName(“com.shengsiyuan.Test”))  (显示初始化)

– 初始化某个类时,则其父类先被初始化

– Java虚拟机启动时被标明为启动类的类,直接使用java.exe命令来运行某个主类

初始化类的顺序是怎样的

现在知道什么时候触发类的初始化了,他精确地写在Java语言规范中。但了解清楚域(fields,静态的还是非静态的)、块(block静态的还是非静态的)、不同类(子类和超类)和不同的接口(子接口,实现类和超接口)的初始化顺序也很重要。下面是类初始化的一些规则:

1、按照代码编写顺序从上到下初始化类,所以声明在顶部的字段早于底部字段的初始化

2、超类早于子类和衍生类的初始化

3、如果是由于访问静态域而触发类的初始化,那么只有声明静态域的类才被初始化。(如果在父类中声明了静态域,在子类中使用该静态域,只会触发父类的初始化,不会触发子类的初始化)。

4、接口初始化不会导致父接口的初始化。

5、静态域的初始化是在类的初始化期间,非静态域的初始化是在类的实例(即类的实例化)创建期间。所以静态域初始化在非静态域之前。

6、非静态域通过构造器初始化,子类在做任何初始化之前,子类构造器会隐含地调用父类的构造器,所以父类的非静态域或实例变量的初始化早于子类。

3、类的加载过程

JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)。链接又分为三个步骤,如下图所示:

1、装载:查找并加载类的二进制数据

2、链接:

(1)验证:确保被加载类的正确性;

(2)准备:为类的静态变量分配内存,并将其初始化为默认值;

(3)解析:把类中的符号引用转换为直接引用;

3、初始化:为类的静态变量赋予正确的初始值

在装载类时,查找是指类加载器(根加载器、扩展加载器、应用加载器)到默认路径下查找.class文件,或自定义加载器到指定路径下查找.class文件。

准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面再说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

加载:查找并加载类的二进制数据

加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

1、通过一个类的全限定名来获取其定义的二进制字节流。

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

3、在内存中(对于HotSpot虚拟机而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为对方法区中这个类的各种数据的访问入口。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

验证:确保被加载类的正确性

验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

Java语言本身是相对安全的语言,使用Java编码是无法做到如访问数组边界以外的数据、将一个对象转型为它并未实现的类型等,如果这样做了,编译器将拒绝编译。但是,Class文件并不一定是由Java源码编译而来,可以使用任何途径,包括用十六进制编辑器(如UltraEdit)直接编写。如果直接编写了有害的“代码”(字节流),而虚拟机在加载该Class时不进行检查的话,就有可能危害到虚拟机或程序的安全。

不同的虚拟机,对类验证的实现可能有所不同,但大致都会完成下面四个阶段的验证:文件格式验证、元数据验证、字节码验证和符号引用验证

 1、文件格式验证,是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。如验证魔数是否0xCAFEBABE;主、次版本号是否正在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型……该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区中,经过这个阶段的验证后,字节流才会进入内存的方法区中存储,所以后面的三个验证阶段都是基于方法区的存储结构进行的。

2、元数据验证,是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。可能包括的验证如:这个类是否有父类;这个类的父类是否继承了不允许被继承的类;如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法……

3、字节码验证,主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。

4、符号引用验证,发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生。验证符号引用中通过字符串描述的全限定名是否能找到对应的类;在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段;符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问

验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。

准备:为类的静态变量分配内存,并将其初始化为默认值

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是在Java代码中被显式地赋予的值。假设一个类变量的定义为:public static int value = 3;那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstantValue属性所指定的值。假设上面的类变量value被定义为: public static final int value = 3;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。

· 这里还需要注意如下几点:

· 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。

· 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的变量,既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。

· 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。

· 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

解析:把类中的符号引用转换为直接引用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用(Direct Reference):直接引用可以是直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)、相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)或是一个能间接定位到目标的句柄。直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

初始化:为类的静态变量赋予正确的初始值

初始化是为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

 ①声明类变量时指定初始值

 ②使用静态代码块为类变量指定初始值

类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的

JVM类初始化的步骤:

 1、假如这个类还没有被加载和链接,则程序先加载并链接该类,然后再初始化该类。

 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类,然后再初始化该类。

 3、假如类中有初始化语句,则系统依次执行这些初始化语句。

类初始化时机:只有主动使用类时才会导致类的初始化,类的主动使用包括以下六种:

– 创建类的实例,也就是new的方式

– 访问某个类或接口的静态变量,或者对该静态变量赋值(常量在准备阶段就被赋值了)

– 调用类的静态方法

– 反射(如Class.forName(“com.shengsiyuan.Test”))

– 初始化某个类时,则其父类先被初始化

– Java虚拟机启动时被标明为启动类的类,直接使用java.exe命令来运行某个主类

结束生命周期

•在如下几种情况下,Java虚拟机将结束生命周期

– 执行了System.exit()方法

– 程序正常执行结束

– 程序在执行过程中遇到了异常或错误而异常终止

– 由于操作系统出现错误而导致Java虚拟机进程终止

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值