1.类的加载过程
类加载器只负责Class文件加载,至于是否可以运行则由 执行引擎(Execution Engine)决定。加载的类信息防御一块称为 方法区(Method Area)的内存空间,除了类信息外方法区还会存放 运行时常量池(Runtime Constant Pool)。
图1-类加载过程
1.1 加载(Loading)
- 通过一个类的全限定名(包路径+类名)获取此类的二进制字节流
- 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口
1.2 链接(Linking)
1.2.1 验证(Verify)
目的在于确保Class 文件的字节流中包含的信息符合当前虚拟机的要求,保证被加载类的正确性。
主要包含4种验证:文件格式、元数据、字节码、符号引用。
1.2.2 准备(Prepare)
为变量分配内存并且设置该变量的默认初始值,即零值。这里不包含用·final 修饰的static变量,因为final在编译的时候就会分配,准备阶段会显示初始化。
这里不会为实例变量分配内存,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中。
1.2.3 解析(Resolve)
将常量池中的符号引用转换为直接引用的过程。
解析过程往往会伴随着JVM执行完初始化之后再执行
解析动作主要针对类、接口、字段、类方法、接口方法等
1.3 初始化(Initialization)
初始化阶段就是执行类构造器方法<clinit>()的过程。
此方法不需要定义,时JAVAC编译器自动收集类中所有的类变量的赋值动作和静态代码块中的语句合并过来的。
构造器方法中的指令按语句在源文件中出现的顺序执行
若该类有父类,JVM必须保证在子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
JVM必须保证一个类的<clinit>()在多线程情况下同步加锁。
类加载器分类
图2-类加载器关系图
- 启动类加载器:这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。
- 扩展类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。
- 应用程序类加载器:这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。
- 自定义加载器:用户自己定义的类加载器。
双亲委派机制
图3-双亲委派机制
- 如果一个类加载器收到类加载请求,他并不会自己去加载,而是不这个请求委托给父类加载器去执行
- 如果父类加载器还有其他父类加载器,则进一步向上委托,依次递归,最终到达顶层启动类加载器
- 如果父类加载器可以完成加载任务,就会成功返回,如果父类加载器无法完成加载任务,子加载器才会自己尝试
双亲委派机制优势
- 避免重复加载
- 避免核心类篡改
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。