jvm学习笔记(四)虚拟机类加载机制、类加载器

一、类加载时机
1、流程: 加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
2、解析一般发生在初始化前,也可以发生在初始化之后,但是其他步骤是不可变的。
3、加载一定发生在初始化之前,而以下情况必须立即初始化,在 初始化之前自然必定先完成加载
  1) 遇到new、getstatic、putstatic、invokestatic这4个字节码指令时,如果类没有进行过初始化,
    需要先触发其初始化

而以上4个字节码指令触发场景:new关键字实例化对象、读取/设置一个类的静态字段(被final修饰已在编译器
把结果放入常量池的静态字段除外)、调用一个类的静态方法时。
  2)使用java.lang.reflect包的方法对类 反射调用时,如果类没有初始化需要先触发其初始化。
  3)初始化一个类时,如果发现其父类还没初始化,需要先触发其 父类初始化。
  4)虚拟机启动时,指定的 入口函数所在的类(main()方法所在类),虚拟机先初始化这个类。
  5)JDK1.7 的动态语言支持,java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、
    REF_putStatic、REF_invokeStatic的方法句柄,且该方法句柄所对应的类没有初始化,先触发其初始化。[不知道说的什么鬼]
二、类加载过程
1、加载
1)通过类全名获取定义该类的二进制字节流
2)将字节流所代表的的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个该类的java.lang.Class对象,作为方法区该类的数据访问入口
非数组类:可以用系统提供的类加载器完成、也可以自定义类加载器完成
数组类:
a:数组类本身由Java虚拟机直接创建,但是数组类的元素是由类加载器创建的。
b:数组元素是引用类型,在加载数组元素的类加载器的类名称空间上标识该数组(类加载器与类共同确定唯一性)
c:数组元素是基本类型,虚拟机会把数组标记为与引导类加载器(系统通过的类加载器)关联
d:数组类的可见性与元素类型的可见性一致,如果元素是基本类型,那么数组类的可见性默认为public
加载完成后,类的二进制流按照虚拟机所需格式存储在方法区中,并在内存中实例化一个java.lang.Class类的对象,
虽然它是对象,但是存放在方法区里。
2、验证
1)文件格式验证(魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围、常量池常量中是否有不被支持的类型、指向常量的索引值中是否有执行不存在/不符合类型的常量、...)
2)元数据验证(是否有父类、父类是否继承了不允许被继承的类(final修饰的类)、非抽象类是否实现了抽象方法/接口、
类中的字段/方法是否与父类产生冲突(覆盖父类的final字段/出现不合规则的方法重载))
3)字节码验证(确定语义合法、符合逻辑)
(保证操作数栈数据类型与指令代码序列能配合工作、保证跳转指令不会跳转到方法体外的字节码指令上、
保证方法体中类型转换是有效的)
4)符号引用验证(虚拟机将符号引用转化为直接引用时触发,对常量池中各种符号引用的信息进行匹配校验)
(符号引用中的全限定名是否能找到对应的类、指定类中是否存在符合方法的字段描述及简单名称所描述的方法和字段、符号引用中丁类/字段/方法是否可被当前类访问)
3、准备
1)为类变量(被static修饰的变量)分配内存、赋初始值,如果类变量赋值的是常量池中已经有的常量,则赋值为该常量,否则为该类型零值。
2)类变量使用的内存在方法区中进行分配。
4、解析(虚拟机将常量池内的符号引用替换为直接引用的过程)
1)符号引用:根据该符号可以无歧义的定位到目标,比如com.example.test.Test.class;符号引用与内存布局无关。
符号引用的目标不一定已经加载到内存中。
2)直接引用:直接指向目标的指针、相对偏移量、一个能简介定位到目标的句柄。与内存布局相关。
如果有了直接引用,那引用的目标必定已经在内存中存在了。
3)解析动作
a:类/接口解析
i:非数组类型,虚拟机把符号引用全限定名传给当前代码所在类的类加载器,去加载该全限定名对应的类。
ii:数组类型,数组元素类型为对象,把符号引用的全限定名传给当前代码所在类的类加载器,加载数组元素类,然后由虚拟机生成一个代表该数组维度和元素的数组对象。
iii:符号引用验证,确定当前类是否对符号引用所指的类有访问权限,无权限会抛IllegalAcessError异常
b:字段解析(对字段所属的类/接口的符号引用解析)
i:字段所属的类本身包含了简单名称、字段描述符与目标相匹配的字段,返回该字段的直接引用。
ii:字段所属类实现了接口,按照继承关系从下往上地柜搜索各个接口和父接口,如果接口包含了简单名称、字段描述符与目标相匹配的字段,返回该字段的直接引用
iii:字段不是java.lang.Object,按照继承关系从下往上地柜搜索其父类,如果父类中包含了简单名称、字段描述符与目标相匹配的字段,返回该字段的直接引用
iv:如果以上三步都没找到该字段的直接引用,抛出NoSuchFieldError异常。
v:对字段进行权限验证,如果不具备对该字段的访问权限,抛出IllegalAccessError异常
c:类方法解析
..
d:接口方法解析
..
5、初始化(执行类构造器<clinit>()方法的过程)
1)<clinit>()方法是 编译器按源代码中出现顺序自动收集类中所有类变量(static修饰的变量)的复制动作和静态块中的语句合并产生的
2)<clinit>()方法与类的构造函数(<init>()方法)不同,它不需要显示调用父类构造器,虚拟机会保证子类的<clinit>()方法执行之前,
     父类的<clinit>()方法已经执行完毕。因此虚拟机中第一个被执行的<clinit>()方法的类一定是java.lang.Object。
