`
smiky
  • 浏览: 253273 次
  • 性别: Icon_minigender_1
  • 来自: 天门
社区版块
存档分类
最新评论

类加载及方法分派笔记

 
阅读更多

内容全部来自深入理解java虚拟机,理解能力有限,可能有错误,只是个人笔记,防止忘了

类加载过程:

package org.gerry.classLoader;
/**
 * -XX:+TraceClassLoading会打印出类加载的过程
 * 加载阶段完成三件事:
 * 1. 根据类的全限定名获取二进制字节流
 * 2. 将二进制字节流代表静态存储结构转成方法区中的运行时数据结构
 * 3. 在堆中实例化一个代表该类的java.lang.Class的对象,作为程序中访问方法区中该类数据的外部接口
 * 
 * 验证:
 * 1. 文件格式验证———— 检查格式是否有问题,如我将魔数改了
 * 2. 元数据验证———— 基于JAVA语法的验证,如两个方法签名相同,
 * 3. 字节码验证———— 验证代码逻辑,如iadd的操作数不能是long
 * 4. 符号引用验证———— 如引用的字段不存在,访问了无权访问的内容如private字段
 * 文件格式验证成功后,虚拟机外的字节流就按照虚拟机所需的格式存放在方法区中
 * 2-4都是基于方法区的存储结构进行
 * 
 * 准备:
 * 为类变量分配内存并赋默认初值(属性表中有ConstantValue的会赋具体值),这些内存在方法区中分配
 * (实例变量随着对象实例化时一起在堆中分配内存,这得在初始化之后)
 * 
 * 解析:
 * 将常量池中的符号引用替换成直接引用
 * 符号引用:用字面量来描述要引用的目标,只要能定位即可(如常量表中的index)
 * 直接引用:可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。如果有了直接引用,那么目标肯定己经在内存中了
 * 
 * 符号引用中在Class文件中以Class_info,Fieldref_info,Methodref_info,InterfaceMethodref_info类型的常量出现
 * 并未规定解析发生的时间,但是在执行以下13个指令之前,一定要对指令使用到的符号引用进行解析:
 * anewarray	multianewarray 	new	getfield	putfield	getstatic	putstatic	
 * invokeinterface	invokespecial	invokestatic	invokevirtual	checkcast	instanceof
 * 
 * 缓存直接引用,在运行时常量池中记录直接引用,并标识为己解析,对于同一个实体,解析一次后后面的都获取同一个
 * 
 * 解析的目标有:
 * 1. 类或接口	————	CONSTANT_Class_info	
 * 2. 字段	————	CONSTANT_Fieldref_info
 * 3. 方法	————	CONSTANT_Methodref_info 类中方法
 * 4. 接口方法	————	CONSTANT_InterfaceMethodref_info	接口中的方法
 * 
 * 类或接口解析过程:如Demo中引用了Son
 * 对应的常量池:
 * const #32 = class       #33;    //  org/gerry/classLoader/Son
 * const #33 = Asciz       org/gerry/classLoader/Son;
 * 对应的字节码:
 *  0:   ldc     #32; //class org/gerry/classLoader/Son
 *  2:   invokevirtual   #34; //Method java/lang/Object.getClass:()Ljava/lang/Class;
 * 根据#32的符号引用找到#32对应的Constant_Class_info常量池打到对应的#33,取出其中的全称类名
 * 根据类名加载对应的类(有父类的话会触发父类加载),这样在方法区中就有对应数据了,最后在测试直接引用是否有效
 * (如无权限的情况下就报非法访问java.lang.IllegalAccessError)
 * 如果是数组的话,会先加载数据元素类型
 * 
 * 字段解析:
 * 1. 解析Constant_Field_ref中的class_index对应的类假如为C(用上面的方式)
 * 2. 如查C中有NameAndTyperef_info中对应的字段就直接返回字段的直接引用
 * 3. 2中没找到,就看其有无接口, 在接口(如果找不到,再到其父接口中找)中查找对应字段,如果有就返回其直接引用
 * 4. 3中没找到,如果不是java.lang.Object,就从下到上依其继承关系找,找到就返回
 * 5. 抛出java.lang.NoSuchFieldError
 * 如果找到,要对其进行引用验证
 * 
 * 类方法解析:
 * 1. 找到C
 * 2. 如果C是接口就报错java.lang.IncompatibleClassChangeError
 * 3. 在类中找
 * 4. 在父类中找
 * 5. 在接口中找(在这里找到只能证明这是个抽象类,且这是个抽象方法,报java.lang.AbstractMethodError)
 * 6. java.lang.NoSuchMethodError
 * 
 * 接口方法解析:
 * 1. 打到C
 * 2. 如果C是类就报错java.lang.IncompatibleClassChangeError
 * 3. 在接口中找
 * 4. 在父接口中找
 * 5. java.lang.NoSuchMethodError
 * 
 * 初始化:
 * 初始化过程是执行类构造器clinit()方法的过程
 * 1. clinit里面的内容是合并static块与static变量组成,static块只能访问在它之前定义的static变量
 * 2. 虚拟机保证父类的clinit会先于子类clinit执行,最先执行的clinit肯定是java.lang.Object的
 * 3. 父类static字段会先赋值,这个2
 * 4. 如果没有static块或节段,不会生成clinit
 * 5. 对于接口,只有在子接口中使用到父接口中变量时,父接口才会初始化,接口实现类初始化时不会执行接口的clinit
 * 6. 虚拟机保证多线程下只有一个线程能执行类的clinit,会被加锁与同步
 */
public class Demo 
{
	static
	{
		System.out.println( ConstantA.love );//对于常量,在编译时就直接写进类里面了
	}
	//入口类会优先加载,只有加载了才有初始化
	public static void main(String[] args) throws ClassNotFoundException 
	{
//		System.out.println( Son.value );//对于静态字段,只有直接定义它的类才会被初始化,这里只会初始化父类
//		Son[] sons = new Son[10];//new一个数组,会有对象被初始化,[Lorg.gerry.classLoader.Son;
//		Class.forName("org.gerry.classLoader.Parent");//加载,且初始化
		Son.class.getClass();//加载,不初始化,会倒致父类加载
//		SonInterface.class.getClass();//加载,不初始化,会倒致接口加载
//		Demo.class.getClassLoader().loadClass("org.gerry.classLoader.Parent");//加载,不初始化
	}
	/**
	 * 四种情况会初始化:
	 * 1. new, getstatic, putstatic, invokestatic 例外:final修饰、在编译期被入入常量池的静态变量
	 * 2. 用java.lang.refelect包中的方法对类进行发射调用时
	 * 3. 初始化类时,如果父类没初始化,先初始化父类
	 * 4. 虚拟机启动时初始化主类
	 * 上面四种情况属于主动调用
	 * 
	 * 下面三种情况属于被动调用:
	 * 1. 引用常量,如上面的System.out.println( ConstantA.love );这个常量直接被写到类中,执行时己与Constant类无关
	 * 2. 子类引用父类静态变量,static字段只初始化直接定义的类,但是会加载子类,如System.out.println( Son.value );
	 * 3. 定义某种类型的数组 , 不会初始化这个类,但是会加载,如Son[] sons = new Son[10];
	 * 
	 * 一定要区分加载与初始化,加载是将类文件加载到虚拟机,初始化是分配内存或给值
	 */
	/**
	 	只有这句时:System.out.println( Son.value );
	 	[Loaded org.gerry.classLoader.Demo from file:/D:/work/workspace/readCode/bin/]
		1314
		[Loaded org.gerry.classLoader.Parent from file:/D:/work/workspace/readCode/bin/]
		[Loaded org.gerry.classLoader.Son from file:/D:/work/workspace/readCode/bin/]
		parent init...
		123
		先加载父类,再加载子类,然后初始化父类,静态字段的调用,只会加载直接定义它的类
	 */
	
	/**
	 	只有这句时:Son[] sons = new Son[10];
	 	[Loaded org.gerry.classLoader.Demo from file:/D:/work/workspace/readCode/bin/]
		1314
		[Loaded org.gerry.classLoader.Parent from file:/D:/work/workspace/readCode/bin/]
		[Loaded org.gerry.classLoader.Son from file:/D:/work/workspace/readCode/bin/]
		可以看出只是加载了Son与Parent,并没有初始化
	 */
	
	/**
	 	Class.forName("org.gerry.classLoader.Parent")会倒致Parent的初始化
	 	[Loaded org.gerry.classLoader.Demo from file:/D:/work/workspace/readCode/bin/]
		1314
		[Loaded org.gerry.classLoader.Parent from file:/D:/work/workspace/readCode/bin/]
		parent init...
	 */
	
	/**
	 	Demo.class.getClassLoader().loadClass("org.gerry.classLoader.Parent");只加载,不初始化
	 	[Loaded org.gerry.classLoader.Demo from file:/D:/work/workspace/readCode/bin/]
		1314
		[Loaded org.gerry.classLoader.Parent from file:/D:/work/workspace/readCode/bin/]
	 */
	/**
	 * 
	 	Son.class.getClass();
	 	Demo.class.getClassLoader().loadClass("org.gerry.classLoader.Parent");只加载,不初始化
	 	[Loaded org.gerry.classLoader.Demo from file:/D:/work/workspace/readCode/bin/]
		1314
		[Loaded org.gerry.classLoader.Parent from file:/D:/work/workspace/readCode/bin/]
		[Loaded org.gerry.classLoader.Son from file:/D:/work/workspace/readCode/bin/]
	 */
}

 

类加载器的双亲委派模型:

package org.gerry.classLoader;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

/**
 * 
 * 类加载器在加载一个类的时候,先查看该类是否己经加载了(loadClass方法里面的逻辑),如果没有,那么就由父类去找,
 * 如果到了最顶层类加载器还没找到,就从顶层类加载器开始加载那个类,加载不成功的话就由下一层的类接着加载
 * 
 * 为什么下面的例子中Parent会被加载两次,分别由自定义类加载器与父类加载器加载,主要原因是直接覆盖了loadClass方法
 * 破坏了双新委派模式引起的
 * AppClassLoader从下到上找不到Parent,并且最后由它自己加载
 * 自定义定加载器在加载类时没有从下到上的找,只是自己加载了Parent,不管在前还是再后AppClassLoader都找不到由它加载的Parent
 * 所以加载了两次
 * 
 * 至于线程上下文,我估计就是为了防止SPI(如JDBC)的类被不同的类加载器加载,所以对于JDBC的类加载,都是取出线程里的
 * 线程上下文类加载器后,再由它来加载,这样就不会出现两个不同类加载器加载的类不能赋值的情况出现
 * System.out.println( ( Parent )sonCl.newInstance() );
 * 要注意:
 * ArrayList与MyList的例子
 * MyList由AppClassLoader加载
 * ArrayList由引导加载
 * 而ArrayList ar = new MyList();是不会出现那( Parent )sonCl.newInstance()的强制改化错误的
 * 看来赋值操作只管类型的可见性,即findLoadedClass能否找到
 * 
 * 对于当前类中出现的Parent是由AppClassLoader加载的,而Son由匿名类加载,它也会加载对应的Parent,
 * 这个时候匿名类加载器只能看到它自己加载的Parent,而强制转化的Parent并不是它的,所以会报错,下面将
 * Parent改成匿名类的父类即AppClassLoader加载后就不会报错了,因为变成可见了
 * 
 */
public class TestClassLoader 
{
	
	public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException 
	{
		/**
		 * 一般是覆盖findClass方法,这样的话就不会破坏双亲委派模型,它会保证尽量由父类去加载子类,防止出现一个类
		 * 由两个类加载器去加载,
		 * 这里覆盖loadClass强制加载指定的类,就出现了同一个类由两个类加载器加载
		 */
		//这是个匿名类,也会被加载
		ClassLoader cl = new ClassLoader()
		{
			@Override
			public Class<?> loadClass(String name)
					throws ClassNotFoundException {
				String fileName = name.substring( name.lastIndexOf(".") + 1 ) + ".class";
//				System.out.println(getClass().getClassLoader().getResource(""));//在bin目路
				//getClass().getResource("")在当前目录
				InputStream is = getClass().getResourceAsStream( fileName );
				if( is == null || fileName.equals("Parent.class" ) )
				{
					return super.loadClass(name);//报ClassNotFoundException异常
				}
//				else
//				{
//					return super.loadClass(name);
					//如果这里改成这样,那么类就是由父类加载的,TestClassLoader.class.getClassLoader获取的将会是AppClassLoader
//				}
				byte[] bt;
				try {
					bt = new byte[is.available()];
					is.read(bt);
					return defineClass(name, bt, 0, bt.length );//报NoClassDefFoundException
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				return super.loadClass(name);
			}
		};
//		Thread.currentThread().setContextClassLoader( cl );//设置线程上下文之后,在当前的线程中遇到的类默认以它来加载
//		System.out.println(Parent.class.getClassLoader());
		ClassLoader l = TestClassLoader.class.getClassLoader();
		//这个类己经被加载了,在双亲委派模式下不会被从新加载
		Class tClass = l.loadClass("org.gerry.classLoader.TestClassLoader");
		System.out.println( TestClassLoader.class == tClass );
		
		//比较不是从同一个类加载器加载的类是无意义的,一个类由类加载器与它本身决定其唯一性
		Class tClass2 = cl.loadClass("org.gerry.classLoader.TestClassLoader");
		System.out.println( TestClassLoader.class == tClass2 );
		System.out.println( tClass2.getClassLoader() );
		
		Class thisClass = TestClassLoader.class;
		// 64:  ldc     #1; //class org/gerry/classLoader/TestClassLoader
		// 66:  astore  5
		// 在文件格式验证之后字节流以虚拟机要求的格式存进了方法区,在解析完成后,符号引用就转成了直接引用,
		//也就是说这里的#1己经替换成直接引用了,而TestClassLoader.class能获取到这个直接引用
		System.out.println( TestClassLoader.class.getClassLoader() );
		//一个类的定义类加载器,是该类中其它类的初始化加载器
		//调用loadClassr的是初始化加载器调用的
		//调用defineClass的是定义类加载器,区分class的类加载器是看定义类加载器,也就是说,谁加载的类谁就是定义类加载器
		//如这里的匿名内部类org.gerry.classLoader.TestClassLoader$1就是由加载TestClassLoader的类AppClassLoader作为初始化类加载器
		
		/**
		 * rt下面的类是由启动类加载器加载的,JAVA程序是无法获得启动加载器
		 */
		System.out.println( String.class.getClass().getClassLoader() );//null
		System.out.println( Thread.currentThread().getClass().getClassLoader() );//null
		
		Class ParentCl = cl.loadClass("org.gerry.classLoader.Parent");
		Class sonCl = cl.loadClass("org.gerry.classLoader.Son");//它也会倒致ParentCl的加载,
		//可以看出自定义类加载器
		System.out.println( ( Parent )sonCl.newInstance());//强制转化异常,字节码指令为checkcast
		System.out.println( "===============不同类加载器加载的类不能赋值===============" );
		System.out.println( sonCl.getClassLoader().toString() );
		System.out.println( Son.class.getClassLoader().toString() );
		System.out.println( (Son)new Son());
		System.out.println( (Son)sonCl.newInstance() );//报错
		System.out.println( "=============================" );
		Class myListCl = l.loadClass("org.gerry.classLoader.MyList");
		ArrayList ar = new MyList();
		/**
		 * ArrayList与MyList分别由两个类加载器加载,但是不影响它们的给值
		 * 同一个类被两个类加载器加载,那么它们生成的对象是不能相互给值的,因为它们是不同的类
		 */
		System.out.println( ArrayList.class.getClassLoader() );
		System.out.println( MyList.class.getClassLoader() );
		
//		Class pl = cl.loadClass("org.gerry.classLoader.Parent");//同一个加载器第二次加载会报错
		System.out.println( Parent.class.toString() + '|' + ParentCl.getClassLoader().toString() );
	}
}

class MyList extends ArrayList 
{

}

方法分派:

package org.gerry.codeExecute;

/**
 * 活动线程中只有最顶端的栈桢是有效的,称为当前栈桢,它对应的方法称为当前方法
 * 局部变量表:
 * 1. 局部变量表中第0位slot默认存储方法所有者实例的引用(名字叫this)
 * 2. 虚机通过局部变量表完成参数值到参数列表的传递,如setValue(1,2)对应方法setValue(x,y)
 * 	那么会将1与2存入局部变量表中的x与y,按顺序排在this后面,之后再为方法内的变量分配slot
 * 
 * 操作数栈(后进先出)
 * 
 * 
 * 动态链接
 * 1. 栈桢中有一个指向方法区中所属方法的符号引用
 * 2. 方法调用指令是以方法的符号引用作为参数的,符号引用有一部分是在加载类或第一次使用时转为
 * 直接引用,这称为静态解析(invokestatic,invokespecial对应的方法符号引用,可以确定调用的是什么方法
 * ————编译期确定,执行期不可变),
 * 还有一部分在每次运行期间才转成直接引用,称为动态链接(invokevirtual多态调用,只有到运行时你才
 * 能知道是谁在调用这个方法,才能去决定调用那个方法)
 * 
 * 方法返回地址:
 * 1. 调用当前方法(对就调用者的一条指令)的方法称为方法的调用者(main中调用test,那么test执行时它的调用者就是main)
 * 2. 方法正常返回时,要恢复调用者方法,可以把调用者PC计数器作为返回地址,栈桢中应该会保留这个地址。
 * 方法异常退出参考异常信息表
 * 3. 方法退出时要做的事,首先恢复调用者方法的本地变量表,操作数栈,如果有返回值就将其压入操作数栈,
 * 接着将PC计数器指向方法调用指令的下一条指令。
 * 
 * 
 * 方法调用:
 * 1. 方法调用是确定调用方法的版本(那一个方法),不是指运行
 * 2. 在编译期就能确定版本,在运行期不可变的方法,在编译期就可以将符号引用转成直接引用,这类方法的调用叫解析
 * 3. 解析是针对非虚方法(invokestatic,invokespecial指令对应的方法,
 * 	对应的方法有,构造方法,父类方法,私有方法,静态方法,final方法不能覆盖,也算能确定版本,但是静态分派能影响它吧)
 * 4. 解析并一定能唯一的确定方法,因为方法是可以重载的,那么怎么选择重载的方法呢?
 * 这里就轮到静态分派了.
 * 静态分派:根据参数的静态类型来决定重载版本(Parent p = new Son(),Parent是静态类型,son是实际类型)
 *	—————————— 静态分派发生在编译阶段(就是选择重载的版本)
 * 对于重载的方法,如下面的invoke(Human human)与invoke(Parent parent)及invoke(Son son)
 * 当执行invoke(son)时有三个方法都是满足条件的,son可以看成parent与human,
 * 这时候编译器会根据一定的规则来选择最合适的方法,当invoke(Son son)存在时它最合适,无它时找Parent一至向上找
 * 如char-->int-->long-->character-->serializable-->object-->args...展示了传入char时方法选择的顺序
 * 
 * 动态分派:在运行期,根据方法接收者的实际类型(调用方法的对象如A.test()类型就是A)来决定调用版本(选择覆盖的版本)
 * invokevirtual的多态查找过程:
 * 1.找到操作数栈顶的元素所指向的实际类型
 * 2.在对应的实际类型中查找名称与描述符到致的方法,找到就OK,找不到就向父亲中找
 * 
 * 单分派与多分派:
 * 方法接收者与参数是影响方法版本的两个因素,如果由其中一项来决定版本就是单分派否则多分派
 * 对于方法重载:要关心调用者的静态类型与参数类型,它是多分派
 * 对于重写:因为在单分派中己经决定了方法的重载版本,所以它只关心调用者的实际类型,所以它是单分派
 * 
 * 虚拟机实现动态分派的方式:
 * 因为动态分派要经常去查找相应方法,很费性能,于是虚方法表出现了,用于替代在元数据中查找方法
 * 结构:虚方法表中存放的是各个方法的实际入口,子类没有重写的方法存放的地址与父类相同,如果重写了,那么
 * 放它自己的地址
 * Parent						Object 						Son
 *  toString-------------------->toString<------------------toString
 *  getClass-------------------->getClass<------------------getClass
 *  test<-----------------------------------------------------test(子类中没有)
 *  abc ------------------------------------------------------abc(子类中也有)
 *  具有相同签名的方法在子类与父类中应该具有相同的索引,这样当从Son切到Parent时直接就可以找到对应方法
 *  
 *  方法表在连接阶进行初始化,当准备阶段完成后,虚拟机会把该类的方法表也初始化完毕
 *  
 *  基于栈的解释器执行:
 *  int i = 100;的执行过程:bipush 100; istore_1;
 *  指令					程序计数				操作数栈(长度为1)		本地变量表(maxLocals 2)
 *  bipush 100;			   1					100						this
 *  istore_1; 			   2					空了						this	100(第二个有值了)							
 */					
public class Demo 
{
	{
		int a= 1;
		System.out.println( a );
	}
	public Demo()
	{
		
	}
	public Demo( int i)
	{
		
	}
	/**
	 * maxLocals:4(因为long要占用两个slot,加上this与i就是4个slot)
	 * maxStack:2(操作数栈可以存放任何类型数据,这里long只占一个)
	 */
	public void maxStackNum()
	{
//		final int a = 12;//有无final在字节码中没区别,编辑器来确定它不会被变
		long l = 2l;//lconst_1是最大的,超过就先idc_w,将常量推送到栈顶,这个2l会被存进Long_info常量池
		int i = 65535;//iconst_5是最大的了,超过5就要判断了,先bipush(-128~127),再sipush(-32768~32767),超过了就常量池上
//		int m = i;//对应指令为iload_3 istore  4,别然iload_3为最大,但超过后用在后面跟4代替,所以不管有多少都没影响
//		long a1 = 1l;
//		long a2 = 2l;
//		long a3 = 3l;
//		long a4 = 4l;
//		long a = a4;
//		int[] barr = new int[ 5 ];//astore  4 aload   4根本就没用数组的操作啊
//		int[] carr = barr;
//		synchronized (this) {
//			monitorenter //获取对象的monitor,用于同步方法或同步块
//			monitorexit //释放 对象的monitor
//		}

	}
	public static void main(String[] args) {
		StaticDispatch.Human parent = new StaticDispatch.StaticParent();
		StaticDispatch.StaticSon son = new StaticDispatch.StaticSon();
		StaticDispatch sd = new StaticDispatch();
		sd.invoke( son );
//		StaticDispatch.invoke( son );
		//这里调用类方法的是invokestatic,但是它也会受到参数的影响啊,去掉invoke(parent)就自动跑到invoke(human)
		//里面去了,那它也是要在运行时才能确定方法的版本啊,那它怎么能叫解析?是分派才对啊
		//看来方法有无static对重载的方法都有影响
		//看来应该是静态分派针对重载(根据参数静态类型来),动态分派针对覆盖(实际类型内的方法选择,如果此时方有重载呢?)
	}
}

// public 1
// final 1 super 2
// interface abstract 24
// synthetic annotation enum 1 2 4
class StaticDispatch
{
	//public private protected static  1248
	//final 1
	//interface abstract 24
	//synthetic annotation enum 1 2 4
	static class Human
	{
		// public private protected static  1248
		// final volatile transient 148
		//
		// synthetic enum 14
		int i;
	}
	
	static class StaticParent extends Human
	{
		
	}
	
	static class StaticSon extends StaticParent
	{
		
	}
	//public private protected static  1248
	//final synchronize bridge varargs 1248
	//native abstract strict 148
	//synthetic 1
	public void invoke( Human human)
	{
		System.out.println("Human");
	}
	
	public static void invoke( StaticParent parent)
	{
		System.out.println("Parent");
	}
	
	public void invoke( StaticSon son)
	{
		System.out.println("Son");
	}
	
}

  

 

 

 

 

 

 

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics