ClassLoader 隔离性的基石是namespace,证明给你看

简介: ClassLoader 隔离性的基石是namespace,证明给你看

一、背景

昨晚开始发烧、肌肉酸痛、无力,导致昨天的日更都断掉了,今天核酸结果还没出来,不能去公司,而身体的状况也无法支撑在公司坐一天,不知道自己是不是阳了。趴在床上用枕头给身体做好支撑,把今天的日更完成。早些天有个朋友在看到《当月亮守护地球 | SkyWalking Agent 守护你的应用...有它相伴才安逸》时,有对其中一些文字表示有异议。

朋友:在我知识体系中ClassLoader的双亲委派机制是流畅丝滑的,可是看到通过委派执行类加载来保障这种分治能力,进而达到了类资源的隔离性突然就感觉有点陌生和排斥呢?

我:类的命名空间有了解嘛?

朋友:你是说package嘛?

我:我说的是ClassLoader中的 nameSpace

朋友:啥玩意儿?

本篇是对ClassLoader中的namespace做个直观的介绍和验证。这个知识点笔者个人认为很重要,多次帮助笔者解决日常工作中遇到的疑难杂症,如果你尚未认真研究过ClassLoader,但懵懂的认知让你觉得这个应该很简单,那请看下图,若看不懂则表明你可能并不了解ClassLoader中得一些关键逻辑;不贩卖焦虑,不感兴趣则忽略,知道个大概即可,不会它并不影响你做一个优秀的程序员。

2Zmh5D.gif

二、Classloader的分治和委派机制

上下文同步,已了解这部分的读者朋友可直接跳过,进过第三小节。

ClassLoader 是个抽象类,其子类 UrlClassLoader 引入 URL 资源概念,以此方式来约束自己只加载 URL 覆盖范围内的类文件;ExtClassLoaderAppClassLoader 都是 UrlClassLoader 的子类,各自分管的资源路径不同,即给定的 URL 不同则管辖范围不同,并通过委派执行类加载来保障这种分治能力,进而达到了类资源的隔离性。

2Zmh5D.gif

上图是标准的委派机制,总结为2个方面:

  1. 父加载器能加载 父加载器来加载:
  • 自己在加载资源之前,先让父类加载器去加载。父类再找其父类,直到BootStrapClassLoader(它没有父类加载器)。
  • 保证了等级越高,加载的优先权越高
  1. 父加载器不加载 我就来加载(findClass);我加载不了的子加载器来加载:
  • 若父类加载器没有加载成功,才逐级下放这个加载权。
  • 子类加载器不能加载父类加载器能加载的类,如 java.lang.String ,即使用户自己编造一份这个类型,启动类加载器优先将 java.lang.String 加载成功后,应用类加载器就不会再加载用户自己编造的。

下图描述了 SkyWalking Agent 通过自定义的类加载器AgentClassLoader 加载插件中的类,不会对宿主应用中的类产生污染。

2Zmh5D.gif

三、namespace是什么?

每个类加载器都对应一个namespace,汉化叫命名空间(我个人其实更喜欢汉化为名字空间),命名空间由该加载器及所有父类加载器所加载的类组成。这样的介绍很抽象,网络资料中也多是这么几句话,我会从更多的细粒度视角带您进一步了解它。

3.1 同一个命名空间中,类只加载一份

比如AppClassLoader加载程序中编写的类。无论加载多少次,只要是被AppClassLoader加载的,其Class信息的hashcode都是相同的。

3.2 子加载器可见父加载器加载的类

这个更容易举例,核心类库的类由BootStrapClassLoder 加载的,比如String;由AppClassLoader所加载的类,都能使用String对吧?

3.3 父加载器不可见子加载器所加载的类

SPI技术的诞生也是这个原因,什么是SPI:程序运行过程中要用到的类,通过当前类加载器自动加载,加载不到(不在当前类加载器的类资源管辖范围),如果要使用这个类,必须指定一个能够加载这个类的加载器去加载,而怎么获取这个加载器是个问题。

