深入理解java虚拟机-JVM高级特性和最佳实现(四)——类加载机制

每篇一叶

前言

上回说到垃圾收集机制和内存分配,这回咱们来了解下虚拟机类加载机制。
“代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步”

基本概念

类加载周期

加载、验证、准备、解析、初始化、使用、卸载


类的生命周期

虚拟机规范中规定有且只有5种情况必须立即对类进行初始化。
1). 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
2). 使用java.lang.reflect包的方法对类进行反射调用
3). 当初始化一个类式,发现其父类还没有初始化,先初始化其父类
4). 虚拟机启动时,用户需要制定一个主类(main),虚拟机会先初始化这个主类
5). 当使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,则这个方法句柄对应的类需要初始化。

我们通过代码来验证下相关信息
a. 被动使用类字段演示
子类调用父类静态变量时

public class SuperClass {
    static{
        System.out.println("SuperClass init ...");
    }
    public  static int value=123;
}
public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init ...");
    }
    
}
public class NoInitialization {
    /**
     * -XX:+TraceClassLoading 查看类加载过程
     * @param args
     */
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

运行结果

SuperClass init ...
123

发现,父类初始化了,至于要不要初始化子类,就要看虚拟机了。

b.通过定义数组引用类

public class NoInitialization {
    /**
     * -XX:+TraceClassLoading 查看类加载过程
     * @param args
     */
    public static void main(String[] args) {
        SuperClass[] sc = new SuperClass[10];
    }
}

发现父类,子类均不初始化

c.访问常量不会导致类初始化

public class SuperClass {
    static{
        System.out.println("SuperClass init ...");
    }
    public  static int value=123;
    public final static String hello="hello,world";
}
public class NoInitialization {
    /**
     * -XX:+TraceClassLoading 查看类加载过程
     * @param args
     */
    public static void main(String[] args) {
        System.out.println(SubClass.hello);
    }
}

运行结果

hello,world

原因是常量会在编译阶段存入调用类的常量池中,本质上没有直接引用到定义常量的类

类的加载过程
  • 加载
    类加载干了什么呢?通过类的全名来获取定义该类的二进制字节流,将字节流代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成代表这个类的java.lang.Class对象作为这个类各种数据的访问入口,这三步。

  • 验证
    验证是连接阶段的第一步。
    java语言之所以是相对安全的语言,是因为使用纯粹的java代码无法实现如访问数组边界以外的数据、将一个对象转型为它并未实现的类型、跳转到不存在的代码行之类的,编译器会帮我们拒绝编译。然而,Class文件并不仅仅是java编译而来的,可以通过别的途径也能实现,如果虚拟机运行时不验证的话,所谓的安全就要打折扣了,因此,虚拟机有了验证作为连接的第一步。

    1. 文件格式验证
      验证字节流是否符合Class文件格式的规范,并能被当前版本虚拟机处理。
    2. 元数据验证
      对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,如,这个类是否有父类,这个类是否继承了不允许被继承的类,如果不是抽象类,是否实现其父类或接口中要求实现的所有方法等
    3. 字节码验证
      最复杂的一个阶段,通过数据流和控制流分析,确定程序语义是合法的符合逻辑的。
    4. 符号引用验证
  • 准备
    准备阶段就是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个时候进行的内存分配仅包括类变量即静态变量,而我们的实例变量时分配在堆中。初始值除了final修饰的外,一般是数据类型的零值。如果是final修饰的将直接赋结果值。

  • 解析
    解析阶段就是虚拟机将常量池内的符号引用替换为直接引用的一个过程。先理解下符号引用和直接引用的概率。
    符号引用:使用一组符号来描述所引用的目标,与虚拟机内存布局无关,引用的目标不一定已经加载到内存,明确定义在java虚拟机规范的Class文件中。
    直接引用:直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。与内存布局相关,一个引用在不同的虚拟机实例翻译过来一般也不同,引用的目标已经在内存中存在。

