1. 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
2. 与那些在编译时需要进行连接工作的语言不通,在java语言里面,类型的加载和连接过程都是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是却能为java应用提供高度的灵活性,java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态链接这个特点实行的。
3. 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证、准备和解析三个部分统称为连接。
4. 虚拟机规范则是严格规定了有且只有四种情况必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始):
1) 遇到new 、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4) 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5. 对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类初始化。
6. 接口与类真正有所区别的是类有且仅有四种描述的第四种,当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口已经初始化过。
7. 类加载的全过程包括:加载、验证、解析、准备、解析和初始化五个阶段。
8. 在加载阶段,虚拟机需要完成以下三件事情:
a) 通过一个类的权限定名来获取此类定义的二进制字节流。
b) 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
c) 在java堆中生成一个代表这个类的java.lang.Class对象,作为访问方法区的入口
9. 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
10. 虽然在java语言是相对安全的,但是在字节码层面,上述java代码无法做到的事情都是可以实现的,至少在语义上是可以表达出来的。所以对字节流进行验证是相当必要的。
11. 如果验证到输入的字节流不符合class文件的存储结构,就抛出一个java.lang.VerifyError异常。验证阶段大致会完成以下四个方面的验证:文件格式验证,元数据验证,字节码验证,符合引用验证。
12. 准备阶段是正式为变量分配内存并设置变量初始值的阶段,这些内存都将在方法区中进行分配。
13. 虚拟机规范之中并未规定解析阶段发生的具体时间,只要求了在执行anewarray、checkcast、getfield、getstatic、instantof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield、putstatic 这13个用于操作符号引用的字节码指令之前,现对他们所有使用的符号引用进行解析。
14. 到了初始化阶段,才真正的开始执行类定义的java程序的代码(或者说是字节码)。
15. 初始化阶段是执行类构造器<clinit>方法的过程。
16. <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器的收集顺序一定是先变量赋值,在静态语句块赋值,因此在静态语句块中可以访问到类变量的初始值了。
17. <clinit>()方法与类的构造器函数(或者说实例构造器<init>()方法)不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个执行的<clinit>()方法的类肯定是java.lang.Object。
18. <clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的复制操作,那么编译器可以不为这个类生成一个<clinit>()方法。
19. 接口是可以由静态变量的,但是不能有静态语句块。
20. 虚拟机会保证一个类的<clinit>()方法在多线程环境汇总被正确的加锁或者同步,如果多个线程同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,知道活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
21. 虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块被称为“类加载器”。
22. 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。说白了就是“判断两个类是否相等,只有在这两个类使用同一个类加载器的前提下才有意义。
23. 站在java虚拟机的角度讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是所有其他类加载器,这些类加载器都是由java语言实现,独立于虚拟机外部,并全部都继承自抽象类java.lang.ClassLoader。
24. 如果您有兴趣的话,可以尝试去写一个与rt.jar类库中已有类重名的java类,将会发现可以正常编译,但永远无法加载运行。
25. OSGI是当前业界“事实上“的java模块化标准,而OSGI实现模块热部署的关键则是它自定义类加载器机制实现的。每一个程序模块(OSGI中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现热替换。