java 类加载,包冲突

JVM类生命周期

  1. 编译器将 Robot.java 编译成字节码文件 Robot.class
  2. ClassLoader 将 Robot.class 转换成 JVM 中的 Class 对象
  3. JVM 使用 Class 对象生成 Robot 实例

类何时被加载

类加载是一个按需的过程。
遇到 new,getstatic,putstatic,invokestatic 这四个字节码指令时,若此时类还没有被初始化, 则会触发类的初始化。

new A(); // new 
String name = A.name; // getstatic
A.name = "aaa"; // putstatic
A.getName(); // invokestatic

ClassLoader

  1. ClassLoader 主要对类的请求提供服务,当 JVM 需要某类时,它根据名称向 ClassLoader 要求这个类,然后由 ClassLoader 返回 这个类的 class 对象。
  2. ClassLoader 负责载入系统的所有 Resources(Class,文件,来自网络的字节流 等)
  3. 每个 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。

类冲突是怎么产生的

  1. 类冲突都是在运行时产生的
  2. 类冲突类型
  • NoClassDefFoundError
    缺少 jar 包或 App 加载了错误版本
  • NoSuchMethodError
    App 加载了错误版本,通常是 App 中不同组件依赖了同一个库的不同版本。
  • ClassCastException
    典型情况:同一个类由不同的加载器加载的,两个类对象互相转化时,就会报此错。只会出现在多加载器场景 ( demo )
  • LinkageError
  1. 如果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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值