虚拟机类加载机制【Java模块化系统】

在JDK9中引入的Java模块化系统(Java Platform Module System,JPMS)是对Java技术的一次重要升级,关键目标是可配置的封装隔离机制,Java虚拟机对类加载架构也做出了相应的变动调整,才使模块化系统得以顺利地运行。JDK9的模块不仅仅像JAR包那样只是简单的充当代码的容器,除了代码外,Java的模块定义还包含以下内容:

  1. 依赖其他模块的列表。
  2. 导出的包列表,即其他模块可以使用的列表。
  3. 开放的包列表,即其他模块可反射访问模块的列表。
  4. 使用的服务列表。
  5. 提供服务的实现列表。

可配置的封装隔离机制首先要解决JDK9之前基于类路径(ClassPath)来查找依赖的可靠性问题。此前,如果类路径中缺失了运行时依赖的类型,那就只能等程序运行到发生该类型的加载、链接时才会报出运行时异常。而在JDK9之后,如果启动模块化进行封装,模块就可声明对其他模块的显式依赖,这样Java虚拟机就能在启动时验证应用程序开发阶段设定好的依赖关系在运行期是否完备。如果缺失就直接启动失败,从而避免很大一部分由于类型依赖而引发的运行时异常。

封装隔离机制还解决了原来类路径上跨JAR文件的public类型的可访问性问题,JDK9中public类型不再意味着程序的所有地方的代码都可以随意访问到它们,模块提供了更精细的可访问控制,必须明确声明其中哪一些public的类型可以被其它哪一些模块访问,这种访问控制也主要是在类加载过程中完成的。

模块的兼容性

为了可兼容传统的类路径查找机制,JDK9提出了与“类路径”(ClassPath)相对应的“模块路径”(ModulePath)的概念。简单而言,就是某个类库到底是模块还是传统的JAR包,只取决于它存放在哪种路径上。只要放在类路径上的JAR文件,无论其中是否包含模块化信息(是否包含了module-info.class文件),它都会被当做传统的JAR包对待;相应地,只要放在模块路径上的JAR文件,即使没有使用JMOD后缀,甚至其中不含module-info.class文件,它仍然被当做一个模块。

模块化系统按照以下规则来保证使用传统类路径依赖的Java程序可以不经过修改地直接运行在JDK9及以后的Java版本上,这套规则也仍然保证了传统程序可以访问到所有标准类库模板块中导出的包:

  1. JAR文件在类路径的访问规则:所有类路径下的JAR文件及其它资源文件,都被视为自动打包在一个匿名模块(Unnamed Module)里,此模块几乎没任何隔离的,它可看到和使用类路径上所有的包、JDK系统模块中所有的导出包,以及模块路径上所有的模块中导出的包。
  2. 模块在模块路径的访问规则:模块路径下的具名模块(Named Module)只能访问到它依赖定义中列明确依赖的模块和包,匿名模块里所有的内容对具名模块来说都是不可见的,即具名模块看不到传统JAR包的内容。
  3. JAR文件在模块路径的访问规则:如果把一个传统的、不包含模块定义的JAR文件放置在模块路径中,它就会变成一个自动模块(Automatic Module)。尽管不包含module-info.class,但自动模块将默认依赖于整个模块路径中的所有模块,因此可访问到所有模块导出的包,自动模块也默认导出自己所有的包。

除了向后兼容外 ,随着JDK9模块化系统的引入,更值得关注的是它本身面临的模块间的管理和兼容性问题:如果同一个模块发行了多个不同的版本,那只能由开发者在 编译打包时人工选择好正确的版本的模块来保证依赖的正确性。Java模块化系统目前不支持模块定义中加入版本号来管理和约束依赖,本身也不支持多版本号的概念和版本选择功能。

在JDK9时加入Class文件格式的Module属性,里面有module_version_index这样的字段,用户可在编译时使用“javac --module-version”来指定模块版本,在Java类库API中也存在java.lang.module.ModuleDescriptor.Version这样的接口可在运行时获取到模块的版本号。这一切表明Java模块化系统对版本号的支持本可以不局限在编译期。

官方理由是维持一个足够简单的模块化系统,避免技术过于复杂。很大可能是厂家之前互相博弈妥协的结果(刻意给OSGi留出生存空间,导致Java模块化系统不能拥有像OSGi那样支持多版本模块并存、支持运行时热替换、热部署模块的能力,失去一个应用进行模块化的最大驱动)。