  • 初始化
    加载类的最后一步。前面的类加载过程除了加载阶段用户通过自定义类加载器参与外,完全是虚拟机主导的,到了初始化阶段才是真正执行类中定义的java代码。
    初始过程是执行类构造器<clinit>()方法的过程。<clinit>()的特点如下

    1. <clinit>()方法是由编译器自动收集类中的所有类变量的复制动作和静态代码块中的语句合并产生。语句是有先后顺序的,如定义在静态代码块之后的变量,静态代码块可以进行赋值,但是不能访问。代码如下
public class FieldResolution {
    static{
         i=8;
        System.out.println(i);
    }
    static int i;
}

这段代码编译时会报Cannot reference a field before it is defined,非法向前引用。而去掉打印语句,发现程序不会报错,编译源码显示static int i=8; 我们可以认为编辑器会第一时间寻找到int j 的变量,在静态代码块中赋值,如果静态代码块后面初始化过的话,会第二次赋值,这样以程序在后面的为准。

  1. 由于父类的<clinit>()方法会先执行,这就意味着父类中定义的静态代码块要优先于子类的变量赋值操作。
  2. <clinit>()对于类或者接口来说不是必需的,如果一个类没有静态代码块也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  3. <clinit>()会被多线程环境下加锁,同步。
public class DeadLoopClass {
    static{
        //如果没有if会报Initializer does not complete normally
        if(true){
            System.out.println(Thread.currentThread().getName()+"init...");
            while(true){
            }
        }
    }
}
class TestDemo{
    public static void main(String[] args) {
        Runnable run = new Runnable() {
            
            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println(Thread.currentThread()+"--start");
                DeadLoopClass deap = new DeadLoopClass();
                System.out.println(Thread.currentThread()+"--over");
            }
        };
        
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t1.start();
        t2.start();
    }
}

运行结果

Thread[Thread-0,5,main]--start
Thread[Thread-1,5,main]--start
Thread-0init...

类加载器

通过一个类的全限定名来获取描述此类的二进制字节流,这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。

public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name)
                    throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if(is == null){
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b,0,b.length);
                } catch (Exception e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        
        Object obj = myLoader.loadClass("com.classloading.ClassLoaderTest");
        System.out.println(obj.getClass());
        System.out.println(obj instanceof com.classloading.ClassLoaderTest);
    }
}

运行结果

class java.lang.Class
false

我们构造了一个简单的类加载器,加载同一个路径下的Class文件,然后和系统应用程序类加载器的去比较,发现是两个独立的类,因此,类加载器不同,类也不同。

  1. 双亲委托模型

从java虚拟机角度来讲,只存在两种类加载器:启动类加载器(Bootstrap ClassLoader),其他类加载器,独立于虚拟机外部,且全部继承自java.lang.ClassLoader。


双亲委托模型

双亲委托模型除了顶部的启动类加载器,其他的都有自己的父加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而都是使用组合关系来复用父加载器的代码。
双亲委托模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每个层次的类加载器都是如此,最终传递到了启动类加载器,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
使用双亲委托机制的好处在于,Java类随着它的类加载器一起具备优先级的层次关系,如Object类,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委托给最顶级的类加载器去加载,这样就不会造成系统中存在多个Object类。

  1. 破坏双亲委托模型
    到目前为止出现过三次大规模的被破坏情况
    a. 双亲委托模型是在JDK1.2之后引入的,ClassLoader在JDK1.0就存在了,为了向前兼容,在JDK1.2后加入了一个findClass()方法,之前用户继承ClassLoader类唯一目的是重写loadClass方法,后来推荐把自己的类加载逻辑放到findClass方法中。
    b. 模型自身缺陷,双亲委托很好的解决了各个类加载器的基础类的统一问题,如果基础类又要调用回用户的代码,这个时候就有问题了。如JNDI服务,JNDI的目的是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath的JNDI接口提供者的代码,但启动类加载器不可能认识这些,为了处理这个问题,引入了线程上下文加载器
    c. 用户对程序动态性的追求而导致。比如热部署啊,代码热替换。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容