java类的加载机制及加载过程,关于java类加载正确的是
目录
I. Tomcat类加载器架构II。动态代理原理3。本文主要介绍了Tomcat类加载器的体系结构,并在类加载和字节码知识的基础上分析了动态代理的原理。
一、Tomcat类加载器架构
Tomcat有自己定义的类加载器,因为一个功能良好的Web服务器必须解决以下问题:
部署在同一服务器上的两个Web应用程序所使用的Java类库可以相互隔离。这是最基本的要求。两个不同的应用程序可能依赖于同一第三方类库的不同版本,因此不要求每个类库在一个服务器中只能有一个副本。部署在同一服务器上的两个Web应用程序所使用的Java类库可以相互共享。例如,一个用户可能有10个由Spring组织的应用程序部署在同一个服务器上。如果在每个应用的隔离目录下存放10份Spring,那将是极大的资源浪费。这不是浪费磁盘空间,而是类库在使用时不得不加载到服务器内存中的问题。如果类库不能共享,虚拟机的方法区域就容易出现过度扩展的风险。服务器需要确保自己的安全性尽可能不受部署的Web应用程序的影响。出于安全原因,服务器使用的类库应该独立于应用程序的类库。支持JSP应用程序的Web服务器十有八九需要支持HotSwap功能。我们知道JSP文件最终会被编译成Java类文件,然后才能被虚拟机执行。所谓hotswap就是用新的代码替换加载的类的内容。由于上述问题,在部署Web应用时,单一的类路径是无法满足要求的,所以各种Web服务器都不约而同地提供了几种意义不同的类路径路径,供用户存储第三方类库。这些路径通常被命名为“库”或“类”。放在不同路径下的类库,访问范围和服务对象都不一样。通常,每个目录都会有一个相应的自定义类加载器来加载放置在其中的Java类库。
在Tomcat目录结构中,Java类库被放在这四组目录中,并且每组都有独立的含义,即:
放在/common目录中。Tomcat和所有Web应用程序都可以使用类库。放在/server目录中。类库可以被Tomcat使用,对所有的Web应用程序都是不可见的。放在/shared目录中。类库可以被所有的Web应用程序使用,但是对Tomcat本身是不可见的。放在/WebApp/WEB-INF目录中。类库只能由这个Web应用程序使用,对于Tomcat和其他Web应用程序是不可见的。为了支持这种目录结构,并加载和隔离目录中的类库,Tomcat定制了几个类加载器,按照经典的父委托模型实现。关系如下图所示。
后台的三个类加载器是默认提供的,而JDKCommon类加载器、Catalina类加载器(也叫服务器类加载器)、Shared类加载器和Webapp类加载器是Tomcat自己定义的类加载器。
它们分别在/common/、/server/、/shared/*和/WebApp/WEB-INF/*中加载Java类库。WebApp类加载器和JSP类加载器通常有多个实例,每个Web应用对应一个WebApp类加载器,每个JSP文件对应一个JasperLoader类加载器。
根据上图:
可由通用类装入器装入的类可由Catalina类装入器和共享类装入器使用,而可由Catalina类装入器和共享类装入器装入的类是相互隔离的。WebApp类加载器可以使用共享类加载器加载的类,但是WebApp类加载器的每个实例都是相互隔离的。JasperLoader的加载范围只是这个JSP文件编译的类文件,其目的是被丢弃:当服务器检测到JSP文件被修改时,会替换当前JasperLoader的实例,构建新的JSP类加载器,实现JSP文件的HotSwap功能。本例中的类加载结构是Tomcat 6之前的默认类加载器结构,在Tomcat 6和更高版本中,默认的目录结构得到了简化。只有指定了Tomcat/conf/Catalina . properties配置文件的server.loader和share.loader项,才会建立catalina类加载器和共享类加载器的实例,否则,会使用通用类加载器的实例。
Tomcat 6之后,默认情况下将/common、/server和/shared三个目录合并成一个/lib目录是合乎逻辑的。这个目录中的类库相当于上一个/common目录中类库的作用。
那么我不妨问另一个问题让读者思考:我之前提到过一个场景,如果有10个Web应用全部使用
Spring来进行组织和管理的话,可以把Spring 放到Common或Shared目录下让这些程序共享
。Spring要对用户程序的类进行管理,自然要能访问到用 户程序的类
,而用户的程序显然是放在/WebApp/WEB-INF目录中的。那么被Common类加载器或 Shared类加载器加载的Spring如何访问并不在其加载范围内的用户程序呢?
答案:如果按主流的双亲委派机制,显然无法做到让父类加载器加载的类去访问子类加载器加载的类,但使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。spring加载类所用的Classloader是通过Thread.currentThread().getContextClassLoader()来获取的,而当线程创建时会默认setContextClassLoader(AppClassLoader),即线程上下文类加载器被设置为AppClassLoader,spring中始终可以获取到这个AppClassLoader(在Tomcat里就是WebAppClassLoader)子类加载器来加载的bean,以后任何一个线程都可以通过getContextClassLoader()获取到WebAppClassLoader来getbean了。
二、动态代理的原理
字节码生成并不是什么高深的技术,因为JDK里面的Javac命令就是字节码生成技术的老祖 宗,并且Javac也是一个由Java语言写成的程序。
在Java世界里面除了Javac和字 节码类库外,使用到字节码生成的例子比比皆是,如Web服务器中的JSP编译器
,编译时织入的AOP框 架
,还有很常用的动态代理技术
,甚至在使用反射的时候虚拟机都有可能会在运行时生成字节码来提 高执行速度
。我们选择其中相对简单的动态代理技术来讲解字节码生成技术是如何影响程序运作的。
什么是动态代理?
动态代理中所说的动态,是指实 现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系 后,就可以很灵活地重用于不同的应用场景之中。
下面代码演示了一个最简单的动态代理的用法,原始的代码逻辑是打印一句hello world,代 理类的逻辑是在原始类方法执行前打印一句welcome。我们先看一下代码,然后再分析JDK是如何做 到的。
import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class DynamicProxyTest { interface IHello { void sayHello(); } static class Hello implements IHello { @Override public void sayHello() { System.out.println("hello world"); } } static class DynamicProxy implements InvocationHandler { Object originalObj; Object bind(Object originalObj) { this.originalObj = originalObj; return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("welcome"); return method.invoke(originalObj, args); } } public static void main(String[] args) { IHello hello = (IHello) new DynamicProxy().bind(new Hello()); hello.sayHello(); }}
运行结果如下:
在上述代码里,唯一的黑匣子就是Proxy::newProxyInstance()方法,除此之外再没有任何特殊之 处。这个方法返回一个实现了IHello的接口,并且代理了new Hello()实例行为的对象。
newProxyInstance一共传进去三个参数:
loader第一个参数,代表的是被代理类的类加载器interfaces代理类要实现的被代理类接口InvocationHandler代表的是将方法调用分派给的调用处理程序跟踪这个方法的 源码,可以看到程序进行过验证、优化、缓存、同步、生成字节码、显式类加载等操作,前面的步骤 并不是我们关注的重点,这里只分析它最后调用sun.misc.ProxyGenerator::generateProxyClass()方法来完 成生成字节码的动作
。
这个方法会在运行时产生一个描述代理类的字节码byte[]数组。如果想看一看这 个在运行时产生的代理类中写了些什么,可以在main()方法中加入下面这句:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
执行完 可以用idea在debug状态下直接双击shift搜索$Proxy即可找到java文件,如下:
import com.gzl.cn.DynamicProxyTest.IHello;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;final class $Proxy0 extends Proxy implements IHello { private static Method m1; private static Method m3; private static Method m2; private static Method m0; public $Proxy0(InvocationHandler var1) throws { super(var1); }// 此处由于版面原因,省略equals()、hashCode()、toString()3个方法的代码 public final void sayHello() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m3 = Class.forName("com.gzl.cn.DynamicProxyTest$IHello").getMethod("sayHello"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } }}
动态代理的原理:
通过ProxyGenerator::generateProxyClass()生成一个代理类这个代理类的实现代码也很简单,它为传入接口中的每一个方法,以及从java.lang.Object中继承来 的equals()、hashCode()、toString()方法都生成了对应的实现,并且统一调用了InvocationHandler对象的 invoke()方法来实现这些方法的 内容。代码中的super.h就是父类Proxy中保存的InvocationHandler实例变量,而实例变量就是刚刚传入的new Hello()。所以无论调用动态代理的哪一 个方法,实际上都是在执行InvocationHandler::invoke()中的代理逻辑。这个例子中并没有讲到generateProxyClass()方法具体是如何产生代理类$Proxy0.class的字节码 的,大致的生成过程其实就是根据Class文件的格式规范去拼装字节码
,但是在实际开发中,以字节为 单位直接拼装出字节码的应用场合很少见,这种生成方式也只能产生一些高度模板化的代码。
对于用 户的程序代码来说,如果有要大量操作字节码的需求,还是使用封装好的字节码类库比较合适。如果 读者对动态代理的字节码拼装过程确实很感兴趣,可以在OpenJDK的 java.baseshareclassesjavalangreflect目录下找到sun.misc.ProxyGenerator的源码。
三、Java语法糖的改变
在Java世界里,每一次JDK大版本的发布,对Java程 序编写习惯改变最大的,肯定是那些对Java语法做出重大改变的版本。
譬如JDK 5时加入的自动装箱、 泛型、动态注解、枚举、变长参数、遍历循环(foreach循环);譬如JDK 8时加入的Lambda表达式、 Stream API、接口默认方法等。事实上在没有这些语法特性的年代,Java程序也照样能写。 现在问题来了,如何把高版本JDK中编写的代码放到低版本JDK 环境中去部署使用?
为了解决这个问题,一种名为Java逆向移植的工具(Java Backporting Tools)应 运而生,Retrotranslator和Retrolambda是这类工具中的杰出代表。
Retrotranslator的作用是将JDK 5编译出来的Class文件转变为可以在JDK 1.4或1.3上部署的版本
, 它能很好地支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导入这些语法特性, 甚至还可以支持JDK 5中新增的集合改进、并发包及对泛型、注解等的反射操作。
Retrolambda
的作 用与Retrotranslator是类似的,目标是将JDK 8
的Lambda表达式和try-resources语法转变为可以在JDK 5、JDK 6、JDK 7中使用的形式
,同时也对接口默认方法提供了有限度的支持。
什么是语法糖?
在前端编译器层面做的改进。这种改进被称作语法糖。也就是这些语法糖主要是帮助我们这些开发人员减少代码量,但是并没有省略掉,只是交给了javac编译器,来替我们做了转换。
如自动装箱拆箱,实际上就是Javac编 译器在程序中使用到包装对象的地方自动插入了很多Integer.valueOf()、Float.valueOf()之类的代码使用enum关键字定义常量,尽管从 Java语法上看起来与使用class关键字定义类、使用interface关键字定义接口是同一层次的,但实际上这 是由Javac编译器做出来的假象,从字节码的角度来看,枚举仅仅是一个继承于java.lang.Enum、自动生 成了values()和valueOf()方法的普通Java类而已。到此这篇关于深入解析Java类加载的案例与实战的文章就介绍到这了,更多相关Java类加载内容请搜索盛行IT以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。