JVM类生命周期
- 编译器将 Robot.java 编译成字节码文件 Robot.class
- ClassLoader 将 Robot.class 转换成 JVM 中的 Class 对象
- JVM 使用 Class 对象生成 Robot 实例
类何时被加载
类加载是一个按需的过程。
遇到 new,getstatic,putstatic,invokestatic 这四个字节码指令时,若此时类还没有被初始化, 则会触发类的初始化。
new A(); // new
String name = A.name; // getstatic
A.name = "aaa"; // putstatic
A.getName(); // invokestatic
ClassLoader
- ClassLoader 主要对类的请求提供服务,当 JVM 需要某类时,它根据名称向 ClassLoader 要求这个类,然后由 ClassLoader 返回 这个类的 class 对象。
- ClassLoader 负责载入系统的所有 Resources(Class,文件,来自网络的字节流 等)
- 每个 class 都有一个 reference,指向自己的 ClassLoader。Class.getClassLoader()
参考:https://www.cnblogs.com/kabi/p/6124761.html
类加载机制
1. 虚拟机类加载机制
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
2. 加载的步骤
加载阶段
主要完成以下3件事情:
1.通过“类全名”来获取定义此类的二进制字节流
2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构
3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口
验证阶段
这个阶段目的在于确保 Class 文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证:
1.文件格式验证:基于字节流验证,验证字节流是否符合Class文件格式的规范,并且能被当前虚拟机处理。
2.元数据验证:基于方法区的存储结构验证,对字节码描述信息进行语义验证。
3.字节码验证:基于方法区的存储结构验证,进行数据流和控制流的验证。
4.符号引用验证:基于方法区的存储结构验证,发生在解析中,是否可以将符号引用成功解析为直接引用。
准备阶段
仅仅为类变量(即 static 修饰的字段变量)分配内存并且设置该类变量的初始值即零值,这里不包含用 final 修饰的 static,因为 final 在编译的时候就会分配了,同时这里也不会为实例变量分配初始化。类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中。
解析阶段
解析主要就是将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析。这里要注意如果有一个同名字段同时出现在一个类的接口和父类中,那么编译器一般都会拒绝编译。
什么是双亲委派
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
- 每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。
- 只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。
双亲委派有何意义
- 使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存在在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader 进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。
- 相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将混乱。因此,如果开发者尝试编写一个与 rt.jar 类库中重名的 Java 类,可以正常编译,但是永远无法被加载运行。
- 参考:https://www.imooc.com/article/34493
类是如何唯一确定的
全限定名 + ClassLoader 名唯一确定一个类。
不同的 ClassLoader 加载同一个 jar 包,是不同的类。
如何写一个最简ClassLoader
https://github.com/qianyiwen2019/learn-java/tree/master/hsfdemo/src/main/java/classLoaderDemo
public class TestClassLoader extends ClassLoader {
private String path;
public TestClassLoader(String path) {
this.path = path;
}
@Override
public Class findClass(String name) {
return loadClass(name);
}
@Override
public Class<?> loadClass(String name) {
try {
// 自己不加载自己,TestClassLoader 本身由 AppClassLoader 加载
if (StringUtils.equals(name, this.getClass().getName())) {
return getParent().loadClass(name);
}
// 目的:替换 AppClassLoader 为 TestClassLoader,原本由 AppClassLoader 加载的类(用户自己定义的类)改成由 TestClassLoader 加载
// 其他系统类(如 Object, String) 仍由 ExtClassLoader 和 BootstrapClassLoader 加载
// 默认情况下,当前 classLoader 的 parent 是 AppClassLoader,
// 而当前 classLoader 的 parent 的 parent 才是 ExtClassLoader
return getParent().getParent().loadClass(name);
} catch (Exception e) {
System.out.println("class " + name + " is not laoded by parent");
}
byte[] b = null;
try {
b = loadClassData(name);
} catch (IOException e) {
e.printStackTrace();
}
Class clazz = defineClass(name, b, 0, b.length);
return clazz;
}
private byte[] loadClassData(String name) throws IOException {
String tmpPath = "";
for (String tmp: name.split("\\.")) {
tmpPath += "/" + tmp;
}
String namePath = path + tmpPath + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(namePath));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
in.close();
out.close();
}
return out.toByteArray();
}
}
new 一个对象时,用的哪个 classloader?
如果在 A 类的实例 instA 中
B instB = new B();
则 instB 的 classloader 为 instA 的 classloader。
类冲突是怎么产生的
- 类冲突都是在运行时产生的
- 类冲突类型
- NoClassDefFoundError
缺少 jar 包或 App 加载了错误版本 - NoSuchMethodError
App 加载了错误版本,通常是 App 中不同组件依赖了同一个库的不同版本。 - ClassCastException
典型情况:同一个类由不同的加载器加载的,两个类对象互相转化时,就会报此错。只会出现在多加载器场景 ( demo ) - LinkageError
- 如果App 不同组件依赖同一个库不同版本,maven 怎么确定执行时使用哪个版本?
答:不能确定。
类冲突如何避免
1. exclustion
通过 mvn dependency:treee 查看找到冲突的包 (logger.api) 是被谁引入的然后通过 exclusion 去除 <dependency>
<groupId>com.aaa.bbb.ccc</groupId>
<artifactId>ccc-client</artifactId>
<exclusions>
<exclusion>
<groupId>com.aaa.bbb</groupId>
<artifactId>logger.api</artifactId>
</exclusion>
</exclusions>
</dependency>
maven-shade-plugin
maven-shade-plugin基本功能:
- 将依赖的jar包打包到当前jar包(常规打包是不会将所依赖jar包打进来的)
- 对依赖的jar包进行重命名(用于类的隔离);
Java 工程经常会遇到第三方 Jar 包冲突,使用 maven shade plugin 解决 jar 或类的多版本冲突。 maven-shade-plugin 在打包时,可以将项目中依赖的 jar 包中的一些类文件打包到项目构建生成的 jar 包中,在打包的时候把类重命名。
对于中间件来说,尤其有用,发布出 shade 包,可以让客户不产生冲突。
参考:https://maven.apache.org/plugins/maven-shade-plugin/
其他类隔离容器
如 Pandora