byte 初始化 java_Java类的加载、连接和初始化

最近在复习Java反射,需要看一下类的加载机制这部分,正好给大家整理总结一下。

1、JVM和类

当我们调用Java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都会处于该Java虚拟机进程里。正如前面介绍的,同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当系统出现以下几个情况时,JVM进程将被终止。

程序运行到最后正常结束。

程序运行到使用System.exit()或Runtime.getRuntime().exit()代码处结束程序。

程序执行过程中遇到未捕获的异常或错误而结束。

程序所在平台强制结束了JVM进程。

从上面的介绍可以看出,当Java程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失。下面以类的静态变量来说明这个问题。下面程序先定义一个包含静态变量的类。

public classA{public static int a = 6;

}

上面程序中的粗体代码定义了一个类变量,接下来定义一个类来创建A类的实例对象,并访问A对象的a变量。

public classTest1{public static voidmain(String[] args){

A a= newA();

a.a++;

System.out.println(a.a);

}

}

下面程序也创建A类的实例对象,并访问其a变量的值。

public classTest2{public static voidmain(String[] args){

A a= newA();

System.out.println(a.a);

}

}

在Test1.java程序中创建A类的实例对象,并让该实例的a变量值自加,程序输出该实例的a的值为7,关键是运行第二个程序Test2时,程序再次创建A对象,并输出A对象的a变量值,结果为6。这是因为运行Test1和Test2是两次运行JVM进程,第一次运行JVM结束后,它对A类所做的修改将全部丢失——第二次运行JVM时将再次初始化A类。

2、类的生命周期

类的生命周期如图所示:

4910b5a0ba9b97f2d3afc1ce5c665821.png

3、类的加载

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

类加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。(JVM相关知识等我看完《深入理解Java虚拟机》再写,这些现在也是没怎么理解)

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。

从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。

通过网络加载class文件。

把一个Java源文件动态编译,并执行加载。

类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

4、类的连接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

1、 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。

2、 准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

3、 解析:将类的二进制数据中的符号引用替换成直接引用。

5、类的初始化

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对静态Field进行初始化。在Java类中对静态变量指定初始值有两种方式:1、声明静态变量时指定初始值。2、使用静态初始化块为静态变量指定初始值。如下所示:

public classTest{public static int a = 5;public static intb;public static intc;public static{

b= 6;

}

}

对于上面代码,程序为静态变量a、b都显示指定了初始值,所以这两个静态变量的值分别为5、6,但静态变量c则没有指定初始值,它将采用默认初始值0。

声明变量时指定初始值以及在静态初始化块中赋值,都将被当成类的初始化语句,JVM会按这些语句在程序中的排列顺序依次执行它们,例如下面的类。

public classTest{static{

b= 6;

System.out.println(“-----------------------------------”);

}static int a = 5;static int b= 9; //1

static intc;public static voidmain(String[] args){

System.out.println(Test.b);

}

}

上面代码先在静态初始化块中为b变量赋值,此时静态变量b的值为6;接着程序向下执行,执行到1处代码处,这行代码也属于类的初始化语句,所以程序再次为变量b赋值,也就是说Test类初始化代码结束后,该类的静态变量b的值为9。

JVM初始化一个类包含如下几个步骤。

1、 假设这个类还没有被加载和连接,则程序先加载并连接该类。

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

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

当执行第2个步骤时,系统对直接父类的初始化步骤也遵循此步骤1-3;如果该直接父类又有直接父类,则熊再次重复这3个步骤来先初始化这个父类。。。依次类推,所以JVM最先初始化的总是java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及所有父类都会被初始化。

6、类初始化的时机

当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口。

创建类的实例对象。为某个类创建实例对象的方式包括:使用new操作符来创建实例对象,通过反射来创建实例对象,通过反序列化的方式来创建对象。

调用某个类的静态方法。

访问某个类或接口的静态变量,或为该静态变量赋值。

使用反射方式来强制创建某各类或接口对应的java.lang.Class对象。例如代码:Class.forName(“Person”),如果系统还未初始化Person类,则这行代码将会导致该Person类被初始化,并返回Person类对应的java.lang.Class对象。

创建某个子类的实例对象。当初始化某个类的子类的实例对象时,该子类的所有父类都会被初始化。

直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会先初始化该类。(这个我不是很懂,有大佬比较懂的话请评论区指点)

除此之外,下面几种情形需要特别指出。

对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。例如下面的程序的结果:

Class MyTest{static{

System.out.println(“静态初始化块。。。。。。”);

}static final String compileConstrant =“Java讲义”;

}public classCompileConstrant{public static voidmain(String[] args){

System.out.println(MyTest.compileConstrant);//1

}

}

上面程序的MyTest类中有一个compileConstrant的静态变量,该变量使用final修饰,而且它的值可以在编译时确定下来,因此compileConstrant会被当成“宏变量”处理。程序中所有使用compileConstrant的地方都会在编译时被直接替换成它的值——也就是说,上面程序1处的粗体代码在编译时就会被替换成“Java讲义”,所以1处代码不可能初始化MyTest类。

反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。例如,我们把上面程序中定义compileConstrant的代码改为如下:

static final String compileConstrant = System.currentTimeMillis()+””;

因为上面定义的compileConstrant静态变量值必须在运行时才可以确定,所以1处的粗体代码必须保留对MyTest类中静态变量的引用,这行代码就变成了MyTest的静态变量而不是“宏变量”,这将导致MyTest类被初始化。

