JAVA类的加载、连接与初始化

时间:2022-04-10 23:11:54

 

JAVA类的加载、连接与初始化

类的声明周期总共分为5个步骤1、加载2、连接3、初始化4、使用5、卸载

当java程序需要某个类的时候,java虚拟机会确保这个类已经被加载、连接和初始化,而连接这个类的过程分为3个步骤

1、 加载:查询并加载这个类的二进制数据

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

java虚拟机可以从多种来源加载类的二进制数据,

a)       从本地文件系统中加载类的.class文件,常用的方式

b)       通过网络下载.class文件

c)        从ZIP、JAR或其他类型提取.class文件

d)       从一个专有数据库中提取.class文件

e)       把一个Java源文件动态编译为.class文件

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

 JAVA类的加载、连接与初始化

 

类的加载是由类的加载器完成的,类的加载器分为两种:

a)       java虚拟机自带的加载器,包括启动类加载器,扩展类加载器,和系统类家在西

b)       用户自定义加载器:ClassLoader类的子类,用户可以通过实现该类来定制类的加载方式

类的加载器并不需要等到某个类被首次使用时初始化,java虚拟机规范允许类加载器在预料某个类将要被使用时优先加载它,如果在预先加载的过程没有找到.class文件或者存在错误,类加载器必须等到程序首次主动使用该类时才抛出LinkageError异常,如果这个类一直没有被程序主动使用,则类加载器不会抛出异常

2、 连接:包括验证阶段、准备阶段、和解析二进制数据阶段

a)       验证阶段:验证类的正确性,如类的语法

类的验证目的是确定java类二进制数据的正确性,也因为java虚拟机不知道.class文件是如何被创建的,有可能是正常创建,也有可能是黑客特质破坏虚拟机的所以要有验证环节,提高程序的健壮性

类的验证包括:

01、           类文件的结构检查:确保类文件遵循java类的固定格式

02、           语义检查:确保类本身符合java语法规定,如验证final修饰的类是否有子类、final修饰的方法是否有重写

03、           字节码验证:确保字节码流可以被java虚拟机正确的执行,它是由操作码的单字节指令组成的序列,每一个操作码都跟着一个或多个操作数,java虚拟机会验证该操作数是否合法

04、           二进制兼容验证:确保类与类之间引用的协调性;如A类中a方法引用B类中b方法,虚拟机会验证A类时会检查方法区内是否有B类的b方法,如果不存在或不兼容时,会抛出NoSuchMethodError异常

b)       准备阶段:为类的静态变量分配内存空间,并将其赋予默认值,注意是初始值 如staticint类型初始值为0

c)        解析阶段:将类中的符号引用转换为直接引用,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符类符号引用进行。如在A类中a方法中引用了B类中的b方法,将b方法放入A类中

3、 初始化:为类的静态变量赋予正确的初始值;如static inta=50;这时50才赋给a这个静态变量,所有java虚拟机在每隔类或接口被java程序首次主动使用时才初始化它们,并且初始化阶段时执行类构造器<clinit>()方法的过程,<clinit>()方法是由编译器自动收集类中的所有类变量赋值动作与静态代码块中的语句合并产生的,编译器收集顺序是由语句在源文件出现的顺序所决定的;如静态代码块只能访问定义在静态代码块之前的变量,而在之后定义的变量可以赋值,但不可以访问,如:

publicclass Test{

        static{

               i=1

               System.out.println(i)//该行代码会编译错误:Cannot reference a field before it is defined

}

static int i;

}

上面说到<clinit>()方法,<init>()方法这两个方法是class文件的两种编译产生方法

它们的区别

<clinit>方法是在虚拟机装在一个类的时候调用<clinit>方法。而<init>方法则是在一个类实例化的时候调用

<clinit>方法与<init>方法的不同是<clinit>不需要显示的调用父类构造器,虚拟机会在保证子类<init>方法执行之前父类的<clinit>方法已经执行完毕,着也就意味着在父类中定义的静态语句会优先于子类变量以及静态代码块

