深入理解JVM之类加载
JVM将类加载过程分为三个步骤:加载、链接(验证、准备、解析)、初始化。
类加载流程
一,加载(loading)
有两种时机会触发类加载:
1,预加载。虚拟机启动时加载,加载的是JAVA_HOME/lib/rt.jar下的class,这里面都是一些常用的Class,如java.lang.String、java.util.List等。
2,运行时加载。虚拟机需要用到一个Class时,先去内存中查找是否被加载,如果没有就会按照类的全限定名来加载这个类。
JVM通过类的全限定名(com.sun.HelloWorld)及类加载器(ClassLoader实例)完成类的加载,相应地,也是用这两个元素来标识一个被加载了的类:类的全限定名 + ClassLoader实例ID。
加载的时候,通过class文件二进制流,将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中。然后在内存中生成一个代表这个.class文件的java.lang.Class对象,在HotSpot虚拟机中,这个Class对象是放在方法区中的。
二,验证
确保class文件内容符合虚拟机的规范,比如验证“魔数”是否为0xCAFEBABE、Class文件编译版本号是否符合当前JVM等。JDK能向下兼容版本号,但不能向上兼容。即JDK1.6能运行在JDK1.5环境编译的class,但是反过来不行。如果格式不符则抛出VerifyError。
如果碰到要引用其它接口或类,也会进行加载,如果加载过程失败,则会抛出NoClassDefFoundError。
三,准备
为类变量在方法区分配内存并初始化。需要注意的是:这个阶段初始化的变量指的是不被final修饰的static变量,如”public static int i = 100;”,i在准备阶段过后是0而不是100,给i赋值为100的动作将在初始化阶段才进行;但是”public static final int i = 100;”就不一样了,在准备阶段,就会给i赋值为100。
四,解析
将常量池中的符号引用替换为直接引用,有哪些符号引用?
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
五,初始化
初始化过程即执行类中的静态初始化代码、构造代码及静态属性的初始化。虚拟机会保证类的初始化在多线程环境中被正确地加锁、同步。以下几种情况会触发初始化过程:
1,调用new
2,反射调用了类中的方法
3,子类调用了初始化
4,JVM启动过程中指定的初始化类
另外,有几种不会触发类初始化的情况:
1,子类引用从父类继承下来的静态字段,不会导致子类初始化(只会初始化父类)
2,通过数组定义引用类,不会触发此类的初始化(new User[10])
3,引用静态常亮(static final常量在编译阶段就已进入类的常量池)
四,类加载器
JVM的类加载通过ClassLoader及其子类来完成,分为Bootstrap ClassLoader、Extension ClassLoader、System ClassLoader及User-Defined ClassLoader,除了Bootstrap外,剩下3个都是有父子层级关系的。
Bootstrap,此类不是ClassLoader的子类,代码中无法拿到这个对象,JDK启动时会初始化该ClassLoader,并由它加载$JAVA_HOME/jre/rt.jar中所有class的加载,这些class是java规范定义的所有接口及实现(java.lang, java.util等)。
Extension加载$JAVA_HOME/jre/ext/*.jar。
System加载启动参数中指定的Classpath中的jar,简言之,用户写的class会由它加载,Sun JDK中对应的实现类为AppClassLoader
User-Defined ClassLoader是开发人员自行实现的ClassLoader,可用于加载非classpath中的jar,如从网络上下载的jar或二进制。
之前说过,ClassLoader是有父子层级关系的,当加载class时,首先会去已加载的class中查找,如果有,直接返回,否则判断parentClassLoader是否存在,存在,调用parent . loadClass,不存在,调用findBootstrapClassOrNull。如果最后还是找不到,则调用findClass,该方法默认直接throw new ClassNotFoundException,需要ClassLoader子类自己覆盖。
但加载时也可以不采用以上顺序,可以直接在当前的ClassLoader中寻找,如Class.forName(),JVM会直接通过当前ClassLoader(也就是执行Class.forName所在类的ClassLoader,caller.getClassLoader0() native方法)加载Class。
由于JVM是采用类全限定名+ClassLoader实例来判断是否加载了某个类,所以,如果直接从当前classLoader直接加载的话,
会造成树上多个不同的classloader中都加载了某class(某处Class.forName,某处直接new),且这些Class的实例对象都不同。JVM会保证同一个ClassLoader实例中只能加载一次同样名称的Class,因此可以借助此来实现类隔离的需求。
但也会有问题,例如ClassCastException,因此在加载类时,尽量保证从根到最下层的ClassLoader上的Class只加载一次。
当ClassLoader在整个树中都没找到Class对象,则抛出ClassNotFoundException
类加载的常见异常:
1,ClassNotFoundException:ClassLoader加载类时未找到该类。
2,NoClassDefFoundError:在加载的类中引用到其它的类不存在,或者加载引用的类失败。
3,ClassCastException:两个对象的Class有不同的ClassLoader加载。