一、基本概念
- 类加载机制其实就是将class文件中的数据读入到内存里面,并生成数据访问的入口
- Class文件加载到方法区,堆中生成Class对象。且Class对象指向方法区中的Class文件
- 众所周知,javac的命令可以将java文件变成class文件,其实就是将java文件进行词法、语义的转化,最终生成class文件
二、类加载过程
加载类(加载Class文件)
- 从本地内存中直接加载
- 从zip、jar等归档文件中加载
- 网络中下载
- 将java源文件动态的编译成class文件
- 从加密文件中获取
类加载流程
- 虚拟机将Class文件加载到内存
- JVM对Class文件校验、准备、解析、初始化
- 最终形成JVM可以直接使用的Java类型,即java.lang.Class
- 方法区存放类信息、静态变量、常量
- 堆中存放Class对象
装载(双亲委派模型)
- JVM没有严格规定一定要加载什么类,基本上都是懒加载,即需要什么加载什么。
- 通过类的全限定名称获取类的二进制字节流,加载进JVM。
双亲委派模型
- 注意BootStrap、Extension、App并不是继承关系。“父加载器”不是加载器的加载器,也不是加载器的父类加载器。可以简单粗暴的理解为,这三个加载器P关系都没有。
- BootStrap底层是c++实现的,所以在Java中没有对应的Class,就是个null
- 双亲委派模型的加载流程(从下往上找,从上往下加载)
- 在Custom ClassLoader里面找(缓存),看是否被加载
- 在App ClassLoader中找(缓存),看是否被加载
- 在Extension ClassLoader中找(缓存),看是否被加载
- 在BootStrap ClassLoader中找(缓存),看是否被加载
- 如果从下往上找,能找到,那就不用加载了,因为已经被加载过了啊。
- 如果没找到,那么就从上往下开始加载,如果加载不到那么就报错ClassNotFoundException
- 为什么要使用双亲委派?
安全。假设有人改了java.lang.String类,那么我们也不会加载,我们通过BootStrap ClassLoader加载核心类的 - Custom ClassLoder用户自定义,其实就是继承了ClassLoder类,重写了findClass方法
- 如何打破双亲委派?
- 重写ClassLoder类中的loaderClass方法(其实这样也是热加载,将以前的ClassLoader干掉,然后重新加载就行了)
- 其实重写了loaderClass方法,那么你就可以给你的Class加密,然后解密,这样提高安全性了
- 一般别打破,因为出了问题很难定位解决的。
- 为什么双亲委派要分3层?
- 假设当前只有BootStrap类加载器的话,我写了一个java.lang.String类,那么它就会跟lang包下原有的类进行冲突,你到底用哪一个?并且还具有安全隐患。这个时候我们用分层次的类加载器就能避免这个问题。
- 类加载的三种特性
- 全盘负责:加载当前Class时,会将这个Class所依赖和引用的Class全部由这个加载器所加载。
- 双亲委派
- 缓存机制
测试
public static void main(String[] args) {
System.out.println(String.class.getClassLoader()); // null,是由Bootstrp加载的
System.out.println(sun.awt.HKSCS.class.getClassLoader()); // null,是由Bootstrp加载的
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader()); // sun.misc.Launcher$ExtClassLoader@6d6f6e28
System.out.println(Test.class.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(Test.class.getClassLoader().getClass().getClassLoader()); // null,App的父类加载器Bootstrp
System.out.println(Test.class.getClassLoader().getParent()); // sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(Test.class.getClassLoader().getParent().getParent()); // null
System.out.println(System.getProperty("sun.boot.class.path").replaceAll(";", System.lineSeparator()));
System.out.println(System.getProperty("java.class.path").replaceAll(";", System.lineSeparator()));
System.out.println(System.getProperty("java.ext.dirs").replaceAll(";", System.lineSeparator()));
}
源码
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();
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;
}
}
public class Launcher {
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private static class BootClassPathHolder {
static final URLClassPath bcp;
private BootClassPathHolder() {
}
static {
URL[] var0;
if (Launcher.bootClassPath != null) {
var0 = (URL[])AccessController.doPrivileged(new PrivilegedAction<URL[]>() {
public URL[] run() {
File[] var1 = Launcher.getClassPath(Launcher.bootClassPath);
}
}
}
}
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new AppClassLoader(var1x, var0);
}
});
}
}
static class ExtClassLoader extends URLClassLoader {
public static ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException)var2.getException();
}
}
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
}
}
}
Verification(验证)
- 确保当前Class文件的字节流不会危害到JVM,必须符合JVM规范。
- 如果你牛皮,那么启动时用 -Xverify:none 来取消验证
- 文件格式验证(Class文件开头是不是“ca fe ba be”,版本号是否正确等等)
- 元数据验证(Class文件中的Java语义是否正确)
- 字节码验证(保证类在运行的时候不会危害JVM)
- 符号引用验证
Preparation(准备)
- 静态变量分配分配内存空间,并赋默认值
- 假设public static int a=1;那么a在准备阶段过后的初始值为0,不为1,这时候只是开辟了内存空间,并没有运行java代码,a赋值为1的指令是程序被编译后,存放于类构造器()方法之中,所以a被赋值为1是在初始化阶段才会执行。
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化。
即,private final static int a = 2; 或者 private final int a = 2 在编译的时候就已经确定值了,看类的class文件就能看出来 - 不会为实例变量分配初始化(就是非static的),类变量会分配在方法区中,而实例变量随着对象分配在了堆里面
Resolution(解析)
- 符号引用:一组符号来描述目标,可以是任何字面量。
- 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。与jvm的内存分布有关,同一个符号引用在不同虚拟机实例上翻译出的直接引用一般不一样的,如果有直接引用,那么引用的目标必定存在内存中
- 解析阶段是虚拟机将常量池里面的符号引用替换为直接应用的过程
- 解析动作主要针对类、接口、字段、类方法、接口方法、方法类型、方法句柄、和调用限定符这7类符号引用进行的
- 解析结果会进行缓存,同一个符号引用会有可能被解析多次的,避免重复解析
Initializing(初始化)
- 初始化阶段是执行类构造器()方法的过程
- 先执行构造方法里面代码,在赋值
- 静态变量的赋初始值(调用初始化方法)
- 必须把静态变量定义在静态代码块的前面。因为两个的执行是会根据代码编写的顺序来决定的,顺序搞错了可能会影响你的业务代码。
- 初始化时机?
- 主动引用:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种:
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法反射(如 Class.forName(“com.carl.Test”) )
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类(JvmCaseApplication ),直接使用 java.exe 命令来运行某个主类
- 被动引用
- 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化
- 定义类数组,不会引起类的初始化
- 引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化的)
- 主动引用:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种:
public class Test {
public static void main(String[] args) {
System.out.println(T.count); // --> 2
}
}
class T {
public static T t = new T();
public static int count = 2;
private T () {
count ++;
}
}
卸载
-
在类使用完之后,如果满足下面的情况,类就会被卸载:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了
-
一般情况下启动类加载器加载的类不会被卸载,而我们的其他两种基础类型的类加载器只有在极少数情况下才会被卸载
三、单例模式用volatile
- 其实就是防止指令重排,也就是双重检查的单例需要用volatile
- 不使用Volatile的问题
- 在多线程的时候,A线程走到了2处,此时发生了指令重排(将初始化顺序打乱,比如先赋值了,在分配空间),然后走到了3的时候,其实instance已经不是null了,但是他没有进行初始化,所有的东西都是默认的。
- 在这个时候,B来了,B走这个过程的时候发现instance不是null了,就去使用这个instance了,然后这个instance里面的成员变量都是默认的,如果有引用的话就是null了。