而<clinit>方法对于类或接口不是必须的,如:一个类或者接口没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为该类生产<clinit>方法,因此,一个父接口不会因为子接口或实现类的初始化而初始化,只有当程序首次使用父接口或特定接口中的静态变量时才会导致初始化;

虚拟机会保证一个类<clinit>方法在多线程环境中被正确的加锁、同步,如果由多个线程同时去初始化一个类,那么只有一个类回去执行<clinit>操作,其他线程会阻塞等待,直到该类<clinit>执行完毕,而如果一个类的<clinit>方法要做的事情很多,就可能造成多个线程阻塞,在实际应用中这种阻塞是被隐藏的

测试代码:

public class Text{

static class DeadLoopClass

    {

        static

        {

            if(true)

            {

                System.out.println(Thread.currentThread()+"init DeadLoopClass");

                while(true)

                {

                }

            }

        }

    }

public static void main(String[] args) {

        Runnable runnable=new Runnable() {

              

               @Override

               public void run() {

                      System.out.println(Thread.currentThread()+" start");

                DeadLoopClass dlc = new DeadLoopClass();

                System.out.println(Thread.currentThread()+" run over");                    

               }

        };

        Thread a=new Thread(runnable);

        Thread b=new Thread(runnable);

        a.start();

        b.start();

}

}

运行结果:(即一条线程在死循环以模拟长时间操作,另一条线程在阻塞等待)

Thread[Thread-0,5,main] start

Thread[Thread-1,5,main] start

Thread[Thread-1,5,main]init DeadLoopClass

需要注意的是:虽然其他线程会被阻塞,但如果执行了<clinit>方法那条线程退出<clinit>方法之后,其他线程唤醒之后也不会在进入<clinit>方法,在同一个类加载器下,一个类型只会被初始化一次

测试代码:(将静态内部类代码换为)

static class DeadLoopClass{

              static

        {

            System.out.println(Thread.currentThread() + "init DeadLoopClass");

            try {

                            TimeUnit.SECONDS.sleep(10);

                     } catch (InterruptedException e) {

                            e.printStackTrace();

                     }

        }

}

执行结果:

Thread[Thread-0,5,main] start

Thread[Thread-1,5,main] start

Thread[Thread-0,5,main]init DeadLoopClass

Thread[Thread-0,5,main] run over

Thread[Thread-1,5,main] run over

虚拟机严格规定了有5中情况必须要对类进行初始化

1、 当遇到new,getstatic,putstatic,invokestatic这种失调字节码指定时,如果类没有进行初始化则先初始化,生成这4条指令的常见java场景:使用new关键字创建对象时、读取或者设置一些类的静态字段(被final修饰或已经在编译器常量池的静态字段除外,但final类型的静态常量,如果在编译是不能计算出常量的取值,则会看作对类的主动使用,会初始化该类)或静态方法的时候,以及要调用静态方法的时候

2、 当使用java.lang.reflect包进行反射调用的时候如果类没有初始化,则先初始化

3、 当初始化一个类的时候发现父类没有进行初始化,这时要先初始化父类

4、 虚拟机启动时,用户要执行的主类(如包含main方法的类),如果没有初始化要先进行初始化

5、 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

当定义数组的引用类不会触发此类的初始化阶段

测试代码

public static void main(String[] args) {

              DeadLoopClass[] loopClasses=new DeadLoopClass[10];

}

结果什么都没有

常量在编译阶段会存入类的常量池中,本质想没有直接引用定义常量的这个类,所以不会触发该类的初始化

测试代码:

public class Text{

       final static String mm="abc";

       static{

              System.out.println("执行了静态代码块");

       }

}

public class Test01 {

       public static void main(String[] args) {

              System.out.println(Text.mm);

       }

}

注意:main方法不可以写在Text类中因为含有main方法的类会被初始化

初始化大体步骤

1、 假如这个类还灭有被加载和连接,那就先加载和连接

2、 假如类中存在直接的父类,或者间接父类,并却该父类没有被初始化,则先初始化父类

3、 类中的初始化语句从上到下执行

类的加载器

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

