java类加载过程包括,加载->验证->准备->解析->初始化->使用->卸载,七个过程,主要说明前面5个过程。
1、加载:此加载千万不要和类加载混淆,此处加载只是类加载的第一个阶段。读取硬盘上编译后的字节码文件到jvm中,并且存储在运行时内存区,会在java堆中生成java.lang.Class对象,后面会作为方法区中该类的各种数据的访问入口。
2、验证:例如文件格式的校验(魔数头0xCAFEBABE开头)、元数据校验、字节码校验、符号引用校验等。
3、准备:给类的静态变量分配内存,赋值默认值。
public static int a = 1;//赋默认值 =0
public static final int a = 1;//存在ConstantValue属性的直接赋值=1
4、解析:将符号引用转化为直接引用,例如把静态方法 main()方法(本身其实就是一个符号,符号引用)替换为指向数据所存内存的指针或句柄等(直接引用),也叫做静态链接过程。动态链接是在程序运行期间完成的将符号引用替换为直接引用。
5、初始化:类中静态变量初始化为实际的值。
public static int a = 12;
接下来重点聊一聊类加载器
类加载到方法区后,主要包括运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
其实java类加载是懒加载,在使用的时候才会加载。
public class MyClass {
static {
System.out.println("---load MyClass---");
}
public int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
MyClass mc = new MyClass();
mc.add(1, 2);
ClassLoader my = mc.getClass().getClassLoader();
ClassLoader parent1 = my.getParent();
ClassLoader parent2 = parent1.getParent();
System.out.println("myclass loader:" + my);
System.out.println("parent1 loader:" + parent1);
System.out.println("parent2 loader:" + parent2);
System.out.println("--- test load ---");
A a = new A();
B b = null;//当 B b=new B() 才会加载
}
}
public class A {
static {
System.out.println("---load A---");
}
public A() {
System.out.println("---init A---");
}
}
public class B {
static {
System.out.println("---load B---");
}
public B() {
System.out.println("---init B---");
}
}
输出:
---load MyClass---
myclass loader:sun.misc.Launcher$AppClassLoader@18b4aac2
parent1 loader:sun.misc.Launcher$ExtClassLoader@4554617c
parent2 loader:null
--- test load ---
---load A---
---init A---
类加载器有三种
启动类加载器(Bootstrap ClassLoader),由c++实现,负责将jre/lib目录中的类库加载到内存中。
扩展类加载器(Extension ClassLoader),由sun.misc.Launcher$ExtClassLoader实现,负责将jre/lib/ext目录中的类库加载到内存中。
应用程序加载器(Application ClassLoader),由sun.misc.Launcher$ApplicationClassLoader实现,也成为系统加载器。
其实还存在用户自定义的加载器,比如tomcat就是自己实现类加载器。
如图1-1所示,就是类的加载顺序。找了两个上级类加载器去加载,如果没有加载再往下加载,也就是委派上级两个父级加载(双亲委派)。
主要流程实现源码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//当前类加载器是否已经加载了该类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//如果当前类加载器的父类不为空,委托父类加载器加载该类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果为空,委托引导类加载器加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//调用URLCLassLoader的findClass方法找到类(classpath)路径下,查找该类并且加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派优点:避免重复加载、避免核心类篡改。比如:网络植入一个java.lang.Integer类,如果通过双亲委派,先是由父类加载器加载,父类肯定会加载,子类不会再加载。
双亲委派缺点:例如像tomcat,如果tomcat发布两个war包程序,里面都引用了***.jar包,但是版本不一样,不可能用两个jvm。如果按照双亲委派势必造成冲突,所以tomcat是自己实现的类加载器,也叫打破双亲委派加载机制。