1.类加载过程:加载、验证、准备、解析、初始化
虚拟机的类加载机制:把描述类的数据从Class文件(或者其他途径)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
加载:
1.通过一个类的全限定名获取定义此类的二进制字节流
2.将这个字节流所代表的静态结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象(并没有明确规定是在java堆中,对于HotSpot虚拟机来说,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),作为对方法区中这些数据的访问入口。
验证:
1.文件格式验证,保证输入的字节流在格式上符合Class文件的格式规范,保证输入的字节流能正确的解析,只有通过这个验证,字节流才会存储在方法区之内;
2.元数据验证,对类的元数据进行语义校验,保证类描述的信息符合Java语言规范。比如验证类的是否实现了父类或者接口中的方法等;
3.字节码验证,通过数据流和控制流的分析,确保类的方法符合逻辑,不会在运行时对虚拟机产生危害;
4.符号引用验证,发生在解析阶段,确保解析阶段将符号引用转化为直接引用的正常执行。符号引用可以理解为类自身以外的信息进行校验匹配。
符号引用(Symbolic References):即用一组符号来描述所引用的目标。它与虚拟机的内存布局无关,引用的目标不一定已经加载到内存中。
直接引用(Direct References):直接引用可以是指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。它是和虚拟机内存布局相关的,如果有了直接引用,那引用的目标必定已经在内存中存在了。
准备:正式为类变量(static)分配内存,并设置类变量初始值(数据类型的零值),这些变量所使用的内存在方法区中分配。
解析:虚拟机将常量池内的符号引用转化为直接引用,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化:
初始化阶段才真正执行类中定义的Java代码.在准备阶段,我们已经给变量赋过一次系统要求的初始值(零值),而在初始化阶段,则会根据程序员的意愿给类变量和其他资源赋值。
初始化阶段是执行类构造器方法的过程。方法(类构造器)是由编译器自动收集类中的类变量的赋值动作和静态语句块中的语句合并产生的。
public class Test {
static{
i = 0;//可以给变量赋值,编译通过
System.out.println(i);//编译不通过!!不能进行访问后面的静态变量
}
static int i =1;
}
子类和父类的初始化过程优先级为:父类类构造器->子类类构造器->父类对象构造函数->子类对象构造函数。类中静态类变量和静态代码块是按照在类中定义的顺序执行的。
2.什么时候进行类的初始化?
JVM规定了有且仅有5中情况——对类进行主动引用,必须立即执行类的初始化。
1)遇到new,putstatic,getstatic,invokespecial四条字节码指令的时候,如果没有进行类的初始化要立即初始化。这四条字节码指令对应的编程中的环境为:使用new关键字实例化对象,读取或设置类的静态变量,调用类的静态方法。
2)使用java.lang.reflect包对类进行反射的时候,如果没有初始化要立即初始化。
3)初始化一个类的时候,如果其父类没有进行初始化要先出发父类的初始化
4)虚拟机启动的时候,main方法所在的主类会被虚拟机先初始化
5)使用动态语言在lava.lang.invoke.MethodHandle实例最后的解析结果是REF_getdtatic,REF_putStatic,REF_invokeStatic的方法句柄,这个句柄对应的类没有被初始化需要先触发其初始化。
虚拟机规范中指明:有且只有 以上行为才会初始化,称为主动引用。除此之外的任何引用类的方法,都不会触发初始化,称之为被动引用。其实根据以上五条规则,就可以知道类的加载顺序了。
被动引用的几个例子:
(1)对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。至于是否要出发子类的加载、验证需要看具体虚拟机实现;
(2)通过数组定义来引用类,不会触发此类的初始化。如 A[] ints = new A[10] , 不会触发A 类的初始化。而是会触发名为 LA的类初始化。它是一个由虚拟机自动生成的、直接继承于Object 的子类,创建动作由字节码指令 newarray 触发。这个类代表了一个元素类型为 A 的一位数组,数组中的属性和方法都实现在这个类中。Java 语言中数组的访问比C/C++ 安全是因为这个类封装了数组元素的访问方法。
3.类加载器
任意一个类,都需要由加载它的类加载器和这个类本身共同确定其在Java 虚拟机中的唯一性
这里的“相等”,包括代表类的 Class 对象的equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括 instanceof 关键字对对象所属关系判定等情况。下面代码演示了不同类加载器对 instanceof 关键字运算的结果的影响。
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class> loadClass(String name)
throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1)
+ ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Class c = myLoader.loadClass("org.bupt.xiaoye.blog.ClassLoaderTest");
Object obj = c.newInstance(); //newInstance方法创建对象是用类加载机制,查看https://www.cnblogs.com/liuyanmin/p/5146557.html。
System.out.println(obj.getClass()); //java有两个获得类名的方法:实例.getClass()和类名.class();前者在运行时候确定,后者在编译时候确定
System.out.println(ClassLoaderTest.class);
System.out.println(obj instanceof ClassLoaderTest);
}
}
这就是因为虚拟机中存在了两个ClassLoaderTest类,一个由系统应用程序类加载器加载,一个由我们自定义的类加载器加载,虽然是 来自同一个Class文件,但依然是两个独立的类。
4.双亲委派模型:
从虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 语言实现, 是虚拟机自身的一部分:另一种就是所有其它的类加载器, 这些类加载器用Java 语言实现,独立于虚拟机外部,并且全都继承与抽象类 java.lang.ClassLoader。
(1)启动类加载器(Bootstrap ClassLoader) : 这个类加载器负责将存放在
(2)扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader 实现,它负责加载
(3)应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher$AppClassLoader 实现。这个这个类加载器是 ClassLoader 中的getSystemClassLoader() 方法的返回值,所以一般称它为系统类加载器。它负责加载用户路径(ClassPath)上所指定的类库,开发者可以使用这个类加载器,如果应用程序没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
类加载器的双亲委派模型是指从顶层到底层分别是启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。类加载器之间的父子关系不是通过继承来实现,而是通过组合来实现代码复用。双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,首先把这个请求委派给父类加载器去完成,所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类反馈自己无法完成类加载请求的时候,自加载器才会尝试自己去加载。
使用双亲委派模型的好处:java类随着他的加载器一起具备了带有优先级的层次结构,最基础的类由顶层的类加载器加载,这样保证在程序中使用该类的地方使用的都是这同一个类。
比如对于类Object来说,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器去加载,因此Object类在程序中的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类自己去加载的话,按照我们前面说的,如果用户自己编写了一个Object类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,此时Java类型提醒中最基础的行为也就无法保证了,应用程序也将变得混乱。
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先检查类是否已经被加载过
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;
}
}