根(bootstrap)加载器:该加载器没有父类加载器。他负责加载虚拟机核心类库,如java.lang.*等。从下面例子可以看出java.lang.Object就是由根类加载器加载的,根加载器是从系统属性sun.boot.class.path所指定的目录加载类库。根加载器的实现依赖于底层的操作系统,属于虚拟机实现的一部分。

扩展(Extension)类加载器:它的父加载器是根加载器。他从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户JAR文件放在该目录下,会自动由扩展类加载,扩展类加载器是纯java类,是java.lang.ClassLoader类的子类

系统(System)类加载器:也成应用加载器,它的父类是扩展类加载器,它的环境变量classpath或者系统属性java.class.path所指定的目录的加载类,它是用户自定义的类加载器的默认父加载器。系统加载器是纯java类,是java.lang.ClassLoader类的子类

用户自定义类加载器:实现java提供的系统加载器ClassLoader抽象类

类加载器关系图:

 JAVA类的加载、连接与初始化

 

测试Object的类加载器是根加载器:

public class Text{

public static void main(String[] args) {

        Class c;

        ClassLoader cl,cll;

        cl=ClassLoader.getSystemClassLoader();//获取系统加载器

        System.out.println(cl);

        while(cl!=null){

               cll=cl;

               cl=cl.getParent();

               System.out.println(cll+"这个类的父类加载器是"+cl);

        }

        try {

               c = Class.forName("java.lang.Object");

               cll=c.getClassLoader();//获取Object的类加载器

               System.out.println("Object的类加载器是"+cll);

               c = Class.forName("test.Text");

               cll=c.getClassLoader();//获取当前类的加载器

               System.out.println("Text的类加载器是"+cll);

        } catch (Exception e) {

               e.printStackTrace();

        }

}

}

打印结果:

sun.misc.Launcher$AppClassLoader@6fd7bd04

sun.misc.Launcher$AppClassLoader@6fd7bd04这个类的父类

加载器是sun.misc.Launcher$ExtClassLoader@3cba8af9

sun.misc.Launcher$ExtClassLoader@3cba8af9这个类的父类加载器是null

Object的类加载器是null

Text的类加载器是sun.misc.Launcher$AppClassLoader@6fd7bd04

第一行获取了系统加载器

第二行是系统加载器的父加载器是扩展加载器,

第三行是系统加载器的父加载器是null(根加载器,根加载器是用null表示,这是为了保护虚拟机的安全,防止黑客利用根加载器,加载非法的类,从而去破坏虚拟机)

第四行Text的类加载器是系统加载器ClassLoader类加载

类加载器的父亲委托(Parent Delegation)机制

在父亲委托机制种,每个加载器都按照父子关系形成树形结构,出了类的跟加载器以外,其他加载器都有且只有一个父类加载器,

例:

loader2继承了loader1,loader1继承了ClassLoader这时loader2去加载一个类

Class c=loader2.loadClass(“Text”)

执行过程是loader2回去自己的命名空间查找该类是否被加载,如被加载直接返回该类的Class引用

如果没有加载则loader2会请求loader1代加载,loader1在请求ClassLoader,ClassLoder在请求扩展加载器,扩展加载器在请求根加载器,如果父类加载器不能加载则子类加载器加载,一次类推,有一级可以加载则返回Class引用,如果所有加载器都不能加载则抛出ClassNotFoundException

如有一个类能够成功加载Text类,那么这个类加载器被称为定义加载器,能够返回Class对象引用的类加载器和定义类加载器都称为初始类加载器,

假设loader1成功加载了Text类那么loader1是定义类加载器,而loader2是Text类的引用加载器,所以loader1,loader2都是Text的初始类加载器

注意:加载器之间的父子关系实际上是加载器对象之间的包装关系,而不是类之间的继承关系,一对父子加载器可能是同一个加载器类的两个实例,也可能不是。在子加载器对象中包装了一个父加载器对象,如:

public class MyClassLoader extends ClassLoader{

private ClassLoader loader;

       public MyClassLoader(){}

       public Test01(ClassLoader loader) {

               super(loader);

       }

       @Override

