类加载顺序
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
验证、准备、解析,统称为连接。
加载、验证、准备、初始化和卸载是顺序是确定的。解析阶段不一定。
出现以下5种情况需要立即“初始化”
- 遇到new、getstatic、putstatic、invokestatic这四条指令。java场景是(1)new、(2)读取或者设置一个类的静态字段(final,已在编译器把结果放入常量池的静态字段除外)、(3)调用一个类的静态方法
- 使用反射调用,类没有初始化的时候。
- 初始化一个类,发现父类没有初始化,则先初始化父类
- 虚拟机启动,执行的主类
- 如果一个java.lang.invoke.MethodHandle实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且对应的类没有初始化
例一:
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
//读取或设置类的静态字段,会触发该类的初始化
public static int value = 123;
}
public class SubClass extends SuperClass {
static{
System.out.println("SubClass init!");
}
}
public class ClassLoadTest {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
运行结果:“SuperClass init!”,不输出”SubClass init!”。
原因:对于静态字段,只有直接定义他的类才会初始化,如果需要同时加载子类通过-XX:+TraceClassLoading来修改
例二:
public class ClassLoadTest {
public static void main(String[] args) {
SuperClass[] sca=new SuperClass[10];
}
}
运行结果:没有输出“SuperClass init!”,说明没有触发,com.SubClass的初始化,但那是触发了一个名为Lcom.SubClasss的类的初始化。这个是由虚拟机自动生成的,直接继承Object的子类,创建动作用指令newarray触发 ,这个类代表了com.SubClass的一维数组。
例三:
public class ConstClass {
static{
system.out.println("ConstClass init!");
}
public static final String HELLOWORLD="hello world";
}
public class NotInitialization{
public static void main(String[] args) {
system.out.println(ConstClass.HELLOWORLD);
}
}
运行结果:没有输出“ConstClass init”。
原因:在编译阶段通过常量优化,已经将此常量值存入NotInitialization类的常量中,以后对ConstClass.HELLOWORLD的调用都会转化为NotInitialization对自身常量的引用。实际他俩在编译成class 的时候已经不存在任何关联了。
类加载过程
- 加载
需要完成的3个事情
(1)通过一个类的全限定名来获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)内存中生成这个类的Class对象,作为方法区这个类的各种数据的访问入口。
非数组的类是开发人员可控性最强的,因为加载阶段可以使用系统提供的引导类去控制,也可以使用用户自定义的类去加载。
对于数组类,由于数组类本身不通过类加载器创建,它是由Java虚拟机直接创建。 - 验证
确保Class文件的字节流中包含的信息符合虚拟机的要求,并不会危害虚拟机自身的安全。
(1)文件格式验证:魔数oxCAFEBABE开头、主次版本号、常量池类型是否支持、、、
(2)元数据验证(语义分析):是否有父类(除了Object都有父类)、是否继承了不被允许继承的类。。。
(3)字节码验证
(4)符号引用验证 - 准备
正式为类变量分配内存并设置类变量初始值。变量使用的内存都在方法区分配。仅分配static的类变量,不包括实例变量。通常为0
public static int value=123;
准备阶段后的初始值为0,赋值123的putstatic指令是程序被编译后,存放在类构造器<clinit>
()方法中的,所以在类初始化阶段才会执行
public static final int value=123;
final的会直接赋值为123
- 解析
虚拟机将常量池内的符号引用替换为直接引用的过程。 - 初始化
是执行类构造器<clinit>
()方法的过程。
<clinit>
()方法是由编译器自动收集类中拿到所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,顺序由代码顺序决定。静态语句块可以访问定义在静态语句块之后的变量,可以复制,但是不能访问
public class Test{
static{
i=0; //可以赋值
System.out.print(i); //不可以访问,提示“非法向前引用”
}
static int i=1;
}
<clinit>
()方法与类的构造函数不同,它不需要显示的调用父类构造器,虚拟机保证先执行父类的<clinit>
()已经执行完毕,因此第一个执行的<clinit>
()方法的类为Object
父类的<clinit>
()先执行,则父类中拿到静态语句块要优先于子类的变量赋值操作。
这里写代码片
类加载器
用于实现类的加载动作。
类与类加载器
对于任何一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。
两个类是否相等 1.同一个Class文件,2.同一个类加载器加载3.同一个虚拟机
Object obj=myLoader.loadClass("com.Test").newInstance();
System.out.println(obj.getClass);
System.out.println(obj instanceOf com.Test);
结果:false 由于他们不是同一类加载器加载的,所以返回false;
双亲委派模型
两种类加载器:
(1)启动类加载器(C++实现,是虚拟机自身的一部分)
负责将存放在<Java_Home>\lib
目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载的内存中,启动类加载器无法被Java程序直接引用。
(2)由Java语言实现的独立于虚拟机外部,全都继承java.lang.ClassLoader
扩展类加载器:这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext
目录中的,或者被java.ext.dirs系统变量所指定的路径中的类库,开发者可以直接扩展类加载器
应用程序类加载器:由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径上的所有指定类库。
双亲委派过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是交由父类,因此所有的加载请求最终都应该传送到顶层的启动类加载器,只有当父加载器无法加载的时候,才由子类去尝试加载。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
原理:先检查是否已经加载,如果没有加载则调用父加载器的loadClass(),如果父类加载器为空,则使用启动类加载器作为父加载器,如果父加载器加载失败,则调用自身的findClass方法加载