一次编译,到处运行

1,071 阅读7分钟

类加载

所有人都知道一句名言:一次编译,到处运行

最近研究一下其中的奥秘,主要参考《深入理解Java虚拟机》。

Java语言无关性的基础:虚拟机字节码存储格式。将任何程序编译后变为字节码,字节码可以在任何有java虚拟机的机器上运行~~

例如:java编译器将java代码编译为存储字节码的Class文件,class文件扔到java虚拟机中执行。

\color{#FFB6C1}{可以看出两个重要的部分:类文件 和 虚拟机, 本文也从这两个入口进行分析。}

类文件结构

二进制流,有关Class文件具体的结构可以详细看下《深入理解java虚拟机》第六章,这里就不详细列举了。

注意 :Class文件并非指Class必须存在于具体磁盘中的某个文件,而是指一串二进制的字节流,无论以何种形式存在都可以。war, jar, JSP, 网络读....都可以作为class文件


虚拟机

类加载

虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机使用的Java类型。(类型的加载和连接过程都是在运行时完成的,这样在类加载时会稍微增加性能开销,但是却为Java提供高度的灵活性,动态加载动态连接)

类加载过程

其中加载、验证、准备、初始化卸载 五个阶段的顺序是确定的,类的加载过程必须按照这种顺序开始。而解析:可以在初始化之后再开始(运行时绑定)。

加载

将二进制字节流按照虚拟机所需格式存储在方法区,并在堆中实例化一个对象作为访问接口

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
  3. 堆中生成一个代表此类的java.lang.Class对象,作为方法区这些数据的访问入口。
验证

确保Class文件的字节流符合当前虚拟机要求,并且不会危害虚拟机自身安全

准备

正式分配内存,设置类变量初始值的阶段,都在方法区中分配。其中进行内存分配的只包括类变量(被static修饰的变量),不包括实例变量,实例变量会在对象实例化时随对象一起分配在java堆中。其中初始化,零值,不会有赋值操作。

例:public static int value = 123; 准备阶段之后,value=0

特殊情况 public static final int value = 123; 准备之后,123

解析

将符号引用(引用的目标不一定加载到内存中)替换为直接引用(引用的目标必定已经在内存中)

初始化

真正开始执行类中定义的java代码,感兴趣的可以看下clinit。有且只有四种情况必须立即对类进行”初始化“:

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有初始化,则需要触发其初始化。这4条指令的场景:使用new实例化对象、读取一个类的静态字段、设置一个类的静态字段、调用一个类的静态方法。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化。
  3. 当初始化一个类的时候,如果父类没有进行初始化。
  4. 虚拟机启动时,需要指定一个要执行的主类,虚拟机会先初始化。

**注意:**除了上述四种情况都不会触发类的初始化。

思考:运行之后输出什么????

对应上面第1条

类加载器

确定类在java虚拟机中的唯一性

  • 启动类加载器 bootstrap(负责/lib下的类)
  • 其他的类加载器
    • 扩展类加载器 extension(负责/lib/ext下的类)
    • 应用程序加载器 app

双亲委派模型

如果一个类加载器收到类加载请求,首先不会自己去加载,而是把这个请求委派给父类加载器去完成

保证在java虚拟机中的唯一性,安全

编写与rt.jar类库中已有类重名的java类,可以编译,但无法加载运行,即使用自己定义的类加载器也不会成功。虚拟机会抛异常:SecurityException

实现

java.lang.ClassLoader的loadClass()方法

  • 先检查是否已经被加载过
  • 没有则调用父加载器的loadClass()方法,父加载器为空则默认使用Bootstrap ClassLoader
  • 父类加载失败,抛ClassNotFoundException,调用自己的findClass()进行加载

破坏双亲委派

\color{#F08080}{why} and \color{#7FFFD4}{how}

\color{#F08080}{双亲委派统一了各个类加载器的基础类,如果基础类要调用用户的代码,要怎么办?}

例如:父类加载器请求子类加载器去完成类加载的动作(SPI) \color{#7FFFD4}{线程上下文类加载器}

\color{#F08080}{追求动态性怎么办?}

例如:代码热替换(HotSwap)、模块热部署(Hot Deployment) \color{#7FFFD4}{OSGi:树状结构发展为网状结构}

例子 Tomcat

需要解决一下几个需求
  • 部署在同一个服务器上的两个Web应用程序使用的Java类库可以相互隔离
  • 部署在同一个服务器上的两个Web应用程序使用的Java类库可以相互共享
  • 服务器保证自身安全不受部署的web影响
  • 支持jsp应用的web服务器,需要hotswap
Tomcat类加载器

一件重要的事情:可以通过继承抽象类java.lang.ClassLoader类编写自己的类加载器。

Tomcat中的载入器不仅仅指类加载器,而是web应用程序载入器

  • CommonClassLoader: /common/* 类库可被Tomcat和所有的Web应用程序共同使用
  • CatalinaClassLoader: /server/* 可被Tomcat使用,对所有Web应用程序都不可见
  • SharedClassLoader: /shared/* 可被所有Web应用程序共同使用,但对Tomcat自己不可见
  • WebappClassLoader: /WebApp/WEB-INF/* 仅可被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见
  • JasperLoader: 加载范围仅仅是这个JSP文件所编译出来的那一个Class,它出现的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的JSP类加载器来实现JSP文件的HotSwap功能

只有指定tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会真正建立CatalinaClassLoader和SharedClassLoader的实例,否则会用到这两个类加载器的地方都会用CommonClassLoader的实例来代替,默认配置文件中是没有设置的

***看图提问:***CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序?

***A:***线程上下文~

Tomcat使用自定义类加载器的原因:

  • 为了在载入类中指定某些规则
    • 在载入web应用程序中需要的servlet类及其相关类时要遵守一些明确的规则。例如,应用程序中的servlet只能引用部署在WEB-INF/classes目录及其子目录下的类。
  • 为了缓存已经载入的类
  • 为了实现类的预载入,方便使用

Loader接口:

  • 载入器必须实现org.apache.catalina.Loader接口,在载入器的实现中,会使用一个自定义类加载器(org.apache.catalina.loader.WebappClassLoader类的一个实例)
  • Tomcat的载入器通常会与一个Context级别的servlet容器相关联,getContainer和setContainer方法用来将载入器与某个servlet容器相关联,如果context容器中的一个或多个类被修改,载入器可以支持对类的自动重载,不需要重启tomcat,Loader接口使用modified()方法来支持类的自动重载。

类自动重载

Reloader接口

WebappClassLoader类

指定一个线程不断调用其类载入器的modified()方法,完成类的重新载入。

  1. 创建一个类加载器
  2. 设置仓库
  3. 设置类路径
  4. 设置访问权限
  5. 启动一个新线程来支持自动重载

内存布局

由此可以继续下一个问题:加载之后,内存是如何分布的

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区

问题

加载到虚拟机中类存储在哪里?

破坏双亲委派模型为什么?