我们所编写的java文件或其他经过编译器生成了class字节码文件,前面两篇已经对class文件的格式有了基本了解。虚拟机想要执行程序则需要将class文件加载到内存中,这个过程从字节流加载内存开始,到卸载出内存结束。其中包括: 加载——验证——准备——解析——初始化——使用——卸载。
上面阶段不是严格顺序执行,各个阶段往往都是交叉混合执行的,其中解析可能出现在初始化之后,满足java的动态绑定。
加载阶段
- 通过类的全限定名来获取二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存生成该类的java.lang.class的代表。
准备阶段
给类变量分配内存并赋初始值,如int型为0,boolean型为false。
解析阶段
虚拟机将常量池的符号引用转换为直接引用。常量池在上篇class中有介绍。
初始化
类构造器<clinit>方法执行(与对象构造器<init>区分开),此时类变量赋值(这个才是真实值)和静态代码块执行。
类的构造器<clinit>触发时机:
- 遇到new,getstatic,putstatic , invokestatic指令时,如果类未初始化,这该类先初始化。
- 使用java.lang.reflect方法对类进行反射调用时,未初始化则初始化。
- 初始化一个类时,如果父类未初始化先初始化父类。
- 虚拟机启动时,会初始化含有main()方法的那个类。
- 使用动态语言支持时,如果java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic, REF_putStatic, REF_invokeStatic的方法句柄。如果对应类没有初始化,先初始化。
类加载器ClassLoader
我们的应用程序大都使用双亲委派模型。使用这种模型加载某个类时,先委派父类执行,一直传到最顶端。只有在父加载器无法完成这个加载请求时,才会由自己去加载。
3个系统提供的类加载器管辖区域不同,Bootstap:加载<JAVA_HOME>\lib下的类库;Extension: 加载扩展的类库<JAVA_HOME>\lib\ext下的类库;APPlication:我们自己项目开发中写的类库。
在代码上看ClassLoader的源码
使用双亲委派模型的原因:如果类加载器不同,即使虚拟机相同,源文件相同,生成的类也是不相等的。比如Object类,如果自己自定义多个加载器去加载Object.class. 那么系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也会一团糟。使用双亲委派模型,委派给最高层的先加载,因此Object类在程序各个来加载器中都是同一个类。
代码验证,下面代码中obj instanceof ClassTest 中的ClassTest是由应用程序类加载器加载的,obj实例的类ClassTest由自定义类加载器myClassLoader加载的。这个例子打破了双亲委派模型,不打破只用重写findClass方法。