内容全部来自深入理解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");
}
}