一般在这些情况下,如果类没有被加载,那么会被自动加载:
使用new关键字创建对象时
使用某个类的静态成员(包括方法和字段)的时候
使用反射对类信息进行获取的时候
加载一个类的子类时
加载接口的实现类,且接口带有default
的方法默认实现时
比如这种情况,那么需要用到另一个类中的成员字段,所以就必须将另一个类加载之后才能访问:
public class Main {
public static void main(String[] args) {
System.out.println(Test.str);
}
public static class Test{
static {
System.out.println("我被初始化了!");
}
public static String str = "都看到这里了,不给个三连+关注吗?";
}
}
这里我们就演示一个不太好理解的情况,我们现在将静态成员变量修改为final类型的:
public class Main {
public static void main(String[] args) {
System.out.println(Test.str);
}
public static class Test{
static {
System.out.println("我被初始化了!");
}
public final static String str = "都看到这里了,不给个三连+关注吗?";
}
}
可以看到,在主方法中,我们使用了Test类的静态成员变量,并且此静态成员变量是一个final类型的,也就是说不可能再发生改变。那么各位觉得,Test类会像上面一样被初始化吗?
按照正常逻辑来说,既然要用到其他类中的字段,那么肯定需要加载其他类,但是这里我们结果发现,并没有对Test类进行加载,那么这是为什么呢?我们来看看Main类编译之后的字节码指令就知道了:
很明显,这里使用的是ldc
指令从常量池中将字符串取出并推向操作数栈顶,也就是说,在编译阶段,整个Test.str
直接被替换为了对应的字符串(因为final不可能发生改变的,编译就会进行优化,直接来个字符串比你去加载类在获取快得多不是吗,反正结果都一样),所以说编译之后,实际上跟Test类半毛钱关系都没有了。
所以说,当你在某些情况下疑惑为什么类加载了或是没有加载时,可以从字节码指令的角度去进行分析
一般情况下,只要遇到new
、getstatic
、putstatic
、invokestatic
这些指令时,都会进行类加载,比如:
这里很明显,是一定会将Test类进行加载的。
类的生命周期一共有7个阶段
加载阶段
加载阶段需要获取此类的二进制数据流,比如我们要从硬盘中读取一个class文件,那么就可以通过文件输入流来获取类文件的byte[]
然后交给类加载器进行加载
数组类要稍微特殊一点,数组类型本身不会通过类加载器进行加载的
验证阶段
验证阶段相当于是对加载的类进行一次规范校验
验证阶段,首先是文件格式的验证:
是否魔数为CAFEBABE开头。
主、次版本号是否可以由当前Java虚拟机运行
Class文件各个部分的完整性如何。
…
有关类验证的详细过程,可以参考《深入理解Java虚拟机 第三版》268页。
准备阶段
这个阶段会为类变量分配内存
并为一些字段设定初始值
解析阶段
此阶段是将常量池内的符号引用替换为直接引用的过程
也就是说,到这个时候,所有引用变量的指向都是已经切切实实地指向了内存中的对象了。
到这里,链接过程就结束了
最后就是真正的初始化阶段了,从这里开始,类中的Java代码部分,才会开始执行
全部完成之后,我们的类就算是加载完成了。
Java提供了类加载器,以便我们自己可以更好地控制类加载
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。
比如,我们先创建一个Test类用于测试:
package com.test;
public class Test {
}
接着我们自己实现一个ClassLoader来加载我们的Test类,同时使用官方默认的类加载器来加载:
public class Main {
public static void main(String[] args) throws ReflectiveOperationException {
Class<?> testClass1 = Main.class.getClassLoader().loadClass("com.test.Test");
CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> testClass2 = customClassLoader.loadClass("com.test.Test");
//看看两个类的类加载器是不是同一个
System.out.println(testClass1.getClassLoader());
System.out.println(testClass2.getClassLoader());
//看看两个类是不是长得一模一样
System.out.println(testClass1);
System.out.println(testClass2);
//两个类是同一个吗?
System.out.println(testClass1 == testClass2);
//能成功实现类型转换吗?
Test test = (Test) testClass2.newInstance();
}
static class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try (FileInputStream stream = new FileInputStream("./target/classes/"+name.replace(".", "/")+".class")){
byte[] data = new byte[stream.available()];
stream.read(data);
if(data.length == 0) return super.loadClass(name);
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
return super.loadClass(name);
}
}
}
}
结果:
通过结果我们发现,即使两个类是同一个Class文件加载的,只要类加载器不同,那么这两个类就是不同的两个类。
实际上,JDK内部提供的类加载器一共有三个
**思考:**既然说Class对象和加载的类唯一对应,那如果我们手动创建一个与JDK包名一样,同时类名也保持一致,JVM会加载这个类吗?
package java.lang;
public class String { //JDK提供的String类也是
public static void main(String[] args) {
System.out.println("我姓,我叫nb");
}
}
我们发现,会出现以下报错:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
但是我们明明在自己写的String类中定义了main方法啊,为什么会找不到此方法呢?实际上这是ClassLoader的双亲委派机制
在保护Java程序的正常运行:
实际上类最开始是由BootstarpClassLoader进行加载
而我们自己编写的类实际上是AppClassLoader加载的
因此我们自己编写的同名包同名类不会被加载,而实际要去启动的是真正的String类,也就自然找不到main
方法了。
public class Main {
public static void main(String[] args) {
System.out.println(Main.class.getClassLoader()); //查看当前类的类加载器
System.out.println(Main.class.getClassLoader().getParent()); //父加载器
System.out.println(Main.class.getClassLoader().getParent().getParent()); //爷爷加载器
System.out.println(String.class.getClassLoader()); //String类的加载器
}
}
既然通过ClassLoader就可以加载类,那么我们可以自己手动将class文件加载到JVM中吗?先写好我们定义的类:
package com.test;
public class Test {
public String text;
public void test(String str){
System.out.println(text+" > 我是测试方法!"+str);
}
}
通过javac命令,手动编译一个.class文件:
nagocoler@NagodeMacBook-Pro HelloWorld % javac src/main/java/com/test/Test.java
编译后,得到一个class文件,我们把它放到根目录下,然后编写一个我们自己的ClassLoader,因为普通的ClassLoader无法加载二进制文件,因此我们编写一个自定义的来让它支持:
//定义一个自己的ClassLoader
static class MyClassLoader extends ClassLoader{
public Class<?> defineClass(String name, byte[] b){
return defineClass(name, b, 0, b.length); //调用protected方法,支持载入外部class文件
}
}
public static void main(String[] args) throws IOException {
MyClassLoader classLoader = new MyClassLoader();
FileInputStream stream = new FileInputStream("Test.class");
byte[] bytes = new byte[stream.available()];
stream.read(bytes);
Class<?> clazz = classLoader.defineClass("com.test.Test", bytes); //类名必须和我们定义的保持一致
System.out.println(clazz.getName()); //成功加载外部class文件
}
现在,我们就将此class文件读取并解析为Class了,现在我们就可以对此类进行操作了(注意,我们无法在代码中直接使用此类型,因为它是我们直接加载的),我们来试试看创建一个此类的对象并调用其方法:
try {
Object obj = clazz.newInstance();
Method method = clazz.getMethod("test", String.class); //获取我们定义的test(String str)方法
method.invoke(obj, "哥们这瓜多少钱一斤?");
}catch (Exception e){
e.printStackTrace();
}
我们来试试看修改成员字段之后,再来调用此方法:
try {
Object obj = clazz.newInstance();
Field field = clazz.getField("text"); //获取成员变量 String text;
field.set(obj, "华强");
Method method = clazz.getMethod("test", String.class); //获取我们定义的test(String str)方法
method.invoke(obj, "哥们这瓜多少钱一斤?");
}catch (Exception e){
e.printStackTrace();
}
首先我们要知道,Tomcat本身也是一个Java程序
它要做的是去动态加载我们编写的Web应用程序中的类
而要解决以上提到的一些问题,就出现了几个新的类加载器,我们来看看各个加载器的不同之处:
Common ClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Web应用程序访问。
Catalina ClassLoader:Tomcat容器私有的类加载器,加载路径中的class对于Web应用程序不可见。
Shared ClassLoader:各个Web应用程序共享的类加载器,加载路径中的class对于所有Web应用程序可见,但是对于Tomcat容器不可见。
Webapp ClassLoader:各个Web应用程序私有的类加载器,加载路径中的class只对当前Web应用程序可见,每个Web应用程序都有一个自己的类加载器,此加载器可能存在多个实例。
JasperLoader:JSP类加载器,每个JSP文件都有一个自己的类加载器,也就是说,此加载器可能会存在多个实例。
通过这样进行划分,就很好地解决了我们上面所提到的问题
双亲委派机制
(在JavaSE阶段讲解过)