[讨论] 虚拟机具体实现相关问题

stevenlye 2013-07-26
在网上看到说JVM的parent delegation中的代理顺序是Parent First的,而IBM Web Sphere Portal Server实现的一些类加载器就是Parent Last的,是子加载器首先尝试加载,如果加载失败才会请父加载器,这样做的原因是:假如你期望某个版本log4j被所有应用使用,就把它放在WAS_HOME的库里,WAS启动时会加载它。如果某个应用想使用另外一个版本的log4j,如果使用Parent First,这是无法实现的,因为父加载器里已经加载了log4j内的类。但如果使用Parent Last,负责加载应用的类加载器会优先加载另外一个版本的log4j。

我觉得对于log4j的例子中,如果“负责加载应用的类加载器会优先加载另外一个版本的log4j”,那么父加载器就无法加载log4j了,就不能实现被所有应用使用了呀
RednaxelaFX 2013-07-27
ClassLoader的加载顺序相关的问题通常归类在纯Java层面,而不算在JVM层面。这部分的代码都是纯Java就能解决的,JVM不关心也不干涉。不过ClassLoader的Java部分其实可以看做JVM对上层开的一个口子,或者一个回调,让用户可以自定义代码加载的逻辑,所以把这部分看作Java底层运行时的一个整体倒也没问题。

===============================================

之前那帖里也提到了,在OpenJDK / Oracle JDK里,Java的parent delegation机制并不是在HotSpot VM里实现,而是在纯Java层实现的。具体来说是在java.lang.ClassLoader的loadClass()方法里实现了parent delegation机制框架,涉及3个主要方法:
1、loadClass():虚方法,可覆写
这是主入口,里面带有parent delegation的委派逻辑:
a. 先搜索自己(作为initiating class loader)已加载的类,找到的话直接返回已加载的类,这就是所谓“parent first”顺序;
b. 未找到的话委派给parent尝试加载,如果parent成功加载了则直接返回parent所加载的类;
c. parent加载失败的话尝试自己加载,这里loadClass()方法会调用findClass()方法。

2、findClass():虚方法,可覆写
这是默认parent delegation机制中一个ClassLoader要尝试自己加载一个类的逻辑的入口。java.lang.ClassLoader的findClass()提供的默认实现直接抛出异常,因为它只是个抽象基类,不包含具体加载类的逻辑。实际的实现类在继承ClassLoader时,如果打算遵循parent delegation机制,那么就不需要覆写loadClass()方法,而只要覆写findClass()方法来尝试加载类即可。
findClass()的名字说明了它最常见的、最主要的任务:“找到”传入的参数所指定的名字所对应的类型的“Class文件”,然后把它以byte[]的形式传给defineClass()去完成后续加载。这里所谓“Class文件”只要是符合JVM规范的“Class文件”格式的byte[]即可,不需要真的是从磁盘上的文件读出来的。

3、defineClass():final方法,不可覆写;实际逻辑由JVM直接实现,是写死的。
这是Java层面的ClassLoader向JVM提供一个名字和一个byte[]申请加载一个类的唯一入口。把它声明为final方法保证了上层ClassLoader一定要向底层JVM提供符合JVM规范的Class文件格式的byte[],具体的加载动作不会受到用户代码的干涉,以保证安全。
前面的findClass()方法主要负责“找到”一个包含Class文件内容的byte[],然后要做的事情就是调用defineClass(),交由JVM去完成后面真正的加载工作。

平时我们用的Java应用里,Java系统默认的两个ClassLoader,extension class loader(sun.misc.Launcher.ExtClassLoader)和system class loader(sun.misc.Launcher.AppClassLoader)都没有覆写loadClass()方法,而只覆写了findClass()方法,这就表明它们遵循默认的parent delegation机制。

而如果一个ClassLoader覆写了loadClass()方法,它就可以用任意的顺序去委派给别的ClassLoader尝试加载类,甚至完全不委派都没问题。这样,是否调用findClass()完全是实现类自己的自由。只是最终“找到”了包含Class文件内容的byte[]之后还是要调用defineClass()。

