一般来说,我们判断两个类是不是同一类,都认为表面是一个类就行了,也就是只要是编译后的字节码文件.class是一样就会认为是同一个类,所以有时候我们可能会盲目的去认为instanceOf去判断。但是呢,其实一个类是通过类加载器和class文件决定的,所以即便是同一个类class,但是如果加载它的类加载器不同,那么就会造成用instanceOf去判断不正确。
一般来说,我们在加载类的时候,要经过上图的几个过程:加载->连接(验证->准备->解析)->初始化
而剩下的使用和卸载可以不用关注的
加载😯
一般需要完成下列三个过程:
- 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;比如代码这些
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;以后我们要访问这个类的静态变量等等底层就是通过这个对象访问的。
验证🦢
这个过程是为了确保我们的字节码文件不会对虚拟机造成伤害,一般包括下面几个部分:
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=10;
public static void main(String[] args){
}
那变量value在准备阶段过后的初始值为0而不是10.因为这时候尚未开始执行任何java方法,而把value赋值为10的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为10的动作将在初始化阶段才会执行。
- 至于特殊情况是指:public static final int value=10,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为10而非0。你也可以当作final修饰后不能二次赋值吧,你可以这样认为的。
解析🐸
把类中的符号引用转化为直接引用。
符号引用和直接引用。比如在方法A中使用方法B
A(){
B();//B是符号引用
}
这里的B()就是符号引用,初学java时我们都是知道这是java的引用,以为B指向B方法的内存地址,但是这是不完整的,这里的B只是一个符号引用,它对于方法的调用没有太多的实际意义,可以这么认为,他就是给程序员看的一个标志,让程序员知道,这个方法可以这么调用,但是B方法实际调用时是通过一个指针指向B方法的内存地址,这个指针才是真正负责方法调用,他就是直接引用。
初始化🐻
我们在准备阶段其实已经为类变量分配了内存,并且已经赋值过一次了,但是呢,那次赋值并不是我们程序员的初衷,除了fianl修饰的之外,也就是说在准备阶段赋值是类型的默认初始值,并不是我们程序员主观意愿的直接赋值,而在初始化阶段就是根据我们程序员的意愿去初始化类变量和其他类资源比如static静态语句块。
类变量,静态语句块都是按顺序初始化的,如果一个类变量定义在了static语句块的后面,那么在语句块里面只能赋值不能访问它。看一段代码吧
这个,因为i在static语句块后面定义,那么我们就不能访问。可见上面访问的哪一行已经报错了。为什么static语句块后面的静态变量可以赋值而不能访问呢,你可以这样理解,我们在准备阶段它已经被分配了内存,所以赋值是肯定没有问题的,但是本质上还没有define,所以你拿它去参与运算就会出错了。
把哪一行注释掉,就可以知道static语句块和静态变量顺序初始化,最后i=1
在这个初始化阶段,本质上是执行的类构造器的< clinit >方法,它将静态语句块和类变量都涵纳在里面顺序执行。它和实例构造器< init >方法是不同的,< clinit >方法能够保证父类的< clinit >方法能够已经执行。
()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成()方法。但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。
我们都知道,类都只能被加载一次,如果你要使用,那么就会在Heap中去创建它的对象实例,而如果在多线程初始化一个类的情况下,虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。
初始化的情况
- 遇到new,getstatic,putstatic,invokestatic这失调字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
什么情况下不用初始化
- 通过数组定义来引用类,不会触发此类的初始化
- 通过子类引用父类的静态变量,不会导致子类初始化
父类:
public class initclass {
static {
i = 0;
System.out.println("father is clinit");
}
static int i = 1;
}
子类:
public class sonclass extends initclass{
static {
i=0;
System.out.println("son is clinit");
}
}
测试类:
public class test {
public static void main(String[] args) {
System.out.println(sonclass.i);
}
}
输出结果:
father is clinit
1
按道理,应该先执行父类的clinit方法,然后执行子类的clinit方法,
但是结果输出father is clinit而没有子类静态语句块中的clinit方法,可见子类static语句块没有执行
- 引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
public class ConstClass
{
static
{
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitialization
{
public static void main(String[] args)
{
System.out.println(ConstClass.HELLOWORLD);
}
}
输出结果:hello world
他没有输出ConstClass的静态语句块的内容,
是因为在编译的时候,HELLOWORLD的值已经在类的常量池中了,
即在class文件中的静态常量池,和我们加载类后在内存中的运行时常量池不同的。
- 当访问一个静态域时,只有真正声名这个域的类才会被初始化
类加载器😼
前面简单记述了一些类加载的粗略过程和需要注意的一些点,那么现在就来宏观看一下,类是怎么被加载的。
java中采用了双亲委派模型去加载类
- 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
- 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
- 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
- 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
这个模型工作过程就是每次加载一个类,底层类加载器不会自己去加载,而是通过父加载器去加载,如果父类加载器找不到这个类,即出现ClassNotFound,那么就会交由自己去加载。
为什么要使用这种模型,因为有些时候,我们jdk有很多jar包都是已经完好的,比如String类,Integer类这些,都是写的比我们自己完美的,如果有些人图谋不轨自己把String代码搞过来,还加入一些可恶的代码,这样就会造成混乱且出现错误,所以说,如果使用这种模型,就会交由父加载器去加载String类,因为同一个类加载器已经加载过一个String类了,所以我们自己写的String类不会被加载,因为我们通过String名字去加载String类,总会到达最上面的类加载器,而最上面的累加器总会去jdk里面加载String类,所以你自己的没用。
也有人会想,使用自定义类加载器,强制加载自己的String类,不向上使用父加载器,但是呢,不是每个人都会使用你的类加载器,而且,我们判断多个String类实例时会造成混乱,因为不同类加载器加载同一个类其实会被认为不同类的,因为类加载器不同。最开初说过,确定一个类要通过类加载器和class字节码文件确定。