类加载
在java代码中,类型的加载,连接与初始化的过程都是在程序运行期间完成的,类型可以理解为class。
类的加载、连接、初始化、使用、卸载
加载:查找并加载类的二进制数据,就是将类的class文件加载到内存中;将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象用来封装类在方法区内的数据结构
连接:就是将已经读入到内存中的二进制数据合并到虚拟机的运行时环境中去
验证:校验class文件是否正确
- 类文件的结构检查
- 语义检查
- 字节码验证
- 二进制兼容性的校验
准备:为类的静态变量分配空间,并初始化其默认值
解析:将类中的符号引用转换成直接引用,就是在类型的常量池中寻找类、接口、字段和方法的符号引用,然后把这些符号引用替换成直接引用的过程
初始化:为类的静态变量初始化其正确的值(即定义的值),会调用"cinit"
还有使用和卸载两个阶段
类的实例化:
- 为新的对象分配内存
- 为新的实例变量赋默认值
- 为实例变量赋正确的初始值
Java编译器为它编译的每一个类都至少生成一个实例初始化方法,在Java的class文件中,这个实例初始化方法被称为“<init>”。针对源代码中的每一个类的构造方法,Java编译器产生一个<init>方法
java程序对类的使用方式分为两种:
- 主动使用
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值getstatic、putstatic
- 调用某个类的静态方法 invokestatic
- 反射
- 初始化一个类的子类
- java虚拟机启动时被标注为启动类的类
- jdk1.7开始提供动态语言支持
- 被动使用
所有的java虚拟机实现必须在每个类或者接口被java程序“首次主动使用”时才初始化,被动使用不会导致初始化阶段发生;-XX:+TraceClassLoading用于追踪类加载信息
主动使用时,如果是final修饰的变量,会存入到调用这个常量方法所在类的常量池中,本质上并不是主动使用,如下str会存入MyTest的常量池中,之后MyTest便于MyParent没有任何关系;但是,该常量值必须是编译期能够确定,如果不能确定,则不会将其放在常量池中,则会主动使用定义该常量的类。
class MyParent{
public static final String str="hello";
static{
System.out.println("test.......");
}
}
public class MyTest{
public static void main(String args[]){
MyParent.str;
}
}
创建空数组的形式是被动使用,其类型有由JVM在运行期动态生成的
MyParent[] parents = new MyParent[1];
对于数组实例,其类型是由JVM在运行期动态生成的,并不是由类加载器加载的,如果数组类型中的元素是原生类型,则使用数组class获取ClassLoader是不存在的,如果不是原生类型的元素,则获取的类加载器与其元素的类加载器是一致的
String [] strings = new String[4];
System.out.println(strings.getClass().getClassLoader()); //输出null 表示是根类加载器
MyTest5 [] my = new MyTest5[3];
System.out.println(my.getClass().getClassLoader());//输出的是AppClassLoader ,表示是应用类加载器
int [] ints = new int[3];
System.out.println(ints.getClass().getClassLoader());//输出null ,这里表示的是没有类加载器
当一个接口在初始化时,并不要求其父接口都完成了初始化,只有在真正使用到父接口的时候(如引用接口中所定义的常量时),才会初始化,因为接口中的变量默认是 public static final修饰,即接口中的变量都是常量,如果是编译器能够确定的则会直接在编译期将常量放入调用该常量的类的常量池中。
JVM 参数:
- -XX:+ :表示打开这个选项
- -XX:- : 表示关闭这个选项
- -XX:= :表示将option选项的值设置为value
java虚拟机在以下几种情况下退出,结束生命周期:
- 程序中显示执行System.exit();
- 程序正常执行结束;
- 程序在执行过程中遇到异常或者错误而异常终止;
- 由于操作系统出现错误而导致java虚拟机进程终止;
加载:
链接:
验证
- 准备
- 解析:
解析过程就是在类型的常量池中寻找类、接口、字段、和方法的符号引用,把这些符号引用替换成直接引用的过程 - 初始化:
为类变量赋正确的初始值 - 类实例化:
为创建的对象分配内存;
为实例变量赋默认值;
为实例变量赋正确的初始值;
java编译器为它编译的每一个类都至少生成一个实例初始化方法,在java的class文件中,这个实例初始化方法被称为“”。针对源代码中的每一个类的构造方法,java编译器都产生一个方 法
类的初始化步骤:
- 假如这个类还没有被加载和链接,那就先进行加载和链接
- 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类
- 假如类中存在初始化语句,那就依次执行这些初始化语句
有两种类型的类加载器:
- java虚拟机自带的加载器:
- 根类加载器(Bootstrap)
- 扩展类加载器(Extension)
- 系统(应用)类加载器(System/Application)
- 用户自定义加载器:
是java.lang.ClassLoader的子类
用户可以定制类的加载方式
类加载器并不需要等到某个类被首次主动使用时再加载它
JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到.class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类是才报告错误
如果这个类一直没有被程序主动使用,则类加载器不会报错
当java虚拟机初始化一个类的时候,要求它所有的父类已经被初始化,但是这条规则不使用于接口:
- 在初始化一个类时,并不会先初始化它所实现的接口
- 在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化
下面例子:
interface Parent{
String name = "parent";
public static Thread t = new Thread() {
{
System.out.println("this is parent init");
}
};
}
class SubParent implements Parent{
public static String sex = "nan";
static {
System.out.println("subParent");
}
}
public class MyTest1 {
public static void main(String[] args) {
System.out.println(SubParent.sex);
}
}
上面如果SubParent 是接口的话,那么SubParent 和Parent都不会加载,更谈不上初始化,如果SubParent 是类,并且sex属性不是final,那么SubParent 和Parent 会被加载,但是Parent不会被初始化,如果sex是final修饰的,那么SubParent 和Parent既不会被加载和初始化
类加载器的双亲委派机制:
在双亲委派机制中,各个类加载器按照父子关系形成逻辑上的树形结构,在实现上并不是继承关系,而是包含关系,除了根类加载器外,其余的类加载器有且只有一个父加载器
各个类加载所加载的类可以通过系统路径改变:
- 根类加载器:System.getProperty(“sun.boot.class.path”)
- 扩展类加载器:System.getProperty(“java.ext.dirs”)
- 系统类加载器:System.getProperty(“java.class.path”)
获得ClassLoader的途径:
- 获取当前类的ClassLoader:clazz.getClassLoader()
- 获得当前线程上下文的Classloader:Thread.currentThread().getContextClassLoader()
- 获得系统的ClassLoader :ClassLoader.getSystemClassLoader();
- 获得调用者的ClassLoader:DriverManager.getCallClassLoader()
自定义ClassLoader
- 继承ClassLoader
- 需要重写findClass方法,此方法是实际加载类的具体方法,调用defineClass方法生成Class,将字节数字转换成Class对象,
同时,自定义ClassLoader需要提供一个参数是ClassLoader的构造器,在ClassLoader的源码中定义了相关的介绍,自定义ClassLoader会调用ClassLoader中的构造方法,而构造方法会调用getSystemClassLoader()方法,获取系统类加载器,在getSystemClassLoader中的initSystemClassLoader()中做相关操作:
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
class SystemClassLoaderAction
implements PrivilegedExceptionAction<ClassLoader> {
private ClassLoader parent;
SystemClassLoaderAction(ClassLoader parent) {
this.parent = parent;
}
//这里是上面方法中调用时所做的操作,查看用户是否设置java.system.class.loader属性,
//如果没有设置,则直接返回系统类加载器,如果设置,
//则使用反射调用参数是ClassLoader 的构造器,将系统类加载器作为其父加载器,
//初始化的类加载器作为AppClassLoader,同时设置当前线程的上下文类加载器
public ClassLoader run() throws Exception {
String cls = System.getProperty("java.system.class.loader");
if (cls == null) {
return parent;
}
Constructor<?> ctor = Class.forName(cls, true, parent)
.getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
ClassLoader sys = (ClassLoader) ctor.newInstance(
new Object[] { parent });
Thread.currentThread().setContextClassLoader(sys);
return sys;
}
类加载器命名空间:
每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载类组成
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
同一个命名空间内的类是相互可见的。
子加载器的命名空间包含所有父加载器的命名空间,因此由子加载器加载的类能够看见父加载器加载的类,例如系统类加载加载的类可以看见根类加载器加载的类。由父加载器加载的类不能看见子加载器加载的类。如果两个两个加载器之间不存在直接或者间接的父子关系,那么它们各自加载的类是相互不可见的
MyTest6 loader1 = new MyTest6("loader1");
MyTest6 loader2 = new MyTest6("loader2");
Class<?> clazz1 = loader1.loadClass("demo.MyPerson");
Class<?> clazz2 = loader2.loadClass("demo.MyPerson");
System.out.println(clazz1 == clazz2);
Object obj1 = clazz1.newInstance();
Object obj2 = clazz2.newInstance();
Method method = clazz1.getMethod("setMyPerson", Object.class);
Object obj = method.invoke(obj1, obj2);
上面示例是使用自定义ClassLoader加载类,程序会在最后一行报错,两个不同的加载器,不存在父子关系,所以它们所加载的类是相互不可见的。
在运行期,一个Java类是由该类的完全限定名(binary name,二进制名)和用于加载该类的定义类加载器(defining loader)所共同决定的,如果同样的名字(即相同的完全限定名)的类是由两个不同的加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载亦如此
类加载器的双亲委派模型的好处:
1、可以确保java核心库的类型安全:例如所有的java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到java虚拟机中;如果这个加载过程是由Java应用自己的类加载器所完成的,那么很有可能就会在jvm中存在多个版本的Java.lang.Object类,而且这些类之间还是不兼容的,相互不可见的(正是命名空间发挥着作用)。
借助于双亲委派机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间是相互兼容的。
2、可以确保Java核心类库所提供的类不会被自定义的类所替代
3、不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用
扩展类加载器比较特殊:它加载的类的时候必须从jar包中加载,不能直接从目录下获取.class文件加载
启动类加载器,即根加载器,是c++编写的,是内嵌于Java虚拟机中的,会在JVM启动时运行,同时会加载java.lang.ClassLoader 和其他Java平台类,也会加载扩展类加载器和系统类加载器
类的卸载:
当一个类被加载、链接和初始化之后,它的生命周期就开始了。当代表某一个类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,某一个类在方法区内的数据也会被卸载,从而结束某一个类的生命周期。
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
由java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载(根类加载器、扩展类加载器、系统类加载器),虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的Class对象,因此这些Class对象时可触及的
由用户自定义的类加载器所加载的类是可以被卸载的
使用-XX:+TraceClassUnLoading命令可以追踪JVM卸载类的过程