===============================================

但是说到底,最初为啥需要parent delegation机制?

《深入Java虚拟机(第2版)》第3章提到,Java的ClassLoader体系是Java平台安全性的第一道防线。运行时,每个ClassLoader实例都形成一个“命名空间”(namespace)——相同的名字在不同命名空间里有着不同的意思;对应到Java,相同名字的类型如果由两个不同ClassLoader实例所加载,那么它们会被认为是不同的两个类。一定要注意的是这是纯运行时概念,而不存在与静态代码里。

两个不同的类型的实例是相互不兼容的,即便它们的名字相同,或者甚至是从同一个Class文件加载而来的也不行。
假如两个不同的ClassLoader加载出来的类需要交互(相互调用方法、访问字段之类),同名类型不兼容就有问题了。
为了让多个ClassLoader加载的类之间能实现一定程度上的交互,Java平台才会需要parent delegation机制。思路是:让ClassLoader实例之间形成有层次的委派关系,需要在下层ClassLoader共用的类型就交由上层ClassLoader去加载;这样,要共用的类型实际的defining class loader就会是同一个(上层的)ClassLoader,从下层ClassLoader来看加载出来的类型就兼容了。

作为特例,java.*开头的类型被限制为只能由bootstrap class loader加载。具体来说是在defineClass()的实现里会检查要加载的类的名字是不是“java.”开头,如果是的话就拒绝非bootstrap class loader的加载请求。用parent delegation机制来看这很好理解:所有Java核心类都应该由所有Java代码所共享,不允许用户改变核心类的实现,以保证安全。

===============================================

理解了parent delegation机制背后的意图,也就可以知道什么时候它不适用了:假如一个JVM里要运行多个相对独立的“部分”,它们之间本来就不需要交互,或者说只需要在Java核心类(例如java.lang.String、java.util.Map之类)的层面交互,更重要的是要保证它们之间的独立性的话,那就不应该采用默认的parent delegation机制,而需要覆写loadClass()方法,订制符合自己需求的加载逻辑。
“部分”可以是Web应用服务器意义上的“应用”,可以是OSGi意义上的“模块”,没有啥特别的含义。

楼主提到WebSphere Application Server里有些ClassLoader默认使用parent last顺序来加载,目的就是让应用拥有对类加载的控制权,保证应用想要独立加载的类不受到其它应用的影响;同时仍然保持一定的易用性,允许Web应用服务器通过全局配置的方式对某些类提供默认实现,所以虽然是“parent last”,但它仍然会在自己加载不了类的情况下尝试让parent来加载。

stevenlye 写道
我觉得对于log4j的例子中,如果“负责加载应用的类加载器会优先加载另外一个版本的log4j”,那么父加载器就无法加载log4j了,就不能实现被所有应用使用了呀

因为本来“被所有应用使用”就是一种默认的、非强制的行为,主要是为了易用性考虑而提供的机制。
如果是为了保证类的兼容性而强行要所有应用都使用同一个ClassLoader来加载log4j的类(这也是很常见的需求),那就不能依赖“parent last”这种非强制性的做法。
(可惜现实中实际上没啥好办法来“强制”要求别人使用什么ClassLoader来加载指定的类,除非像对java.*那些核心类的加载一样在底层系统里就加上强制的限制,而这在应用层面是做不到的。这类问题主要还是要靠工程上的约定来解决,不要靠写Java代码来尝试解决。)
stevenlye 2013-07-27
RednaxelaFX 写道
但是说到底,最初为啥需要parent delegation机制?

《深入Java虚拟机(第2版)》第3章提到,Java的ClassLoader体系是Java平台安全性的第一道防线。运行时,每个ClassLoader实例都形成一个“命名空间”(namespace)
——

