类加载
所有人都知道一句名言:一次编译,到处运行
最近研究一下其中的奥秘,主要参考《深入理解Java虚拟机》。
Java语言无关性的基础:虚拟机和 字节码存储格式。将任何程序编译后变为字节码,字节码可以在任何有java虚拟机的机器上运行~~
例如:java编译器将java代码编译为存储字节码的Class文件,class文件扔到java虚拟机中执行。
类文件结构
二进制流,有关Class文件具体的结构可以详细看下《深入理解java虚拟机》第六章,这里就不详细列举了。
注意 :Class文件并非指Class必须存在于具体磁盘中的某个文件,而是指一串二进制的字节流,无论以何种形式存在都可以。war, jar, JSP, 网络读....都可以作为class文件
虚拟机
类加载
虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机使用的Java类型。(类型的加载和连接过程都是在运行时完成的,这样在类加载时会稍微增加性能开销,但是却为Java提供高度的灵活性,动态加载动态连接)
类加载过程
其中加载、验证、准备、初始化和卸载 五个阶段的顺序是确定的,类的加载过程必须按照这种顺序开始。而解析:可以在初始化之后再开始(运行时绑定)。
加载
将二进制字节流按照虚拟机所需格式存储在方法区,并在堆中实例化一个对象作为访问接口
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
- 堆中生成一个代表此类的java.lang.Class对象,作为方法区这些数据的访问入口。
验证
确保Class文件的字节流符合当前虚拟机要求,并且不会危害虚拟机自身安全
准备
正式分配内存,设置类变量初始值的阶段,都在方法区中分配。其中进行内存分配的只包括类变量(被static修饰的变量),不包括实例变量,实例变量会在对象实例化时随对象一起分配在java堆中。其中初始化,零值,不会有赋值操作。
例:public static int value = 123; 准备阶段之后,value=0
特殊情况 public static final int value = 123; 准备之后,123
解析
将符号引用(引用的目标不一定加载到内存中)替换为直接引用(引用的目标必定已经在内存中)
初始化
真正开始执行类中定义的java代码,感兴趣的可以看下clinit。有且只有四种情况必须立即对类进行”初始化“:
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有初始化,则需要触发其初始化。这4条指令的场景:使用new实例化对象、读取一个类的静态字段、设置一个类的静态字段、调用一个类的静态方法。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化。
- 当初始化一个类的时候,如果父类没有进行初始化。
- 虚拟机启动时,需要指定一个要执行的主类,虚拟机会先初始化。
**注意:**除了上述四种情况都不会触发类的初始化。
思考:运行之后输出什么????
对应上面第1条
类加载器
确定类在java虚拟机中的唯一性
- 启动类加载器 bootstrap(负责/lib下的类)
- 其他的类加载器
- 扩展类加载器 extension(负责/lib/ext下的类)
- 应用程序加载器 app
双亲委派模型
如果一个类加载器收到类加载请求,首先不会自己去加载,而是把这个请求委派给父类加载器去完成
保证在java虚拟机中的唯一性,安全
编写与rt.jar类库中已有类重名的java类,可以编译,但无法加载运行,即使用自己定义的类加载器也不会成功。虚拟机会抛异常:SecurityException
实现
java.lang.ClassLoader的loadClass()方法
- 先检查是否已经被加载过
- 没有则调用父加载器的loadClass()方法,父加载器为空则默认使用Bootstrap ClassLoader
- 父类加载失败,抛ClassNotFoundException,调用自己的findClass()进行加载
破坏双亲委派
and
?
例如:父类加载器请求子类加载器去完成类加载的动作(SPI)
例如:代码热替换(HotSwap)、模块热部署(Hot Deployment)
例子 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()方法,完成类的重新载入。
- 创建一个类加载器
- 设置仓库
- 设置类路径
- 设置访问权限
- 启动一个新线程来支持自动重载
内存布局
由此可以继续下一个问题:加载之后,内存是如何分布的
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 方法区
- 堆
问题
加载到虚拟机中类存储在哪里?
破坏双亲委派模型为什么?