Java类加载器种类:
- bootstrap :负载加载Java_home/lib下的jar(只有被虚拟机认可的基础jar才会被加载),启动类加载器无法被虚拟机直接引用,用户在编写自定义类加载器时,如果需要把请求委派给引导类加载器,直接填null即可
- extensions:负责加载JRE的扩展目录,Java_home/lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
- system:负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
- 自定义类加载器:父类加载器在不指定下是ExtClassLoader。可以使用双亲委托,也可以打破双亲委托。可以使用缓存,也可以跳过缓存实现热部署。
类加载的机制:
- 双亲委托:子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类
- 可见性:子类可以看见父类所加载的类,父类无法看见子类加载的类
- 全盘负责:当一个ClassLoader装载一个类时,除非显示地使用另一个ClassLoader,则该类所依赖及引用的类也由这个CladdLoader载入(也会使用委托机制,但不能委托子类)。
- 单一:一个类只会被一个类加载器加载一次
类加载器loadClass源码解析:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 对类名加锁 保证这个类不会同时被加载
synchronized (getClassLoadingLock(name)) {
// 检测是否曾经加载过此类
Class<?> c = findLoadedClass(name);
if (c == null) { // 没有加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果parent为空 就使用Bootstrap类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类没有加载到此类 抛异常后接着调用自己的findClass()
}
if (c == null) { // 父类没有找到
// 调用自己的findClass()加载类,如果没有找到类就会抛出异常,那么这一层底用结束
// 子类会继续调用自己的findClass()加载类
long t1 = System.nanoTime();
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;
}
}
线程上下文类加载器:为了满足类隔绝、SPI(Service Provider Interface)服务。eg:当两个插件使用不同版本的同名类时(tomcat部署多个应用),双亲加载只会加载一个进去,无法满足要求。当基础类要调用实现类的代码时候(eg:JNDI服务),双亲模型下基础类由上层加载器加载,无法加载到实现类代码。线程上下文类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
类什么时候会被加载:
- 生成该类对象的时候,会加载该类及该类的所有父类;
- 访问该类的静态成员的时候(通过子类访问继承于父类的静态成员也会加载父类、子类);常量除外,常量在编译期就确认了。
- class.forName("类名");
Java类加载机制的七个阶段,加载、验证、准备、解析、初始化、使用、卸载。
- 加载:把代码数据加载到内存中,创建class对象
- 验证:验证class文件合法
- 准备:类变量分配内存并初始化初始值(static修饰的才是类变量)
- 解析:虚拟机将常量池内的符号引用替换为直接引用的过程
- 初始化:根据语句执行顺序对类对象进行初始化(初始化类的成员变量)
- 使用:执行用户代码
- 卸载:jvm退出。将信息清除
类什么时候会被初始化:
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
类初始化步骤:
- 执行静态变量和静态代码块,加载顺序由编写先后决定。只会执行一次。如果静态变量有赋值操作就执行赋值操作,没有就是默认值。(在准备阶段就赋了初始值)
- 普通成员和普通代码块,加载顺序由编写先后决定,每次都会执行。如果普通变量有赋值操作就执行赋值操作,没有就是默认值。(这个阶段赋值)
- 构造函数。调用就会执行。
父子的顺序:父静态模块-->子静态模块-->父普通模块-->子普通模块(静态块只会执行一次,就是第一次初始化的时候)
代码示例:
public class TestInit {
public static void main(String[] args) {
new TestInit();
}
static int num = 4;
{
num += 3;
System.out.println("b");
}
public int a;
{
System.out.println("c");
}
TestInit() {
System.out.println("d");
}
static {
System.out.println("a");
}
}