java虚拟机是怎样实现这个“命名空间”的呢?什么情况下会用到这个特点?
RednaxelaFX 2013-07-28
stevenlye 写道
RednaxelaFX 写道
但是说到底,最初为啥需要parent delegation机制?

《深入Java虚拟机(第2版)》第3章提到,Java的ClassLoader体系是Java平台安全性的第一道防线。运行时,每个ClassLoader实例都形成一个“命名空间”(namespace)
——

java虚拟机是怎样实现这个“命名空间”的呢?什么情况下会用到这个特点?

请仔细阅读上面的链接里的内容再看看理解了没有。我觉得《深入Java虚拟机(第2版)》举的例子挺好,不想重复了。

在抽象概念中的JVM里,运行时的一个类由一个二元组来确定:(类名, 类加载器)
在HotSpot VM这个实现里,SystemDictionary记录着所有当前加载的类,它维持的映射关系正是:(类名, 类加载器) -> 类,具体到代码里的名字就是:
JDK7或以下:(symbolOop, oop) -> klassOop
JDK8:(Symbol*, ClassLoaderData*) -> Klass*
(实际上还存着protection domain信息,不过不需要那个信息也可以查询到类)

这样,传入类名还不足以查找到一个类,必须再提供类加载器信息才满足查询条件。这就使类加载器对类名形成了“命名空间”。
stevenlye 2013-07-30
通过父类委托模型加载时,看到有些资料上讲到可见性原则,意思是每个类对加载器的可见性是不一样的,不知道这个是怎么理解的?见链接http://biancheng.dnbcw.info/1000wen/360516.html
它里面的一个截图


编辑:要用这个地址才对:http://dl.iteye.com/upload/picture/pic/126771/4faa3295-dade-3c2e-808a-7f1c13d84f50.gif
RednaxelaFX 2013-07-30
stevenlye 写道
通过父类委托模型加载时,看到有些资料上讲到可见性原则,意思是每个类对加载器的可见性是不一样的,不知道这个是怎么理解的?见链接http://biancheng.dnbcw.info/1000wen/360516.html

其实这里说的“可见性”就是命名空间的概念啦。
stevenlye 2013-07-30
RednaxelaFX 写道
stevenlye 写道
通过父类委托模型加载时,看到有些资料上讲到可见性原则,意思是每个类对加载器的可见性是不一样的,不知道这个是怎么理解的?见链接http://biancheng.dnbcw.info/1000wen/360516.html

其实这里说的“可见性”就是命名空间的概念啦。

我还是有点不太明白,不同的类加载器有不同的命名空间,就拿这张图来说,为什么ClassLoader A的命名空间中只能看到类A,而不能看到类B,而ClassLoader B的命名空间中除了可以看到类B,却还可以看见类A呢?这是不是说ClassLoader之间的关系会影响到命名空间中类的可见性呢?
RednaxelaFX 2013-07-31
stevenlye 写道
其实这里说的“可见性”就是命名空间的概念啦。
我还是有点不太明白,不同的类加载器有不同的命名空间,就拿这张图来说,为什么ClassLoader A的命名空间中只能看到类A,而不能看到类B,而ClassLoader B的命名空间中除了可以看到类B,却还可以看见类A呢?这是不是说ClassLoader之间的关系会影响到命名空间中类的可见性呢?

您对那图的理解有误。图中“可见”指的是“某个类加载器所加载的类”。例如说ClassLoader B可见A、B,意思是在parent delegation机制下,ClassLoader B可以看到ClassLoader A与ClassLoader B所加载的类;换句话说,ClassLoader B所构成的命名空间的一部分是ClassLoader A的命名空间。
miroku 2013-08-21
抛开RednaxelaFX 大人对于classloader的解释,其实楼主的问题在J2EE规范中是有解答的。在J2EE规范中,要求servlet容器在默认情况下是子优先,也就是反向代理。不仅仅是was这样,tomcat和jetty也都是这样的。tomcat具体查看 delegate参数
Global site tag (gtag.js) - Google Analytics