当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class的forName()静态方法才会导致强制初始化该类。例如如下代码。

classTester{static{

System.out.println(“Tester类的静态初始化块。。。。”);

}

}public classClassLoaderTest{public static voidmain(String[] args){

ClassLoader cl=ClassLoader.getSystemClassLoader();

cl.loadClass(“Tester”);

System.out.println(“系统加载Tester类。。。。”);//如果系统没有加载该类,则首先加载该类并随后完成该类的初始化

Class.forName(“Tester”);

}

}

上面程序中的两行粗体字代码都用到了Tester类,但第一行粗体字代码只是加载Tester类,并不会初始化Tester类,运行上面程序会看到如下运行结果:

系统加载Tester类。。。。。。

Tester类的静态初始化块。。。。。。

7、类加载器

类加载器负责将.class文件(可能在磁盘上,可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。(虽然开发中不用咱写)

类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。现在的问题是,怎么样才算“同一个类”?

正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person、pg、kl)。这意味着两个类加载器加载的同名类:(Person、pg、kl)和(Person、pg、kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader 。

扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。

除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader 类的方式实现自己的类加载器,以满足一些特殊的需求。

Bootstrap ClassLoader被称为引导类加载器,它负责加载Java的核心类。在Sun的JVM中当执行java.exe命令时,使用-Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值可以指定加载附加的类。

根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。下面程序可以获得根类加载器所加载的核心类库。

packagecom.langsin.test;importjava.net.URL;public classTest {public static voidmain(String[] args) {

URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();for(URL url : urls){

System.out.println(url.toExternalForm());

}

}

}

运行上面程序,会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径:

623574666890ed04b29604ba66c0de61.png

看到这个运行结果,就应该明白为什么程序中可以使用String、System这些核心类库——因为这些核心类库都在java/jdk/jre/lib/rt.jar文件中。

Extension ClassLoader被称为扩展类加载器,它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。通过这种方式,就可以为Java来扩展核心类以外的新功能,只要我们把自己开发的类包打成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径下即可。

System ClassLoader被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。

8、类加载机制

JVM的类加载机制主要有如下3种。

全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

父类委托:所谓的父类委托,则是先让parent类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

除了可以使用Java提供的类加载器之外,开发者也可以实现自己的类加载器,自定义的类加载器通过继承ClassLoader来实现。JVM中这4种类加载器的层次结构如图下图所示:

下面程序示范了访问JVM的类加载器:

package com.langsin.test;

import java.io.IOException;

import java.net.URL;

import java.util.Enumeration;public classTest {public static voidmain(String[] args) throws IOException {//获取系统类加载器

ClassLoader systemLoader=ClassLoader.getSystemClassLoader();

System.out.println("系统类加载器:"+systemLoader);/** 获取系统类加载器的加载路径,通常由系统环境变量CLASSPATH指定,

* 如果操作系统没有指定CLASSPATH环境变量,则默认以当前路径作为

* 系统类加载器的加载路径*/Enumeration eml = systemLoader.getResources("");while(eml.hasMoreElements()){

URL url=eml.nextElement();

System.out.println(url);

}//获取系统类加载器的父类加载器,得到扩展类加载器

ClassLoader extensionLoader=systemLoader.getParent();

System.out.println("扩展类加载器:"+extensionLoader);

System.out.println("扩展类加载器的加载路径:"+System.getProperty("java.ext.dirs"));

System.out.println("扩展类加载器的parent:"+extensionLoader.getParent());

}

}

运行上面程序看到如下结果:

28fb272c080e5e5242c685b9e2ff3e35.png

从上面运行结果可以看出,系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是C盘自己安装的JDK中的ext,扩展类加载器的父加载器是null,并不是根类加载器。这是因为根类加载器并没有继承ClassLoader抽象类,所以扩展类加载器的getParent()方法返回的是null。但是实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器并不是Java实现的。

系统类加载器是AppClassLoader的实例,扩展类加载器是ExtClassLoader的实例,这两个类都是URLClassLoader的实例。

类加载器加载Class大致要经过如下8个步骤:

1、 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。

2、 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。

3、 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。

4、 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。

5、 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。

6、 从文件中载入Class,成功后跳至第8步。

7、 抛出ClassNotFountException异常。

8、 返回对应的java.lang.Class对象。

其中,第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass()方法来实现自己的载入过程。

9、类的卸载

当类被加载、连接和初始化后,它的生命周期就开始了。

当类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载,从而结束类的生命周期。

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

引用关系

加载器和Class对象:

在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。

另一方面,一个Class对象总是会引用它的类加载器。调用Class对象的getClassLoader()方法,就能获得它的类加载器。

由此可见,Class实例和加载它的加载器之间为双向关联关系。

类、类的Class对象、类的实例对象:

一个类的实例总是引用代表这个类的Class对象。

在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。

此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。

类的卸载

由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。

前面介绍过,Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。

Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

由用户自定义的类加载器加载的类是可以被卸载的。

想象一个这样的场景:

b038fda3f3cf11ad13800c43cfad5c4d.png

loader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接引用它。

如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。

当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)。

【总结】

(1) 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范);

(2) 被系统类加载器和标准扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者标准扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小。(当然,在虚拟机快退出的时候可以,因为不管ClassLoader实例或者Class(java.lang.Class)实例也都是在堆中存在,同样遵循垃圾收集的规则);

(3) 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到.可以预想,稍微复杂点的应用场景中(尤其很多时候,用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)

综合以上三点, 一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下来实现系统中的特定功能。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值