程序都是在线程中执行,那么从线程的上下文中去拿最合理,所以就诞生了线程上下文类加载器,这中场景下加载器就得采用非自动加载,即通过 forName 或者 loadClass 的方式去加载类。

下边通过示例+科技来验证加载不到的情况

示例中有Parent类,Son类。Parent类中有个getSon方法中,会使用到Son类。 编译后,在classpath下,把Son类移到自定义子加载器的资源目录中,让AppClassLoader无法加载。达到我们的运行环境要求:

  1. ParentAppClassLoader加载
  2. Son由自定义的子加载器加载.
  3. 调用ParentgetSon方法的时候要new Son(),这样会触发Son类的加载,但是加载不到Son类,报错信息为:java.lang.NoClassDefFoundError: com/rock/Son
public class Parent {
    private Object son;
    public Object getSon(){
        return new Son();
    }
    public Object setSon(Object son){
        this.son = son;
        return this.son;
    }
}  
public class Son {
}
复制代码
/**
     * 父加载器加载不了,子加载器所加载的类,
     * 父加载器加载Parent
     * 子加载器加载son
     * Parent#getSon 方法里 new Son()对象.//报错,找不到Son的类定义.
     */
    @Test
    public void testParentCanntFindSon(){
        CustomClassLoader01 customClassLoader01 = new CustomClassLoader01(ClassLoader.getSystemClassLoader());
        try {
            Class<?> classParent = customClassLoader01.loadClass("com.rock.Parent");
            System.out.println("classParent:" + classParent.getClassLoader());
            Class<?> classSon = customClassLoader01.loadClass("com.rock.Son");
            System.out.println("classSon:" + classSon.getClassLoader());
            Object objParent = classParent.newInstance();
            Object objSon = classSon.newInstance();
            Method setSon = classParent.getMethod("setSon",Object.class);
            Object resultSon = setSon.invoke(objParent, objSon);
            System.out.println(resultSon.getClass());
            System.out.println(resultSon.getClass().getClassLoader());
            try {
                Method getSon = classParent.getMethod("getSon");
                Object invoke = getSon.invoke(objParent);
                System.out.println(invoke.getClass().getClassLoader());
            }catch (Exception exp){
                //java.lang.NoClassDefFoundError: com/rock/Son
                exp.printStackTrace();
            }
        }catch (Exception exp){
            exp.printStackTrace();
        }
    }
复制代码

3.4 不同命名空间的类互相不可见

这个特征也通过实例来验证一下,自定义类加载器 new 出来两个实例,每个实例各自加载一次Man类,通过newInstance方式实例化出两个对象,对象之间互相调用setFather赋值就会报错。即a空间的不识别b空间的类,即使全限定名相同也不识别。这种特性所导致的坑你踩到过嘛?

public class Man {
    private Man father;
    public void setFather(Object obj){
        father = (Man)obj;
    }
}
复制代码
@Test
    public void testSifferentNamespaceClass(){
        CustomClassLoader01 customClassLoader01 = new CustomClassLoader01(ClassLoader.getSystemClassLoader());
        CustomClassLoader01 customClassLoader02 = new CustomClassLoader01(ClassLoader.getSystemClassLoader());
        try {
            Class<?> aClass1 = customClassLoader01.loadClass("com.rock.Man");
            System.out.println("class1 : " + aClass1.getClassLoader());
            System.out.println("class1 : " + aClass1.);
            Class<?> aClass2 = customClassLoader02.loadClass("com.rock.Man");
            System.out.println("class2 : " + aClass1.getClassLoader());
            System.out.println("class2 : " + aClass2);
            Object man1 = aClass1.newInstance();
            Object man2 = aClass2.newInstance();
            Method setFather = aClass1.getMethod("setFather", Object.class);
            setFather.invoke(man1,man2);
        }catch (Exception exp){
            exp.printStackTrace();
        }
    }
