类加载器
类加载器将class文件字节码内容加载到内存中,并将这些内容转换成方法区中运行的数据结构。
ClassLoader只负责class文件的加载,至于它是否可以运行,则由执行引擎Execution Engine决定
类加载器的分类
- 启动类加载器 (BootstrapClassLoader):
也叫根加载器,用于加载$JAVA_HOME/jre/lib/rt.jar包内的class文件。rt.jar是Java基础类库,包含Java运行环境所需的基础类
- 扩展类加载器(ExtClassLoader)<JDK1.9后改名为标准扩展类加载器(PlatformClassLoader)>:
由Java语言实现,用于加载$JAVA_HOME/jre/lib/ext/.jar目录下的class文件 - 应用程序类加载器(AppClassLoader):
用于加载当前应用的classpath的所有类
,也就是我们自己写的那些Java代码.
可以用以下代码,查看类所属的加载器
public class Test {
public static void main(String[] args) {
System.out.println(Test.class.getClassLoader());
}
}
用户自定义加载器
通过继承Java.lang.ClassLoader抽象类自定义一个类加载器
加载器的关系图
通过代码验证:
public class Test {
public static void main(String[] args) {
Class<Test> testClass = Test.class;
System.out.println(testClass.getClassLoader());
System.out.println(testClass.getClassLoader().getParent());
System.out.println(testClass.getClassLoader().getParent().getParent());
}
}
运行结果:
类加载步骤
分为三布
-
加载 Loading
通过一个类的全类名获取其二进制字节流,然年后转换成方法区的运行时数据结构,然后在内存中生成一个代表这和类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。 -
链接 Linking
该过程分为三个阶段:验证、准备、解析-
验证阶段
用于确保加载的Class文件的字节流包含的信息是否符合虚拟机要求,保证其合法性。 -
准备阶段
为类变量(静态变量)分配内存并跟根据对象类型赋对应的默认值。这里并不包含常量,常量在编译的时候就分配了,准备阶段会显示其对应的常量值,而实例变量不会在这个阶段初始化
-
解析阶段
用于将符号引用转换为直接引用。
实例代码:
-
public class Test { public static void main(String[] args) { String str = "hello"; System.out.println(str); } }
在保存Calss出打开cmd,使用javap -v Test.class
命令查看其对应的字节码
可以看到常量池中有许多符号引用(比如#2),解析阶段就是将其解析为直接引用(比如#2表示 字符串常量hello)的过程
- 初始化Initialization
该阶段就就是执行类的构造方法的过程。
clinit(构造器方法)不是类的构造器,不需要我们自己定义,是javac编译器自动搜集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
一个类不包含类变量(静态变量)和静态代码块,那么它的字节码中就不会有构造器方法()。
示例代码:
public class Test {
private static int aaa = 1;
static {
aaa = 200;
System.out.println(Test.bbb);
bbb = 300;
System.out.println(Test.bbb);
}
private static int bbb = 2;
public static void main(String[] args) {
System.out.println(Test.bbb);
}
}
运行结果
0
300
2
为什么呢?
可以看出bbb为类变量,在链接阶段会分配内存并且会赋默认值0,所以会首先输出0,然后赋值为300,在赋值为2。所以在静态代码块中可以进行输出修改。
变量的初始化顺序
常量
:在编译时就会被分配具体值静态变量
:在类加载的过程的初始化阶段会被赋值(在链接阶段时分配内存空间,并赋默认值)成员变量
:在对象初始化时赋初值
双亲委派机制
所谓的 双亲委派机制
就是:当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此。只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
实例:
在src/main/java目录下新建java.lang包,然后在该包下新建一个String类
代码如下:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("helo");
}
}
程序输出结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public staticvoid main(String[] args) 否则 JavaFX
应用程序类必须扩展javafx.application.Application
什么意思呢?明明在类中我们有main方法为什么说找不到呢?
所以上面的例子中,AppClassLoader委派给它的父类ExtClassLoader去加载,ExtClassLoader又委托给它的父类BootstrapClassLoader去加载。BootstrapClassLoader从它的加载路径$JAVA_HOME/jre/lib/rt.jar 下找到了 java.lang.String 类,即rt.jar包下的String类,而该类里并没有main方法,所以便抛出了如上异常。
优点:
采用双亲委派的好处是:不管那个加载器加载这个类,最终都是委托给顶层的启动类(根)加载器进行加载。这样就保证了使用不同的类加载器最终得到的都是同一个string对象,所以我们自定义的java类并不会去污染jdk自带的类,这种保护机制也叫做沙箱安全机制