分析类的加载机制前,先来抛出一个单例模式思考:
单例模式,最常用的设计模式,线程安全的有五种创建方式:
1.饿汉式,由类加载机制保证线程安全,但是可能会浪费资源???
2.双重校验懒汉式:双重校验保证线程安全,用则初始化,不用则不初始化
3.静态代码块:和懒汉式原理一样,本质也一样
4.静态内部类:通过内部类的类加载机制保证线程安全,非要说缺点就是多加载一个类吧
5.枚举:也是通过类加载机制保证线程安全,但是他的最大特点就是安全,上面四种方式创建单例模式,但是我们可以通过反射机制来破坏单例模式,但是枚举不行,所以安全系数高
但是为什么大家都觉得使用饿汉式会让人觉得不够专业?那就要问问,为什么说可能会浪费资源?究竟哪种情况会浪费资源?有2种情况会浪费资源:
1.如果调用单例类中的静态方法,则如果该类没有被加载需要被加载进来,加载的同时会触发单例模式,创建对象
2.如果通过反射方式创建单例对象,则如果该类没有被加载需要被加载进来,加载的同时会触发单例模式,创建对象
我们来分析一下,先说情况2,我想我们设计单例模式的初衷就是为了让整个进程中只有该类的一个实例吧,那你用反射,也不能说不对哈,不过是不是违背了咱们设计的初衷,所以基本上实际中不会存在(不存在不代表没有,我只能说基本用不到,可以忽略)。再来说情况1,调用单例类中的静态方法,因为静态方法不需要类实例化就可以直接调用,如果此时该类没有被加载进来,则会触发类加载机制,同时创建对象,所以这种原因是对的。如果你的单例模式中没有静态方法,那么这种情况不存在,而且效率是最高的。
好了,开始步入正题,JVM类的加载机制:
类的加载流程:
第一步:加载.class文件到元空间。通过IO流将类文件加载进来,双亲委派机制
第二步:链接
校验:校验字节码文件格式是否符合jvm标准
准备:给静态变量分配内存,赋默认值
解析:将符号引用替换为直接引用,定义的常量符号直接替换为内存地址
第三步:初始化,执行类的clinit方法,会对类变量和静态代码块合并执行赋值
第四步:使用,就是通过解析生成的class来创建对象使用
第五步:卸载,GC回收掉内存分配的空间
通过类的加载流程我们可以知道:
1.类变量和静态代码块是在类加载的过程中就进行了初始化,代码就执行了,而且执行的顺序就是我们写代码的顺序。例如:
class Two {
private static String name = "wang";
static {
name = "zhang";
age = "30";
}
private static String age = "21";
}
name的值为null--wang--zhang 。age的值为 null--30--21;也就说真正的执行代码如下:
class Two {
private static String name=null ;
private static String age=null;
clinit(){
name= "wang";
name="zhang";
age="30";
age="21";
}
}
2.静态常量的为什么效率高,因为在类的加载过程中就已经将符号引用换成了直接引用
3.类的使用,和为类的使用?例如 我们写个属性 A a=null?那么这算使用类了吗?如果A类此时没有被加载呢,会被加载进来吗?答案是,不算使用,类不会被加载进来。如何证明呢?监控类的加载过程就是可以通过静态代码块来监测。可以使类加载的四种情况分别是:new实例、修改和读取类中类变量、使用反射创建对象。
双亲委派机制
加载器分类:
1.引导类加载器:加载程序启动文件的加载器,native层,我们无法获取到
2.系统类加载器:加载java层核心类库的加载器
3.应用类加载器:加载我们自己写的代码
4.自定义加载器:默认自定义加载器都是继承应用类加载器
需要注意一点就是,类的加载器不是类的继承关系,而是上下级关系,所以叫做委派机制
委派流程:
需要加载的类----应用类加载器(内部会保存记录所有已经加载过的所有类)----加载过则直接返回,没有加载过-----系统类加载器是否加载过-----加载过直接返回,没有加载过------引导类类加载器------加载过直接返回,没有加载过,检查是否需要自己加载,如果需要则加载,同时保存到已加载类的集合中,返回;不需要则------交给应用类加载器-------检查是否是自己需要加载的(比如java.lang包下类)------是自己加载,加载类,同时将加载过的类保存起来返回------不是需要自己加载------应用类加载器加载,同时保存到已加载类的集合中。
伪代码逻辑如下:
public Class loadClass() {
//检查自己是否加载过
Class cla = checkIsLoaded();
//加载过,直接返回
if (null != cla) {
return cla;
}
//没有加载过,调用父亲的加载方法
cla = getParentClassLoader().loadClass();
//父亲加载过,直接返回
if (null != cla) {
return cla;
}
//父亲没有加载过,则检查是否需要自己加载
if (checkIsNeedLoad()) {
//是需要自己加载,加载
cla = load();
//将加载的类保存起来,并返回
save();
return cla;
}
//不是自己的职责,返回null
return null;
}
为什么要用双亲委派机制:
首先先说一下如何判断是否是同一个类:首先类的路径要一致,也就是包名要一致,其次就是类的加载器也要一致,两个都一样才可以保证是同一个类。所以说,类加载器类保存了所有他所加载的类的集合,同时类里也保存了是由哪个加载器加载的加载器信息。
1.安全:核心类库可以保证只加载一次,防止被恶意篡改核心类库
2.高效:已经加载过的类,没必要重复加载,效率高
补充知识:
我们都知道类中可以有静态代码块,代码块,上面分析了静态代码块的执行流程,那代码块呢?
class One {
private int params = 0;
{
params = 1;
}
public One() {
params = 2;
}
{
params = 3;
}
public int getParams(){
return params;
}
}
执行结果是:params值为2,上面代码等同于:
class One {
private int params = 0;
public One() {
params = 1;
params = 3;
params = 2;
}
public int getParams(){
return params;
}
}
所以代码块又称为构造器代码块。当然如果父类也有构造器代码块,那么执行的顺序是---父类构造器代码块----构造器代码块----父类构造器---构造器。