复制代码
class1 : com.rock.classLoader.CustomClassLoader01@1f28c152
class1 : 2006034581
class2 : com.rock.classLoader.CustomClassLoader01@1f28c152
class2 : 488044861
...
Caused by: java.lang.ClassCastException: com.rock.Man cannot be cast to com.rock.Man
  at com.rock.Man.setFather(Man.java:6)
  ... 27 more
复制代码

四、最后说一句

我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。


相关文章
|
存储 Cloud Native Linux
C++ namespace对全局变量屏蔽的工程化意义解读
C++ namespace对全局变量屏蔽的工程化意义解读
|
1月前
|
网络协议 Linux 应用服务中间件
Namespace技术概述
【10月更文挑战第6天】在Linux内核中,为了隔离不同类型的资源,实现了多种namespace,包括UTS(hostname)、User(用户和组)、Mount(文件系统挂载点)、PID(进程ID)和Network(网络协议栈)。常用指令`nsenter`可进入指定的namespace,而`unshare`则创建并加入新的namespace。在内核层面,每个进程的`task_struct`包含这些namespace的信息。
|
1月前
|
Kubernetes 容器 Perl
Namespaces 隔离资源
【10月更文挑战第3天】Namespaces 隔离资源
|
6月前
|
C++
C++中使用namespace关键字定义和访问命名空间的技术性探讨
C++中使用namespace关键字定义和访问命名空间的技术性探讨
46 3
|
6月前
|
Kubernetes 调度 微服务
K8s 生产最佳实践 - 限制 NameSpace 资源用量
K8s 生产最佳实践 - 限制 NameSpace 资源用量
|
算法 C语言 C++
【C++技能树】NameSpace --命名空间的使用
我在这段代码中想使用rand这个变量名字.这是正常的行为.
96 0
|
Linux Shell Docker
为什么构建容器需要 Namespace ?
Namespace 是 Linux 内核的一项功能,该功能对内核资源进行分区,以使一组进程看到一组资源,而另一组进程看到另一组资源。Namespace 的工作方式通过为一组资源和进程设置相同的 Namespace 而起作用,但是这些 Namespace 引用了不同的资源。资源可能存在于多个 Namespace 中。这些资源可以是进程 ID、主机名、用户 ID、文件名、与网络访问相关的名称和进程间通信。
141 0
|
Shell Linux
linux通过namespace技术实现user用户隔离的例子
在Linux中,可以使用namespace技术实现用户隔离。下面是一个简单的示例,演示如何使用namespace隔离用户。 1. 首先,我们可以创建一个新的用户和组。使用以下命令创建一个新用户和组: ```shell sudo useradd -m testuser ``` 这将创建一个名为testuser的新用户,并为其创建一个相应的用户目录。 2. 接下来,我们将在新的用户namespace中启动一个新的bash Shell。运行以下命令: ```shell sudo unshare -U /bin/bash ``` 该命令将创建一个新的用户namespace,并在该names
649 0
|
存储 关系型数据库 MySQL
5分钟明白容器的核心概念namespace
近年来容器大火,也正是因为容器,生生灭掉了一个IT岗位!哥也是被生生的逼上了邪路。
|
存储 缓存 分布式计算
Fluid新魔法:跨namespace共享数据
什么是Fluid?在云上通过云原生架构运行AI、大数据等任务,可以享受计算资源弹性的优势,但同时也会遇到,计算和存储分离架构带来的数据访问延迟和远程拉取数据带宽开销大的挑战。尤其在GPU深度学习训练场景中,迭代式的远程读取大量训练数据方法会严重拖慢GPU计算效率。另一方面,Kubernetes只提供了异构存储服务接入和管理标准接口(CSI,Container Storage Interface),
234 0
Fluid新魔法:跨namespace共享数据