类加载
jvm的类加载,可以分为以下3个步骤
加载
加载,是指jvm借助类加载器查找字节流,并且据此创建类的过程。对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的。
系统提供的类加载器有
-
启动类加载器
负责加载/lib/目录中的类库(如rt.jar),或者-Xbootclasspath参数指定的路径的类库。启动类加载器不能被Java程序直接引用,如果自定义的类加载器需要把加载请求委托给启动类加载器,那直接返回null引用替代即可。 -
扩展类加载器
负责加载/lib/ext目录中的类库,或者被java.ext.dirs系统变量指定的路径的类库。 -
应用程序类加载器
负责加载用户类路径(即Classpath)指定的类库,如果应用程序没有自定义类加载器,这个就是程序默认的类加载器。
双亲委派模型
双亲委派过程:如果一个类加载器收到加载类的请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父加载器去完成。每一个层次的类加载都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中。只有当父加载器反馈无法完成加载请求时(搜索不到这个类),子加载器才会尝试自己去加载。
双亲委派的实现逻辑在java.lang.Classloader(jdk1.8.0)的loadClass方法之中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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;
}
}
总结
- 除了启动类加载器,
- 类的唯一性是由类加载器实例以及类的全限定类名一同确定的(Class类的equals、isAssigableFrom、isInstance方法的返回结果,以及instanceof关键字所做的对象所属关系判断)。
链接
链接可分为验证、准备和解析3个阶段。
- 验证阶段的目的,在于确保被加载类能够满足 Java 虚拟机的约束条件。包括:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
- 准备阶段为类变量分配内存并设置类变量初始值(注意不是实例变量,这里的初始值是指类型的默认值如int的默认值为0)
- 解析阶段是虚拟机将常量池内的[符号引用]替换(#jump1)为直接引用的过程。
初始化
初始化阶段是执行类构造器<clinit>()(不是实例构造器)的过程。<clinit>()是编译器自动收集类中所有的赋值语句和静态语句块(static{}块)合并产生的(按照在java文件中出现的顺序)。
初始化时机
以下几种情况,如果类没有初始化,则需要触发其初始化。
- new、getstatic、putstatic或者invokestatic这4个字节码指令
- 对类进行反射调用的时候
- 当初始化一个类的时候,如果其父类没有初始化,则需要先触发其父类的初始化。
- 虚拟机启动时用户指定要执行的主类
- 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例解析的结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄时,并且这个方法句柄对应的类没有初始化,则需要触发其初始化。
总结
- 虚拟机确保一个类的<clinit>()方法是线程安全的(线程安全的单例实现)。
- 访问类或者接口的静态常量(final修饰且编译期放进了常量池的字段)不会触发类的初始化
一些定义
符合引用: 以一组符合来描述所引用的目标。可以是任何形式的字面量,与jvm的内存布局无关,引用的目标不一定被加载到内存中。
直接引用: 可以是直接指向目标的指针、相对偏移量或者一个能够间接定位到目标的句柄。直接引用的目标一定存在jvm内存中。
实例的初始化
(需要继续完善补充)
在Java程序中类可以被明确或者隐含地实例化,有四种途径:明确使用new操作符;调用Class或者Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;或者通过objectInputStream类的getObject()方法反序列化。
当虚拟机创建一个新的实例时,都需要在堆中为保存对象的实例分配内存。所有在对象的类中和它的超类中声明的变量(包括隐藏的实例变量)都要分配内存。一旦虚拟机为新的对象准备好堆内存,它立即把实例变量初始化为默认的初始值。这一点很类似于类变量在链接的准备阶段赋予默认初始值是一样样的。
一旦虚拟机完成了为新的对象分配内存和为实例变量初始化为默赋予正确认的初始值后,接下来就会为实例变量的初始值。即调用对象的实例初始化方法,在java的class文件中称之为< init >()方法,类似于类初始化的< clinit >()方法。
一个< init >()方法可能包含三种代码:
调用另一个< init >()方法
实现对任何实例变量的初始化
构造方法体的代码