17.Java类的生命周期 与 类加载器
1、简介
Java 虚拟机为 Java 程序提供运行时环境, 其中一项重要的任务就是管理类和对象的生命周期。类的生命周期从类被加载、连接、初始化开始,到类被卸载结束。当类处于生命周期中时,它的二进制数据位于方法区内,在方法区还会有一个相应的描述这个类的Class对象,只有当类处于生命周期中时,Java程序才能使用它,比如调用类的静态属性和方法,或者创建类的实例。ClassLoader只负责class文件的加载,而是否能够运行则由 Execution Engine 来决定。
2、一个类被加载到内存并供我们使用, 需要经历如下三个阶段:
- 加载: 查找并加载类的二进制数据(把类的 .class文件读取到内存中),把他存放到运行时数据区的方法区内。所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。加载时机:当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。
- 连接: 包括验证(确保被加载的类的正确性)、准备(为类的静态变量分配内存,并将其初始化, 并且如果必需的话,将常量池中的符号引用转化为直接引用。)和 解析类(把类中的 符号引用 转换成 直接引用)的二进制数据 。
- 初始化: 每个类或接口被 java 程序主动使用的时候才会初始化, 用于执行该类的静态初始器和静态初始块。如果类存在直接的父类,并且父类还没有被初始化,那就先初始化父类。
3、类的初始化时机
- 创建类的实例。
- 调用类的静态方法。
- 访问某个类或接口的静态变量, 或者对静态变量赋值。
- 调用Java API中某些反射方法,比如:Class.forName("Test")。 (调用ClassLoader类的loadClass()方法加载一个类不会导致初始化。)
- 初始化一个类的子类。
4、类加载器
类加载器用来把类加载到Java 虚拟机中。从JDK1.2开始,类的加载采用父亲委托机制,这种机制能更好的保证Java平台的安全。除了Java 虚拟机自带的根类加载器外(没有父加载器), 其余的类加载器都只有一个父类加载器,当Java 程序请求load1去加载Sample 类时, load1首先委托父加载器去加载,若父加载器能加载, 则由父加载器加载, 否则由load1本身去加载。
Java 虚拟机自带的几种类加载器:
- 根(Bootstrap)类加载器: 加载虚拟机的核心类库, 如 java.lang.*。 由虚拟机实现, 没有继承 java.lang.ClassLoader类。
- 扩展(Extension)类加载器: 它的父加载器是根类加载器。 它负责从: java.ext.dirs 系统属性所指定的类库或从 JDK安装的目录:jre\lib\ext子目录下加载类库。如果把用户创建的Jar文件放在这个目录下,也会自动有扩展类加载器加载。扩展类加载器是纯Java类加载器, 是java.lang.ClassLoader的子类。
- 系统(System)类加载器: 也称为应用类加载器, 它的父加载器是扩展类加载器。它从环境变量 classpass 或系统属性:java.class.path 所制定的目录中加载类。他是用户自定义类加载器的默认父加载器,系统类加载器是纯Java类, 是java.lang.ClassLoader的子类。
5、类的卸载
当 Sample类被加载、连接和初始化后, 它的生命周期就开始了,当代表Sample类的 Class 对象不再被引用时, 即不可触及, Class对象的生命周期就会结束。Sample类在方法区的数据也会被卸载。从而结束Sample 类的生命周期。前面介绍的 Java 虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中始终不会被卸载。Java 虚拟机会始终引用这些类加载器,而这些类加载会始终引用它们所加载的类的Class对象, 因此这些类始终是可及的。
6、java.lang.Class 类对象
每一个类都有一个Class对象,每当编译一个新类就产生一个Class对象(Class对象对应着java.lang.Class类),基本类型 (boolean, byte, char, short, int, long, float, and double)有Class对象,数组有Class对象,就连关键字void也有Class对象(void.class)。 每个类的实例运行时的类信息就是用Class对象表示的。它包含了与类有关的信息。其实我们的实例对象就通过Class对象来创建的。 Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。
一个类的实例总是引用代表这个类的 Class 对象,在 Object 类中定义了 getClass()方法, 这个方法返回代表对象所属类的Class对象的引用。此外所有的Java类都有一个静态属性 class, 它引用代表这个类的class对象。
6.2、获取Class对象的三种方式:
- 1、Class.forName(“类的全限定名”)。
- 2、实例对象.getClass()。
- 3、类名.class (类字面常量)。
但是在包装类中有个一个字段TYPE,TYPE字段是一个引用,指向对应的基本数据类型的Class对象,如下所示,左右两边相互等价:
int.class.equals(Integer.TYPE)
boolean.class.equals(Boolean.TYPE)
例如:
// c1 引用代表Study类的Class对象
Class c1 = Study.class;
// c2 引用代表Study类的Class对象
Class c2 = new Study().getClass()
// c3 引用代表Study类的Class对象
Class c3 = Class.forName("com.dw.study.Study")
c1= c2 = c3 = true
一旦类被加载了到了内存中,那么不论通过哪种方式获得该类的Class对象,它们返回的都是指向同一个java堆地址上的Class引用。jvm不会创建两个相同类型的Class对象。但是对于任意一个Class对象,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个Class对象来源于同一个Class文件,只要加载它们的类加载器不同,那这两个Class对象就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。所以在java虚拟机中使用双亲委派模型来组织类加载器之间的关系,来保证Class对象的唯一性。
6.3、获取Class对象的三种方式的区别:
类名.class 来创建对Class对象的引用时,不会自动地初始化该Class对象,但是如果使用Class.forName() 来产生引用,就会立即进行了初始化。如果是实例对象的引用(c2), Class 已经被初始化,则直接获取Class对象的引用。
测试如下:
public class A {
// 同时被static 与 final 修饰的成员在类被编译时,就已经被初始化,放入常量池
static final String s1 = "A_s1";
static String s2 = "A_s2";
static {
System.out.println("Loading A");
}
}
public class B {
static String s1 = "B_s1";
static {
System.out.println("Loading B");
}
}
public class ClassTest {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("-------Star A-------");
Class a = A.class;
System.out.println("------------");
System.out.println(A.s1);
System.out.println("------------");
System.out.println(A.s2);
System.out.println("------start B------");
Class cat = Class.forName("com.example.classReflec.B");
System.out.println("--------------------");
System.out.println(B.s1);
System.out.println("finish main");
}
}
打印结果如下:
如果一个字段同时被static和final修饰,我们称为”编译时常量“,就像A类中的A_s1字段那样,那么在调用这个字段的时候是不会对A类进行初始化的。因为被static和final修饰的字段,在编译期就把结果放入了常量池中了。但是,如果只是将一个域设置为 static 或 final 的,还不足以确保这种行为,就如调用B的B_s1字段后,会强制B进行类的初始化,因为B_s1字段不是一个编译时常量。
7、泛型Class引用
Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。在JavaSE5中,允许你对Class引用所指向的Class对象的类型进行限定,也就是说你可以对Class对象使用泛型语法。通过泛型语法,可以让编译器强制指向额外的类型检查:
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {}
**7.1、**虽然int.class和Integer.class指向的不是同一个Class对象引用,但是它们是基本类型和包装类的关系,int可以自动包装为Integer,所以编译器可以编译通过。
Class<Integer> c1 = int.class;
c1=Integer.class;
//c1=Double.class; 编译报错
**7.2、**泛型中的类型可以持有其子类的引用吗?不行:虽然Integer继承自Number,但是编译器无法编译通过。
Class<Number> c1 = Integer.class; //编译报错
**7.3、**为了使用泛化的Class引用放松限制,我们还可以使用通配符,它是Java泛型的一部分。通配符的符合是”?“,表示“任何事物“:
// ? 表示所有类型Class<?> c1 = int.class;
c1= double.class;
// 被限定为该类型,或者该类型的子类型
Class<? extends Number> c1 = Integer.class;
c1 = Number.class;
c1 = Double.class;
// c1=String.class; 报错,不属于Number类和其子类
**7.4、**通配符?不仅可以与extend结合,而且还可以与super关键字相结合,表示被限定为某种类型,或该类型的任何父类型:
Class<? super Integer> c1 = Integer.class;
c1 = Number.class;
c1 = Object.class;
c1=Integer.class.getSuperclass();
8、Class类的方法
getName()
:返回String形式的该类的名称。getSimpleName()
: 返回源代码中给出的底层类的简称getPackage()
: 返回该类的包,如果存档或基本代码中没有可用的包信息,则返回 null。forName()
: 返回与带有给定字符串名的类或接口相关联的 Class 对象。forName(String name, boolean initialize,ClassLoader loader)
: 上一个方法的重载方法,可以指定类名称、加载时是否运行静态区块、指定类加载器。newInstance()
:创建此 Class 对象所表示的类的一个新实例。如同用一个带有一个空参数列表的 new 表达式实例化该类。如果该类尚未初始化,则初始化这个类。getClassLoader()
:返回该Class对象对应的类的类加载器。getSuperClass()
:返回某子类所对应的直接父类所对应的Class对象。getModifiers()
: 获取修饰符,表示该类修饰符的 int值。可以通过Modifier.toString()转成字符串形式。getComponentType()
:如果当前类表示一个数组,则返回表示该数组组件的 Class 对象,否则返回 null。getConstructor(Class[])
:返回当前 Class 对象表示的类的指定的公有构造子对象。getConstructors()
:返回当前 Class 对象表示的类的所有公有构造子对象数组。getDeclaredConstructor(Class[])
:返回当前 Class 对象表示的类的指定已说明的一个构造子对象。getDeclaredConstructors()
:返回当前 Class 对象表示的类的所有已说明的构造子对象数组。getDeclaredField(String)
:返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段。name参数是一个String,它指定所需字段的简称。注意,此方法不反映数组类的 length 字段。getDeclaredFields()
:返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段。包括公共、保护、默认(包)访问和私有字段,但不包括继承的字段。getDeclaredMethod(String, Class[])
:返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。name 参数是一个 String,它指定所需方法的简称,parameterTypes 参数是 Class 对象的一个数组,它按声明顺序标识该方法的形参类型。如果在某个类中声明了带有相同参数类型的多个方法,并且其中有一个方法的返回类型比其他方法的返回类型都特殊,则返回该方法;否则将从中任选一个方法。getDeclaredMethods()
: 返回 Method 对象的一个数组,这些对象反映此 Class对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。返回数组中的元素没有排序,也没有任何特定的顺序。如果该类或接口不声明任何方法,或者此 Class 对象表示一个基本类型、一个数组类或 void,则此方法返回一个长度为 0 的数组。getField(String)
:返回当前 Class 对象表示的类或接口的指定的公有成员域对象。getFields()
:返回当前 Class 对象表示的类或接口的所有可访问的公有域对象数组。getInterfaces()
:返回当前对象表示的类或接口实现的接口。getMethod(String, Class[])
:返回当前 Class 对象表示的类或接口的指定的公有成员方法对象。getMethods()
:返回当前 Class 对象表示的类或接口的所有公有成员方法对象数组,包括已声明的和从父类继承的方法。isInstance(Object)
:此方法是 Java 语言 instanceof 操作的动态等价方法。isAssignableFrom()
: 确定此Class对象表示的类或接口是否与指定参数表示的类或接口相同,或者是该Class类或接口的超类或超接口。如果是A.isAssignableFrom(B) 确定一个类(B)是不是继承来自于另一个父类(A),一个类(A)是不是实现了另外一个接口(B),或者两个类相同。isAnnotation()
: 判定此Class对象所对应的是否是一个注释对象isArray()
:判定此Class对象所对应的是否是一个数组对象isInterface()
:判定指定的 Class 对象是否表示一个接口类型isPrimitive()
:判定指定的 Class 对象是否表示一个 Java 的基本类型。