1 类加载机制
java语言从编写完成,到执行,由以下几个过程组成
1) 源码的编译
2) 类的加载
3) 执行
类的加载:
类的生命周期
加载 --> 验证 -->准备 -->解析 -->初始化 -->使用 -->卸载
(附: 解析这个过程,有可能是在初始化之后进行,这是为了支持动态绑定)
连接:验证 -->准备 -->解析
系统要用到一个类,就必须加载这个类(就是把这个类的字节码 .class 读到内存中)
==可以用预加载的机制进行加载
虚拟机启动的时候加载,加载的 JAVA_HOME/jre/lib/rt.jar 里.class
==可以在某个类首次使用的时候加载
1)加载
(1) 把.class文件内的二进制数据读进内存中,放到运行时数据区的方法区中
(2) 然后在堆上创建一个对象 java.lang.Class 用来封装类在方法区内的数据结构
类加载的最终产品就是堆中的 java.lang.Class 对象,它提供了访问方法区中的数据结构的接口
(HostSpot 虚拟机比较特殊,这个对象不是放在堆上,是放在方法区中了)
虚拟机规范并不具体,实现也可能各有不同,比如 第一条,就可能存多种方式
加载 .class 的方式
–从文件系统中直接加载
–从网络上下载
–从.zip,.jar加载
–从专用数据库中提取
–在运行的时候动态生成(典型的是动态代理)
2) 验证
确保.class文件的字节流中包含的信息符合当前java虚拟机的要求
文件格式验证
元数据验证
字节码验证
符号引用验证
3) 准备
为类变量 (静态变量) 分配内存并设置初值,这个内存的分配是发生在方法区中的,
在准备阶段,会给类中的静态成员置初值
class T{
static int age=10;
}
在准备阶段 age的值是多少: 0, 至于age=10 这个动作是发生在 “初始化” 阶段
要注意
(1)准备阶段不会给实例成员进行初始化
(2)用final 修饰的静态成员例外,它会直接赋初值
4) 解析
将常量池中的符号引用替换为直接引用
5) 初始化
是类加载机制中的最后一步,这时候会执行类中的初始化代码,比如上例中的
static int age=10; 这个动就会执行
要注意,父类和子类的初始化顺序 , 会先初始化父类,再初始化子类
java中静态成员的初始化,有两种方式,它们的优先级相同
(1) 声明类变量的时候直接初始化
static int age=10;
(2) 用静态代码块初始化
static {
age=10;
}
初始化的时机
(1) 创建类实例的时候
– 用new关键字创建类对象 new Cat();
– 用反射创建类对象 Class.forName(“com.Cat”).newInstance();
– 通过反序列化的方式创建对象
(2) 调用类的某个静态方法
public class Test {
public static void main(String[] args) {
A.test(); //A中的静态块, test
}
}
class A{
static void test() {
System.out.println("test");
}
static {
System.out.println("A中的静态块");
}
}
(3) 访问某个类或接口中的类变量
public class Test {
public static void main(String[] args) {
//A.country="外国"; 也会引起类的初始化 A中的静态块
System.out.println(A.country);
A中的静态块
中国
}
}
class A{
static {
System.out.println("A中的静态块");
}
static String country="中国";
}
(4) 初始化某个类的子类,会引起父类的初始化
public class Test {
public static void main(String[] args) {
System.out.println(B.age);
//A 中的初始化
//B 中的初始化
//10
}
}
class A{
static {
System.out.println("A 中的初始化");
}
}
class B extends A{
static int age=10;
static {
System.out.println("B 中的初始化");
}
}
(5) 用java.exe 来运行某个类
除了上面所说的情况外,其他的访问方式都不会引起类的初始化,称为被动引用
== 子类引用父类的静态成员不会引起子类的初始化
public class Test {
public static void main(String[] args) {
System.out.println(B.country);
//A中的初始化
//中国
}
}
class A{
static String country="中国" ;
static {
System.out.println("A中的初始化");
}
}
class B extends A{
static {
System.out.println("子类B中的初始化");
}
}
== 通过组组定义引用类,不会引起类的初始化
public class Test {
public static void main(String[] args) {
Cat [] c =new Cat[10]; //不会引起初始化,不会有任何输出
}
}
class Cat{
static {
System.out.println("Cat类初始化了");
}
}
== 引用常量的时候,不会引起类的初始化
public class Test {
public static void main(String[] args) {
System.out.println(Cat.age); //10
}
}
class Cat{
final static int age=10;
static {
System.out.println("Cat类初始化了");
}
}
用 final 修饰某个静态变量的时候,它的值在编译期就确定好了,放在常量池中了
初始化的步骤:
1 如果类还没有加载,则程序要先加载这个类
2 如果该类的直接父类还没有加载,则先加载并初始化该类的直接父类, 依次类推JVM 最先初始化的总是Object类
3 如果类中有初始化语句,则系统依次执行这些初始化语句
//例 下面程序的输出结果
public class Test {
public static void main(String[] args) {
System.out.println(B.country); //因为访问的是 static final类型的成员,所以不会引起任何类的初始化
System.out.println(B.PI); //会引起类的初始化,而且要先初始化父类,再初始化子类
//中国 A中的静态块 B中的静态块 3.14
}
}
class A{
static {
System.out.println("A中的静态块");
}
}
class B extends A{
public static final String country="中国" ;
static {
System.out.println("B中的静态块");
}
public static float PI=3.14F;
}
虚拟机会保证类的初始化在多线程环境下被正确的加锁(同步)
但要注意,初始化只会进行一次,如果有一个线程开始了初始化语句,别的线程就不会再执初始化语句了
==== 2 类装载器
“通过一个类的全限定名来得到这个类的二进制字节流”, 实现这个动作的代码模块称为 “类装载器”
虚拟机设计团把这个动作放到虚拟机外部去实现,目的是便于让应用程序自已决定如何去得到所需要的类
类加载器负责将可能是文件中的,网络中的,数据库的class加载到内存中,并且生一个对应的java.lang.Class 类对象
同一个类不会被再次装载
怎么样才算同一个类? 在java中,用全限定名(包名+类名)作为唯一标识
但是在jvm中,一个类的全限定名是 上面说的全限定名+装载器 为其唯一标识, 也就是说,在java中的同一个类
如果用不同的装载器装载,则生成的class对象认为是不同的
当JVM启动的时候,会由三个类装载器组成一个层结构
1) 启动类的加载器 BootStrap ClassLoader
是嵌在JVM内核中的类加载器,是C++语言编写的(hotspot 虚拟机), 它主要负责加载 JAVA_HOME/jre/lib 下的库
我们没有办法手动调用它
-
扩展类加载器 Extension ClassLoader
它是java语言编写, 它的父加载器是 BootStrap ClassLoader ,它主要负责加载 JAVA_HOME/jre/lib/ext 下的库
或 java.ext.dirs 这个系统变量指定的目录下的库附 得到 java.ext.dirs 系统变量的内容
System.out.println(System.getProperty(“java.ext.dirs”));
D:\Program Files (x86)\Java\jdk1.8.0_151\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext
开发者可以使用 扩展类加载器
开如者可以把类打包成jar文件,放到 java.ext.dirs 指定的目录里或 JAVA_HOME/jre/lib/ext 下,就可以被这个类加载器加载
3) 系统类加载器 App ClassLoader //也有人叫 System ClassLoader
也称为应用程序加载器, 它负责加载应用程序 classpath 下的类 (.jar, .class) , 它的父加载器是 Extension ClassLoader
要注意: 类加载器的体系不是继承关系,而是 委派 体系
public class Test {
public static void main(String[] args) {
//得到当前类的类装载器 是 AppClassLoader
System.out.println(Test.class.getClassLoader()); //sun.misc.Launcher$AppClassLoader@73d16e93
System.out.println(Thread.currentThread().getContextClassLoader()); //sun.misc.Launcher$AppClassLoader@73d16e93
System.out.println(ClassLoader.getSystemClassLoader()); //sun.misc.Launcher$AppClassLoader@73d16e93
//得到前类的装载器的父装载器 是 ExtClassLoader
ClassLoader loader =Test.class.getClassLoader();
System.out.println(loader.getParent()); //sun.misc.Launcher$ExtClassLoader@15db9742
//得到 ExtClassLoader 的父装载器,实际上是 BootStrapClassLoader
System.out.println(loader.getParent().getParent()); //null,因为它不是 java 写的,得不到
}
}
JVM 的类加载机制
== 全盘负责
当一个类加载器装载某个 .class 文件的时候, 该class所依赖或引用的所有的其他的class也将由该类加载器加载
除非显示的使用另一个装载器来载入
== 父类委托
先让父类先试图加载该类, 只有在父类加载器无法加载该类的时候,才尝试从自已的类路径中加载该类
== 缓存机制
缓存机制保有加载过的class都会被缓存,当程序要使用某个class时,类加载器先从缓存区寻找class,
只有缓存区不存在,系统才会去读取这个类对应的二进制数据,并转成 Class 对象,存入缓存
这也是为什么我们修改Class以后,必须重启JVM,程序的修改才有效
双亲委派模型
如果一个类加载器收到了一个类加载的请求,它不会自已去装载这个类,它会把这个请求交给父装载器去完成
每一个类层次的类加载器都是如此,因此,所有的类加载请求都应该传递到最顶层的启动类装载器中,只有父
加载器反馈自已无法完成这个加载请求(在它的搜范围没有找到这个类) 时,子加载器才会自已去加载
委派的好处就是避免某些类被重复加载
例如:
我们编写一个类,名为 Test
1) AppClassLoader 来装载这个类,它会先委托 ExtClassLoader 去装载
2) ExtClassLoader 它会委托 BootStrapClassLoder去完成
3) BootStrapClassLoder 加载失败 (因为它会在 JAVA_HOE/jre/lib 查找,但找不到)
4) ExtClassLoader 尝试加载失败 (它会找 JAVA_HOME/jre/ext 或 java.ext.dirs指定的目录下找,但找不到 )
5) AppClassLoader 谁也指不上了,只好自已装载
可不以自已写一个: java.lang.System 类
可以写,也可以编译通过,但不能用
(1) 根据双亲委派模型, 它会先找到系统中的 java.lang.System 类 (rt.jar 中)
(2) 校验这关过不了, 如果用java 做包为,将出错
==== 3 自定义类装载器
类加载的三种方式
1) 启动应用的时候,是JVM 装载
2) 通过 Class.forName()
3) 通过 ClassLoader.loadClass() 方法去加载
附: Class.forName() 和 ClassLoader.loadClass() 加载类的时候的区别
//用Class.forName() 会起类的初始化
public class Test {
public static void main(String[] args) throws Exception {
Class c=Class.forName("com.Cat"); //会有输出 cat 类实始化了
}
}
class Cat{
static {
System.out.println("cat 类实始化了");
}
}
//用 ClassLoader 装载不会引起初始化
ClassLoader loader=Test.class.getClassLoader();
Class c =loader.loadClass("com.Cat"); //没有任何输出
//Class.forName() 有重载,可以传入参数,指明是不是要进行初始化
ClassLoader loader=Test.class.getClassLoader();
Class c=Class.forName("com.Cat",false,loader);
自定义类装载器
为什么要自定义类装载器
例:主流的web服务器(Tomcat), 都实现了自定义类装载器(都不只一个), 一般来说,一个功能健全的web服务器要解决以下几个问题
(1) 部署在同一个服务器上的多个web应用使用java类库可以互相隔离
(2) 部署在同一个服务器上的多个web应用使用java类库可以共享
(3) 支持执布署(热替换)
热部署就是当应用正在运行时,升级软件就不需要重启应用。就比如我们修改了代码的某一部分,不需要再次启动程序,等启动完毕后再到浏览器刷新。有了热部署,我们只需启动一次程序,当有了修改后,只需刷新就好。如在页面中修改一些代码只需要刷新就可以改变页面)
Tomcat的类装载器结构
BootStrapClassLoder ==> ExtClassLoder ==>AppClassLoader ==>CommonClassLoder
==>CatlinaClassLoder
==>SharedClassLoader ==>WebAppClassLoder ==>JspClassLoader
JDK 中的 ClassLoader 是怎么实现的
public abstract class ClassLoader {
private final ClassLoader parent; //组合,指向的是父类装载器
//这个方法的返回值是 Class 类型
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 {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name); //如果经过上面的折腾,还没有找到,则调用findClass() 方法去找
// 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;
}
}
//它是 protected ,它默认的实现其实就类似空现,因为它期望我们自已去实现
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
从上面的代码我们可以看到
==如果想自已定义类装载器,要不想打破双亲委派机制,我们可以重写 findClass()
==如果想打破双亲委派机制,要重写 loadClass()
//例
1) 创建一个类, 编译,生成字节码文件
package com.highcom;
public class NiceCat{
int age=2;
String catName="咖啡猫" ;
public void speak(){
System.out.println("我是一只猫,我的名字是: "+this.catName +" 我的年龄是: "+ this.age) ;
}
}
2) 自定义类装载器
public class MyClassLoader extends ClassLoader{
//name 默认传过来的是 com.highcom.NiceCat 这样的全类名
protected Class<?> findClass(String name) throws ClassNotFoundException {
String filePath="F:/NiceCat.class";
try {
InputStream in=new FileInputStream(filePath);
byte [] buff=new byte[in.available()];
in.read(buff);
in.close();
Class<?> c= this.defineClass(name,buff,0,buff.length);
return c;
}
catch(Exception ex) {
ex.printStackTrace();
}
return null;
}
}
3) 使用自定义类装载器装载上面的类
public class Test {
public static void main(String[] args) throws Exception {
MyClassLoader loader=new MyClassLoader();
Class<?> c=Class.forName("com.highcom.NiceCat",true,loader);
Object obj =c.newInstance();
System.out.println(obj);
System.out.println(obj.getClass().getClassLoader()); //查看这个对象的类装载器 com.MyClassLoader@6d06d69c
Method m= c.getDeclaredMethod("speak");
m.setAccessible(true);
m.invoke(obj);
}
}