最近在复习Java反射,需要看一下类的加载机制这部分,正好给大家整理总结一下。
1、JVM和类
当我们调用Java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都会处于该Java虚拟机进程里。正如前面介绍的,同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当系统出现以下几个情况时,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。这是因为运行Test1和Test2是两次运行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虚拟机》再写,这些现在也是没怎么理解)
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
类加载器通常无须等到“首次使用”该类时才加载该类,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都显示指定了初始值,所以这两个静态变量的值分别为5、6,但静态变量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种方式来使用某个类或接口时,系统就会初始化该类或接口。
除此之外,下面几种情形需要特别指出。
对于一个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()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class的forName()静态方法才会导致强制初始化该类。例如如下代码。
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中表示为(Person、pg、kl)。这意味着两个类加载器加载的同名类:(Person、pg、kl)和(Person、pg、kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
java.lang.ClassLoader
。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()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。
原文:https://www.cnblogs.com/javaandpython/p/13932946.html