首页 > 编程语言 > 详细

Java类的加载、连接和初始化

时间:2020-11-05 17:50:17      阅读:29      评论:0      收藏:0      [点我收藏+]

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

1、JVM和类

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

  • 程序运行到最后正常结束。
  • 程序运行到使用System.exit()或Runtime.getRuntime().exit()代码处结束程序。
  • 程序执行过程中遇到未捕获的异常或错误而结束。
  • 程序所在平台强制结束了JVM进程。

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

public class A{

public static int a = 6;

}

 

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

public class Test1{

public static void main(String[] args){

A a = new A();

a.a++;

System.out.println(a.a);

}

}

 

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

public class Test2{

public static void main(String[] args){

A a = new A();

System.out.println(a.a);

}

}

 

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

 

2、类的生命周期

  类的生命周期如图所示:

 

 

 

                     技术分享图片

 

 

 

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 class Test{

public static int a = 5;

public static int b;

public static int c;

public static{

b = 6;

}

}

 

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

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

public class Test{

static{

b = 6;

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

}

static int a = 5;

static int b= 9;    //1

static int c;

public static void main(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 class CompileConstrant{


public static void main(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()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用ClassforName()静态方法才会导致强制初始化该类。例如如下代码。

 

class Tester{


static{


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


}


}


public class ClassLoaderTest{


public static void main(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中表示为(Personpgkl)。这意味着两个类加载器加载的同名类:(Personpgkl)和(Personpgkl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

 

  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自身实现的。下面程序可以获得根类加载器所加载的核心类库。

package com.langsin.test;

import java.net.URL;

public class Test {

public static void main(String[] args) {

URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();

for(URL url : urls){

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

}

}

}

 

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

   技术分享图片

  看到这个运行结果,就应该明白为什么程序中可以使用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()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。

 

Java类的加载、连接和初始化

原文:https://www.cnblogs.com/javaandpython/p/13932946.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!