       public Class<?> loadClass(String name) throws ClassNotFoundException {

               return super.loadClass(name);

       }

}

ClassLoaderloader1=new MyClassLoader();

ClassLoader loader2=new MyClassLoader(loader1);

父亲委托机制的优点是提高软件体统的安全性。因为在刺激之下,用户自定义的加载器不可能加载父类加载器的可靠类,从而防止不可靠的代码去代替父加载器去加载可靠的代码,例如java.lang.Object类只能由根类加载器加载,其他用户任何自定义的类加载器都不不可能加载含有恶意代码的Object类

命名空间

每个类加载器都有自己的命名空间,命名空间是由该加载器以所有父加载器所加载的类组成。在同一个命名空间中不会出现类的完全限定名一样的两个类,但在不同的命名空间中有可能会出现两个完全限定名一样的类

当同一个.class文件被一个用户自定义loader1加载器加载和用户自定义loader3加载器加载这时方法区会生成两个该class类,也就是说在loader1和loader3各自的命名空间中都存在Sample和Dog类

 JAVA类的加载、连接与初始化

 

不同的类加载器的命名空间存在以下关系:

1、 同意命名空间内的类是相互可见的

2、 子加载器的命名空间包含所有附加在其的命名空间,因此子加载器加载的类能看见父加载器加载的类,如系统加载器加载的类可以看见根加载器加载的类。

3、 由父加载器加载的类对子加载器加载的类是不可见的

4、 两个加载器之间没有直接或间接父子关系,则这两个加载器加载的类相互不可见

所谓A类可见B类是在A类中可以引用B类的名字,如:

class A{

B b=new B();

}

两个不同的命名空间内的类是项目不可见的,但可以通过java的反射机制来访问对象的实例与方法。

运行时包

由同一个类加载器加载属于同包的类组成可运行时包,决定两个类是不是一运行时包要看它们的包名是否相同,还要看加载器是否相同。只有属于同意运行时包的类才能访问默认权限修饰符的类和类的成员,这样避免了用户自定义的类去冒充核心类库的类,如自定义了一个java.lang.Spy,并由用户自定义的类加载器加载,由于java.lang.Spy和核心类库不是一个加载器加载的,它们属于不同运行时包,所以java.lang.Spy不能访问核心类的java.lang包下面的默认权限修饰符的成员

URLClassLoader

在JDK的java.net包中,提供了一个功能强大的URLClassLoader类,它不仅能从本地文件中加载类,还可以从网上下载类。java程序可直接用URLClassLoader类作为用户自定义的类加载器

URLClassLoader类的构造方法:

public URLClassLoader(URL[] urls);//urls是存放URL的数组

public URLClassLoader(URL[] urls, ClassLoader parent)//parent是指定父加载器

URLClassLoader类的默认父加载器是系统加载器

简单运用:

URLurl=new URL(“www.XXXX.com/java/classes/”);

URLClassLoader loader=new URLClassLoader(new URL[]{url});

Class<?> clazz=loader.loadClass(“XXX”);

clazz.newInstance();

4、 类的卸载

由java虚拟机自带的类加载器加载的类,在虚拟机的整个声明周期中,始终不会卸载,如根加载器、扩展加载器、系统加载器,虚拟机本身会始终引用这些类加载器,而类加载器会始终引用它们所加载的Class对象,因此这些Class对象始终是可触及的;

一个类的何时被卸载的是当该类的Class对象不再被引用时,该类在方法区内的数据也会被卸载

由用户自定义的类加载器所加载的类是可以被卸载的,在类加载器的内部视线中,是用java集合来存放所有加载类的引用,

运行代码:

 JAVA类的加载、连接与初始化

 

 JAVA类的加载、连接与初始化

 

当将引用变量变为null的时候,此时Sample对象结束声明周期,和类加载器MyClassLoader也结束生命周期这时,Sample类在方法区的二进制数据就被卸载

执行结果:

JAVA类的加载、连接与初始化

 

objClass对象引用的哈希码改变了说明objClass变量两次引用了不同的Class对象

 JAVA类的加载、连接与初始化