目录
概述
类加载子系统负责将我们的class文件加载到JVM的内存模型中,在方法区加入他的类信息,在堆中创建对应的java.lang.Class对象。这个类就可以被java程序所使用了。
类加载过程
分为 加载->链接->初始化 三个阶段
- 加载
通过类的全限定名,从文件系统(或者网络等其他方式)加载Class文件,将这些字节流的文件信息,转化为运行时的数据结构,生成java.lang.Class对象。作为方法区这个类的各种数据的访问入口。
加载的类信息放置在方法区,除了类信息,方法区还会存放运行时常量池(其实就是Class文件的常量池)信息,可能包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池映射)
- 链接:
验证-> 准备 -> 解析 三个阶段
验证(verify)
字节码合法性:格式,元数据,字节码,引用符号。
准备(prepare)
为类变量分配内存以及为静态变量的赋默认初始值(即0值) 不包含final + static修饰的,final修饰的在编译阶段就已经赋值,所以准备阶段会显示初始化,而不是赋默认值。不会为实例变量分配初始化。
解析(Resolve)
将常量池引用(字面量形式)创建为直接引用
主要针对常量池中的Constant_Class_info,Constant_Fieldref_info,Constant_methodref_info,对接口,类方法,接口方法,方法类型等,对进行解析,将class文件中的这些字面量信息生成直接引用。
invokevirtual的处理。生成虚方法表,便于函数重写时知道调用哪个类的方法。
事实上解析往往伴随实例化完成的。
- 初始化:
执行clinit (就是对类的static区的代码运行)
虚拟机必须保证同一个类的clinit是被加锁的。
补充:类和对象的创建和赋值顺序
1、父类和子类的 final static 的基本数据类型赋值(准备阶段) ->
父类static执行或static{}(谁在按顺序执行) -> 执行 子类static执行
2、子类对象开始构造 -> 父类对象开始构造 -> 父类对象属性赋值 -> 子类对象属性赋值。
类加载器
主要有
启动类加载器(bootstrap ClassLoader),扩展类加载器(extClassLoader),应用类加载器(SystemClassLoader/AppClassLoader)
他们并非继承关系,这里说的parent 是 子类加载器会有一个成员变量叫parent的。比如AppClassLoader 的 parent属性中放的是ExtClassLoader。
分为两类:
引导类加载器(由C和C++编写的)
自定义类加载器(所有派生于ClassLoader的都是自定义加载器,比如extClassLoader,AppClassLoader)
代码案例:拿到ClassLoader.getSystemClassLoader(); 并打印他们的parent
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
System.out.println(classLoader);
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoader.getParent());
//sun.misc.Launcher$ExtClassLoader@85ede7b
System.out.println(classLoader.getParent().getParent());
//null
System.out.println(ClassLoaderTest.class.getClassLoader());
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(String.class.getClassLoader());
//null
System.out.println(Integer.class.getClassLoader());
//null
System.out.println(ArrayList.class.getClassLoader());
//null
BootStrapClassLoader是获取不到的,并且是C和C++编写的,因此运行结果为null,他只负责加载java核心类库中的类,如String,Integer,ArrayList等。
- 引导类加载器(BootStrapClassLoader /启动类加载器)
C和C++编写
启动java核心类库:jdk的,sun的,resource.jar的
不继承ClassLoader,无父类加载器(肯定的不是java语言编写的)
加载扩展类和应用类加载器,并指定为他们的父类加载器
出于安全BootStrapClassLoader只加载java,javax,sun开头的(并且这些类即时被我们自定义了,JVM也只会加载jdk中的)
- 虚拟机自带类加载器(ExtClassLoader)
在cun.misc.Launcher实现,从java.ext.dir目录中或jdk安装目录jre/lib/ext目录下加载类,如果用户自定义类在此目录下,也会由自定义类加载器加载。
- 虚拟机自带类加载器(AppClassLoader)
在cun.misc.Launcher实现,负责环境变量或者java.class.path下的类库,是系统中默认的类加载器。
可以由ClassLoader.getSystemClassLoader()获取
- 用户自定义类加载器
基本使用上面三种类加载器就够了,但是有些场景是需要的:
1、隔离加载类: 某些框架当中需要使用中间件,中间件和应用模块隔离的,需要将类加载到不同环境当中,确保应用jar包和中间件的不冲突。比如中间件有自己的jar,但是可能会有类的冲突。很多主流框架都会自定义类加载器。
2、修改类的加载方式。
3、扩展加载源,可能考虑从其他来源加载类
4、防止源码泄露,可以对字节码加密。解密的时候可以用自定义类加载器。
- 如何自定义类加载器?
继承ClassLoader或者URLClassLoader
1.2之前可以重写loadClass,之后建议findClass配合defineClass实现。
- 关于ClassLoader
主要接口
获取ClassLoader的方式
clazz.getClassLoader() /Class.forName("aaa/bbb.XXX").geClassLoader;
Thread.currentThread().getContextClassLoader()
ClassLoader.getSystemClassLoader()
DriverManager.getCallderClassLoader()
双亲委派机制
按需加载,需要用到才加载,是一种任务委派模式:
- 如果一个类收到类加载请求,他不会立即加载,而是交给其父类加载器,
- 如果父类加载器还存在父类加载器,则进一步委派,依次递归。一直到引导类加载器。
- 如果父类加载器可以完成类加载则加载返回,否则交给子类。
例子1
如果我们自定义在java.lang下建一个String类,还是会加载核心类库的String防止恶意攻击,因为委托到BootStrapClassLoader的时候会识别到其为java.lang下面的。如果在这个自定义的类下面运行main方法,便找不到这个方法,因为加载的是核心类库下面的String
例子2、
我们实现rt.jar下面的接口,那么接口由BootStrapClassLoader加载,而实现类由AppClassLoader加载。
双亲委派优势
- 避免重复加载(比如有继承时)
- 防止恶意攻击
体现了沙箱安全机制 :将对核心类库的保护,成为沙箱安全机制
补充
判断java中两个Class对象是不是同一个类的条件:
- 类全限类名一样
- ClassLoader是一样的
JVM必须知道一个类是由启动类加载器加载的还是启动类加载加载的,如果一个类是用户类加载器加载的,JVM会将类加载器的引用作为类的一部分信息保存在方法区当中,当解析一个类型到另一个类型引用的时候,JVM则必须保证两个类加载器是相同的。(动态链接)
JVM中对类的使用分为主动和被动:(主要看有没有调用其clinit方法)
主动创建方式:
- 创建一个类的实例
- 访问其静态变量
- 访问其静态方法
- 反射
- 初始化其子类
- 表明为启动类的
1.7提供的动态语言。