一、java类加载机制
前几天在学习tomcat内核的时候,应用到了自定义加载类。其实这里是java的难点和重点。今天,我们就从java的类加载机制出发,从顶层到底层详细介绍类加载!
1.1 类加载
几乎我看过的所有博客讲解这里是,都是以一段代码开始,这里我也就不例外了:
首先看下面的代码:
public class Example {
private static Example example = new Example();
private static int count1;
private static int count2 = 0;
public Example() {
count1++;
count2++;
}
public static Example getExample() {
return example;
}
}
public class Test {
public static void main(String[] args) {
Example example = Example.getExample();
System.out.println("count1:" + example.count1 + "\n" + "count2:" + example.count2);
}
}
至于运行结果。你们猜到了没有?
这里直接上图
结果有没有出乎意料?别急,在来一个:
package com.tomcat.classLoader.lib1类加载过程.example2;
public class SuperSuperClass {
static {
System.out.println("SSClass");
}
}
package com.tomcat.classLoader.lib1类加载过程.example2;
public class SuperClass extends SuperSuperClass {
static
{
System.out.println("SuperClass init!");
}
public static int value = 123;
public SuperClass()
{
System.out.println("init SuperClass");
}
}
package com.tomcat.classLoader.lib1类加载过程.example2;
public class SubClass extends SuperClass {
static
{
System.out.println("SubClass init");
}
static int a;
public SubClass()
{
System.out.println("init SubClass");
}
}
package com.tomcat.classLoader.lib1类加载过程.test;
import com.tomcat.classLoader.lib1类加载过程.example2.SuperClass;
public class Demo {
public static void main(String[] args) {
System.out.println(SuperClass.value);
}
}
看结果
至于结果,有没有出乎你的意料?分析我就先不说了,继续往下看!
1.2 java虚拟机内存模型
在了解jvm类加载过程之前,我们必须学习java虚拟机的内存模型;
java虚拟机内存模型是java程序运行的基础,为了能使java程序正常运行,jvm虚拟机将其内存数据分为程序计数器,虚拟机栈,本地方法栈,java堆和方法区这五个部分。
我们可以根据受访权限的不同,可以定义上述几个区域为线程共享和线程私有两大类。线程共享是指可以允许被所有线程共享访问的一类内存区,这类区域包括堆内存区,方法区,运行常量池单个内存区。这里简单的说明一下其各个内存区域的功能:程序计数器用于存放下一条运行的指令,虚拟机栈和本地方法栈用于存放函数调用堆栈信息,java堆用于存放java程序运行时所需对象等数据,方法区用于存放程序的类元数据信息。
在类加载过程中,各个阶段就相当于在内存区域内赋值初始化的过程;
1.3 类加载过程
这部分内容在《java虚拟机规范》中有详细的讲解!
加载
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。
链接
将java类的二进制代码合并到jvm的运行状态之中的过程
验证
确保加载的类信息符合jvm规范,没有安全方面的问题
准备
正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配
解析
虚拟机常量池内的符号引用替换为直接引用的过程。(比如String s ="aaa",转化为 s的地址指向“aaa”的地址)。其实就是在内存层面上进行一一映射。
初始化
初始化阶段是执行类构造器方法的过程。类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
读到这里,我相信你一定对本文开始的第一个例子有了自己的理解。
首先我们在Example类中定义了两个静态变量
private static Example example = new Example();
public static int count1;
public static int count2 = 0;
在经历对这个类的加载,验证,准备后,其静态变量就已经被分配了内存空间,并赋初始值,注意,这里的初始值并不是赋值过程,而是将这里变量的值赋值为变量的默认值,这里的int类型的默认值就是0,随着程序执行,我们要去初始化这个类,则调用其构造方法,构造方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。这么,我们就看到了程序执行的结果。由于调用对类中静态方法的调用触发类的初始化,首首先执行静态块中的方法,new触发构造函数的执行,但是程序继续执行,这才到了真正给其变量赋值的阶段,所以又将count2的值改为0;
好的,我们将上面的代码进行修改:
public class Example {
private static int count1;
private static int count2 = 0;
private static Example example = new Example();
public Example() {
count1++;
count2++;
}
public static Example getExample() {
return example;
}
}
public class Test {
public static void main(String[] args) {
Example example = Example.getExample();
System.out.println("count1:" + example.count1 + "\n" + "count2:" + example.count2);
}
}
在观察期运行结果:
count1 = 1
count2 = 1
这里的结果有和刚才不一样,是因为静态块中代码的执行顺序。
类的加载过程分为:主动引用和被动引用。
类的主动引用(一定会发生类的初始化)
--new一个类的对象
--调用类的静态成员(除了final常量)和静态方法
--使用java.lang.reflect包的方法对类进行反射调用
--当初始化一个类,如果其父类没有被初始化,则先初始化他的父类
--当要执行某个程序时,一定先启动main方法所在的类
类的被动引用(不会发生类的初始化)
--当访问一个静态变量时,只有真正生命这个静态变量的类才会被初始化(通过子类引用父类的静态变量,不会导致子类初始化)
--通过数组定义类应用,不会触发此类的初始化 A[] a = new A[10];
--引用常量(final类型)不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
至此本文的第二个例子就有了答案。
1.4 类加载器
类加载器的任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。
在JVM中并不是一次性把所有的文件都加载到,而是一步一步的,按照需要来加载。
比如JVM启动时,会通过不同的类加载器加载不同的类。当用户在自己的代码中,需要某些额外的类时,再通过加载机制加载到JVM中,并且存放一段时间,便于频繁使用。
因此使用哪种类加载器、在什么位置加载类都是JVM中重要的知识。
1.4.1 父类委托机制
JVM类加载器采用 父类委托机制,如下图所示:
JVM类加载包括几种类加载器:
- Bootstrap ClassLoader 引导类加载器
主要负责jdk_home/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作.
- Ext ClassLoader 扩展类加载器
主要负责jdk_home/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包装入工作
- App ClassLoader 应用类加载器(系统类加载器)
主要负责java -classpath/-Djava.class.path所指的目录下的类与jar包装入工作.
- Custom ClassLoader 用户类加载器(java.lang.ClassLoader的子类)
在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性.
当JVM运行过程中,用户需要加载某个类时,会按照下面的步骤(父类委托机制):
1.用户加载自己的类,把请求传递给父类加载器,父类加载器在传递给其父类加载器,一直加载到加载器树的顶层。
2.最顶层的类加载器首先针对其特定的位置进行加载,如果加载不到就传给其子类。
3.如果到底层类都没有加载到,则抛出ClassNotFoundException异常。
通俗的讲,就是某个特定的类加载器在接到类加载请求时,首先将任务委托给父类加载器,依次递归,如果父类可以完成加载任务,就成功返回;只有父类无法完成加载任务时,才自己加载;
摘抄张敦化在一遍文章中用这段代码解释这个过程:
protected synchronized Class loasClass(String name, boolean resolve)
throws ClassNotFoundException{
//first check if the class has already been loaded
Class c = findLoadedClass(name);
if(c == null) {
try {
if(parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass(name);
}
} catch (ClassNotFoundException e) {
//if still not found, then call findClass in order to find the class
c = findClass(name);
}
}
if(resolve) {
resloveClass(c);
}
return c;
}
1.4.2 自定义类加载器
自定义类加载器需要继承抽象类ClassLoader,实现findClass方法,该方法会在loadClass调用的时候被调用,findClass默认会抛出异常。
findClass方法表示根据类名查找类对象
loadClass方法表示根据类名进行双亲委托模型进行类加载并返回类对象
defineClass方法表示跟根据类的字节码转换为类对象
为什么要让父类加载器优先去加载呢?试想如果子类加载器先加载,那么我们可以写一些与java.lang包中基础类同名的类,然后再定义一个子类加载器,这样整个应用使用的基础类就都变成我们自己定义的类了。这样就有很大的安全隐患!
所以自己编写类加载器时,如果没有特殊原因,一定要遵守类加载的双亲委派模型。
二、TOMCAT类加载机制
Tomcat基本遵守了JVM的委派模型,但也在自定义的类加载器中做了细微的调整,以适应Tomcat自身的要求。
当tomcat启动时,会创建几种类加载器:
- Bootstrap 引导类加载器
加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)
- System 系统类加载器
加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。
- Common 通用类加载
加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,比如servlet-api.jar
- webapp 应用类加载器
每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。
想要了解更多的关于Tomcat类加载器的知识,我建议大家去看看Tomcat的是实现源码。。。。