1.我们先通过工具去编写 .java代码。然后通过 javac 编译为 .class字节码文件。
2.类加载器会把 .class字节码文件 加载到 jvm 的工作内存中。
3.接下来jvm 的解析器去执行我们的代码。
类的加载主要过程
加载 -> 连接(验证 - > 准备 -> 解析)-> 初始化 -> 使用 -> 卸载
注意:只有使用到的class类,在当前jvm类加载器中,找不到的时候,才会触发类的加载。
1.创建对象
2.访问静态属性,或者调用静态方法
3.如果初始化一个类的时候,发现他的父类还没有初始化,那么必须先初始化他的父类
4.包含 "main()" 方法的主类,必须先初始化
1.加载
就是把 编译好的.class 文件,根据全限定名称,加载到jvm的方法区中,并且生成一个 java.lang.Class对象,作为方法区这些数据的访问入口(对于HotSpot虚拟机而言,Class 对象比较特殊,虽是对象,但是存放在方法区)。
加载途径:
1.从压缩包中获取。(jar)
2.从网络中获取(Applet)
3.动态代理
4.jsp文件生成对应的class文件
5.数据库读。
这里也要注意一点,如果这个类是数组,有些不同。
数组会由虚拟机进行创建,本身就是不通过类加载进行创建的。
如果是引用数组,那么会递归采用正常的加载过程进行加载这个引用数组的类型组件。
如果是基本类型数组,会标记与引导类加载器关联。
数组的可见性将默认为public。
2.验证
就是根据 java虚拟机的规范,对加载进来的 .class 文件,进行检验,包括 文件格式验证、元数据验证、字节码验证和符号引用验证。比如调用到别的类的私有方法,重写了 final修饰的方法等等,检验合格后,后续才能交给jvm去执行。
3.准备
在准备的阶段中,会给当前类的静态变量(staic 修饰)分配内存空间,对于基本变量会给一个初始值(0 或 null),对于当前类的静态常量,这个时候,会直接进行赋值操作。
tip:
1.访问一个类的静态常量的时候,静态代码块不会执行。
2.对于一般的成员变量是在类实例化的时候,随对象一起分配到堆内存中。
4.解析
指检查类是否引用了其他的类、接口,包括类或接口、字段、类方法、接口方法的解析,就是把符号引用替换为直接引用。
public void gotoWork(){
car.run(); //这段代码在Worker类中的二进制表示为符号引用
}
在Worker类的二进制数据中,包含了一个对Car类的run()方法的符号引用,它由run()方法的全名 和 相关描述符组成。在解析阶段,Java虚拟机 会把这个符号引用替换为一个指针,该指针指向Car类的run()方法在方法区的内存位置,这个指针就是直接引用。
5.初始化
类初始化是类加载的最后一步,除了加载阶段,用户可以通过自定义类加载器参与,其他阶段都完全由虚拟机主导可控制。到了初始化阶段才真正的执行java代码。
在这个阶段,会执行此类中的静态代码块,并执行静态变量赋值操作。
public class Test{
public static String p1 = Test2.P2;
public static String p3 ;
static{
p3 = "p3";
}
}
6.使用
就是正常使用,如创建对象 或者 访问属性、方法。
7.卸载
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就 结束了。
1、该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
2、加载该类的ClassLoader已经被回收。
3、该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
类加载器和双亲委派机制
- 对于任意一个类,都需要由加载它的类加载器和这个类本身来一起确立其在Java虚拟机中的唯一性。
1.启动类加载器 bootstrap ClassLoader 第一层
他主要是负载加载jave安装目录下 “JAVA_HOME/lib”下的核心类。或者被 -Xbootclasspath 参数指定的路径中的,并且是虚拟机识别的(仅按照名称,如 rt.jar 名字不符合的类库,即使放到 lib 目录下也不会重载)。
2.扩展类加载器 Extension ClassLoader 第二层
这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JAVA_HOME/lib/ext 目录下的,或者被java.ext.dirs 系统变量所指定的路径种的所有类库。开发者可以直接使用扩展类加载器。
3.应用加载器 Application ClassLoader 第三层
这个类加载器由sun.misc.launcher$AppClassLoader 实现, 他负责加载用户类路径(classPath)上所指的类库。由于这个类是 getSystemClassLoader方法的返回值,所以也称为 系统类加载器。
主要是负责夹杂 “ClassPath”环境变量所指定的类。其实就是加载我们编写的类。
4.自定义加载器
我们也可以根据需要,自己实现类加载器。比如 可以对字节码进行加密,然后在自定义加载器 进行类加载的时候,解密。
5. 双亲委派机制
当应用类需要加载一个类的时候,会先委派自己的父类加载器物种加载,若父类不存在,则继续向上委派。若最终父类加载器,没能加载到,才会自己去加载。这样做的好处,1
1.安全,避免重新定义核心类,导致服务异常的问题出现。
2.避免重复加载同一个类。
双亲委派模型的破坏者-线程上下文类加载器
在Java应用中存在这个很多服务提供者接口( Service provider interface, SPI), 这些接口允许第三方为他们提供实现,如常见的SPI 有 JDBC、JNDI等,这些SPI的接口属于 Java核心库,一般存在在 rt.jar包中,由 BootStrap类加载,而以来的第三方实现代码的jar ,存放在classPath 下,所以双亲委派的机制下,bootStrap加载器,无法直接去加载。
从jdk 1.2 开始已入了 线程上下文类加载器(contextClassLoader),我们可以通过 java.lang.Thead 类中的
getContextLoader() 和 setContextClassLoader(ClassLoader cl) 方法来获取和设置,如果没有设置线程的上下文加载器,线程将继承其父线程的类加载器,初始线程的上下文类加载器就是系统类加载器(AppClassLoader)。在线程运行的代码中可以通过此类加载器来加载资源。这种方式就破坏了 '双亲委派模型'。
Tomcat 的类加载器是怎么设计破坏双亲委派的?
首先,我们来问个问题:
Tomcat 如果使用默认的类加载机制行不行?
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。
web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。
再看看我们的问题:Tomcat 如果使用默认的类加载机制行不行?
答案是不行的。为什么?我们看,第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。第三个问题和第一个问题一样。我们再看第四个问题,我们想我们要怎么实现jsp文件的热修改(楼主起的名字),jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
Tomcat 如何实现自己独特的类加载机制?
所以,Tomcat 是怎么实现的呢?牛逼的Tomcat团队已经设计好了。我们看看他们的设计图:
我们看到,前面3个类加载和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/、/server/、/shared/(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
好了,至此,我们已经知道了tomcat为什么要这么设计,以及是如何设计的,那么,tomcat 违背了java 推荐的双亲委派模型了吗?答案是:违背了。 我们前面说过:
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。
很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。
我们扩展出一个问题:如果tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类,该怎么办?
看了前面的关于破坏双亲委派模型的内容,我们心里有数了,我们可以使用线程上下文类加载器实现,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。