如果要在JDK9之后实现这种目的,只能将OSGi和JPMS混合使用,无疑带来更高的复杂度。模块的运行时部署、替换能力没有内置在Java模块化系统和Java虚拟机之中,仍然必须通过类加载器去实现,实在是一个缺憾。


OSGi与JPMS交互.png

其实在Java虚拟机内置的JVMTI接口(java.lang.instrument.Instrumentation)提供了一定程序的运行时修改类的能力(RedefineClass、RetransformClass)。但这种修改能力会受到很多限制,不可能直接用来实现OSGi那样的热替换和多版本并存,用在IntelliJ IDEA、Eclipse这些IDE上做HotSwap(是指IDE编辑方法的代码后不需要重启即可生效)倒是很适合。

模块化下的类加载器

为了保证兼容性,JDK9并没有从根本上动摇三层加载器架构和双亲委派模型,但为了模块化系统的顺利施行,类加载器仍发生了应被注意到的变动,主要是以下部分:

  1. 首选,是扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代,整个JDK都基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD文件),其中的Java类库就已天然地满足了可扩展的需求,自然不在保留\lib\ext目录,此前使用这个目录或java.ext.dirs系统变量来扩展JDK功能的机制已没有存在的价值,用来加载这部分类库的扩展类加载器也完成了它的历史使用。类似的,在新版JDK中也取消了\jre目录,因为随时可以组合构建出程序运行所需的JRE来。如:
jlink -p $JAVA_HOME/jmods --add-modules java.base --output jre
  1. 其次,平台类加载器和应用程序加载器都不在派生自java.net.URLClassLoader,如果有程序直接依赖了中继承关系,或者依赖了URLClassLoader类的特定方法,那代码可能会在JDK9及更高版本的JDK中崩溃。现在启动类加载器、平台类加载器、应用程序类加载器全都继承与jdk.internal.loader.BuiltinClassLoader,在BuiltinClassLoader中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理。
    另外,“BuiltinClassLoader”存在,启动类加载器现在是在Java虚拟机内部和Java类库共同协作实现的类加载器,尽管有了BootClassLoader这样的Java类,但为了与之前的代码保持兼容,所有在获取启动类加载器的场景(如Object.class.getClassLoader())中仍然会返回null来代替,而不会得到BootClassLoader的实例。

  2. 最后,JDK9中虽仍维持三层类加载器和双亲委派的架构,但类加载的委派关系也发生了变动,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责这个模块的加载器完成加载,可看成是双亲委派模型的“第四次”破坏。


    JDK9后的类加载器委派关系.png

Java模块化系统明确规定了三个类加载器负责各自加载的模块(归属关系),如下

  • 启动类加载器负载加载的模块
    java.base ; java.security.sasl;
    java.datatransfer ;java.xml;
    java.desktop ; jdk.httpserver;
    java.instrument ; jdk.internal.vm.ci;
    java.logging ;jdk.management;
    java.management ; jdk.management.agent;
    java.management.rmi;jdk.naming.rmi;
    java.naming;jdk.net;
    java.prefs;jdk.sctp;
    java.rmi;jdk.unsupported;
  • 平台类加载器负责加载的模块
    java.activation;jdk.accessibility;
    java.compiler
    ;jdk.charsets;
    java.corba;jd.crypto.cryptoki;
    java.scripting;jdk.crypto.ec;
    java.se;jdk.dynalink;
    java.se.ee;jdk.incubator.httpclient;
    java.security.jgss;jdk.internal.vm.compiler
    ;
    java.smartcardio;jdk.jsobject;
    java.sql;jdk.localedata;
    java.sql.rowset;jdk.naming.dns;
    java.transaction;jdk.scripting.nashorn;
    java.xml.bind
    ;jdk.security.auth;
    java.xml.crypto;jdk.security.jgss;
    java.xml.ws;jdk.xml.dom;
    java.xml.ws.annotation
    ;jdk.zipfs;
  • 应用程序类加载器负载加载的模块
    jdk.aot;jdk.jdeps;
    jdk.attach;jdk.jdi;
    jdk.compiler;jdk.jdwp.agent;
    jdk.editpad;jdk.jlink;
    jdk.hotspot.agent;jdk.jshell;
    jdk.internal.ed;jdk.jstatd;
    jdk.internal.jvmstat;jdk.pack;
    jdk.internal.le;jdk.policytool;
    jdk.internal.opt;jdk.rmic;
    jdk.jartool;jdk.scripting.nashorn.shell;
    jdk.javadoc;jdk.xml.bind;
    jdk.jcmd;jdk.xml.ws
    ;
    jdk.jconsole;

你可能感兴趣的:(虚拟机类加载机制【Java模块化系统】)