3) 父类的<clinit>()先执行,所以父类中定义的静态语句块要先于子类的变量赋值
4)<clinit>()方法对于类/接口不是必须的,类中没有静态块、没有对变量的赋值操作,编译器可以不为这个类生成<clinit>()方法。
5)与类不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()放阿飞,只有当父类接口中定义的变量使用时,父类接口才会初始化。接口的实现类在初始化时也不会执行该接口的<clinit>()方法。
6)类的<clinit>()方法是线程安全的,所以<clinit>()中的耗时操作在多线程初始化类时可能造成阻塞。
6、使用
7、卸载
三、类加载器
1、类与类加载器
  任何一个类,都要由加载它的类加载器和这个类本身共同确定它在Java虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。
  所以同一个类,用不同类加载器加载,也是不同的。
2、双亲委派模型
   1)启动类加载器(Bootstrap ClassLoader)
  a:负责把存放在<JAVA_HOME>\lib目录下或被-Xbootclasspath参数指定的路径中,
    能被虚拟机识别(仅按文件名识别rt.jar,名字不符合的类库不会被加载)的类库加载到虚拟机内存中。
  b:该类加载器无法被应用程序直接引用,自定义类加载器时如果要把加载请求委派给它加载,直接用null代替即可。
   2)扩展类加载器(Extension ClassLoader)
  该加载器由sum.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中或者java.ext.dirs系统变量指定的路径
  中所有类库。   
   3)应用程序类加载器(Application ClassLoader)/也称为系统类加载器
  该加载器由sum.misc.Launcher$App-ClassLoader实现,在ClassLoader类中的getSystemClassLoader()方法返回该类加载器。负责加载用户类路径(ClassPath)上所指定的类库,程序默认使用这个类加载器。
4)双亲委派模型
一个类加载器收到类加载的请求,它首先把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成 这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。
委派顺序:自定义类加载器 > ApplicationClassLoader > Extension ClassLoader > Bootstrap ClassLoader
双亲委派模型保证最终都由启动类加载器去加载类,从而保证类加载不会混乱。类,由类加载器、类本身共同确定唯一性。

5)双亲委派模型实现

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) {
	}
	if (c == null) {
	    long t1 = System.nanoTime();
	    c = findClass(name);//父类加载器处理不了,就由加载器的findClass方法来加载
	    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
	    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
	    sun.misc.PerfCounter.getFindClasses().increment();
	}
    }
    if (resolve) {
	resolveClass(c);
    }
    return c;
}
			}
6)从双亲委派模型的实现可知, 如果自定义类加载器,覆盖了loadClass方法,会导致双亲委派模型被破坏,
 那么该自定义类加载器与启动类加载器加载同一个class得到的也不会是相同的类对象。
 所以自定义类加载器,正常不要去覆盖loadClass方法
,但是可以重新findClass方法。

猜你喜欢

转载自blog.csdn.net/u010577768/article/details/80293226