JVM运行程序说明以及类加载机制

与C和C++等语言不同,C和C++是通过编译器直接将代码编译成CPU能理解的代码格式,即机器码,然后执行。

Java为了实现跨平台运行,是将程序编程成Java字节码,将字节码交给JVM来运行,这样做的好处不仅是实现了跨平台,同时JVM还会提供一个Managed Runtime(托管环境),这个东东能够帮助我们处理自动内存管理、GC、数组越界、安全权限等检测,避免我们写这些无关业务逻辑的代码。

 

JVM如何运行字节码:

程序都是从main方法开始运行,然后再调用其他方法,而类信息、常量、静态变量等数据都被放在一个在JVM规范中被称为“方法区”的地方,HotSpot中被称为永久代(PermGen)。

需要注意的是JDK1.8之前,永久代是在JVM中的,但是JDK1.8中,使用元空间(Metaspace)替换了永久代,并且不在虚拟机中了,转而使用的是本地内存。这么做的目的一是防止方法区太小导致内存溢出,而是为了将HotSpot和JRockit融合,因为后者没有永久代。

在调用方法时,JVM会在当前线程的栈中生成一个栈帧,一个方法会对应一个栈帧。

栈帧内部有“局部变量表”用来存放局部变量、操作数栈、返回地址和动态链接。栈帧的大小是提前计算好的,且不要求连续分布。当方法执行完毕之后,进行出栈操作。

HotSpot在执行时,是采用解释执行和即时编译混合执行的方法,前世逐条将字节码编译成机器码然后执行,后者是将所有的字节码都编译完了之后再执行。后者运行速度更快,但是需要等待编译。

HotSpot有多个即时编译器,默认采用分层编译的方法,热点代码首先被Client编译器编译,然后热点中的热点又会被Server编译器编译,理论上Java程序会执行的越来越快,实际嘛…至少暂时没啥感觉,可能因为我水平没有达到那个份上

 

动态链接:

     下文JVM加载类中说到的“解析”,它的目的就是讲符号引用转化为直接引用,不过这个过程叫静态链接。动态链接也是将符号引用转化为直接引用,不过它发生在程序运行的过程中(因为Java是动态编译的,并不是在程序开始运行的时候就把所有要用到的class文件全部加载)。

    举个直观点的例子:

    使用javac编译文件,然后使用javap -v ***.class反编译class文件,

    可以看到有一个Constant pool,里面存放的都是一些引用,比如 #3...#2.#49,这些就叫符号引用,符号引用就是一堆字符串。在程序运行的时候,运行#3这一行时,会根据#2.#49这堆字符串寻找加载对应类中对应的方法,这个过程就叫将符号引用转化成直接引用

 

JVM类加载机制:

JVM的类加载指的是将类的.class文件中的二进制数据读入到内存,将其放在运行时数据区的方法区内,然后再堆区创建一个java.lang.Class对象。Class对象封装了类在方法区内的数据结构,并向程序眼提供了访问这些数据结构的接口。

类加载过程包括加载、验证、准备、解析和初始化,如下所示:

加载:

通过类的全限定名来获取其定义的二进制字节流(一般是.class文件),生成一个Class对象并初始化它的数据结构(类加载器在后续进行说明)。

加载既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)

 

验证:

为了保证JVM的安全进行对Class文件的字节流进行一些验证,细节就不说了。

 

准备:

发生在方法区,为类的静态变量分配内存,并将其初始化为默认值并构造与该类相关联的方法表这个是实现动态绑定的关键。但是如果这个变量同时被final和static修饰,那就会初始化为它定义的值

单例模式中静态内部类实现方法的依据就是这个。

 

解析

把常量池中的符号引用转为直接引用,符号引用就是class文件中的:

CONSTANT_Class_info

CONSTANT_Field_info

CONSTANT_Method_info......

符号引用目标不一定要已经加载到内存中,但是直接引用要求目标一定要在内存中,且能定位到目标(句柄)如果符号引用指向一个未被加载的类或者它的字段或者方法,那么就会触发这个类的加载(但不一定会触发它加载之后的操作)。符号引用在Java编译器生成Class文件的时候,在文件中就已经指定好了。

 

初始化:

真正执行Java程序代码(除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导),主要是对类变量进行初始化。先初始化父类,再初始化子类:

1.声明类变量是指定初始值

2.使用静态代码块为类变量指定初始值

初始化阶段是执行类构造器<clinit>()方法的过程,它是编译器自动生成的,并且会优先执行父类的<clinit>()方法,并且它的线程安全性由JVM来保证。如果类中没有对静态变量赋值也没有静态代码块的话,可以不生成这个方法。

有且仅有5种情况必须立即初始化:(这些情况被称为类的主动引用)

1.程序启动,main()那个类必须先初始化

2.使用new、读取或者设置类的静态字段(被final修饰过的已在准备阶段赋值,不算)或者调用类静态方法的时候

3.如果子类的父类没有初始化,那么先初始化父类

4.使用反射的时候

5.JDK1.7中的动态语言支持会触发初始化,这部分不是特别明白

 

引用类方法但不会初始化类的被称为被动引用,有如下几种:

1.引用final static对象不会导致类的初始化

2.通过子类引用父类的静态字段,父类会初始化,子类不会

3.通过类名获取Class对象不会触发类的初始化

4.通过ClassLoader默认的loadClass方法也不会触发初始化

5.对应对象数组,不会触发初始化,即new [Class***]

......

 

需要注意的是,解析和初始化不一定会按顺序执行,这样做是为了支持Java的动态绑定。动态绑定(重写)是和静态绑定(重载)做区分。静态绑定的方法调用时,实际的引用时将指向具体的目标方法,因为重载方法的区分在编译阶段已经完成;而动态绑定时,JVM会根据调用者的实际类型,指向的是方法表中的内容,JVM会为每个类生产一张表,这张表叫方法表,通过查询这张表来确定实际调用的方法。

(就是JVM进行动态绑定的实现原理,并且JVM会使用到一种叫内联缓存的技术,这是一种加快动态绑定的技术,可以理解为动态绑定的缓存)

 

类加载器:类加载阶段使用(双亲委派模型)

JVM提供了3种类加载器:

启动类加载器

      加载JAVA_HOME/lib目录或者-Xbootclasspath指定路径且被JVM认可的类库

扩展类加载器

      加载Java_HOME/lib/ext或者java.ext.dir指定路径中的所有类库,开发者可直接使用

应用程序类加载器

      通过getSystemClassLoader获取,加载classpath中的类库

用户还可以自定义类加载器,如果没有,那默认使用“应用程序类加载器”

 

类加载器是这么工作的,当一个类加载器收到类加载任务时,会先交给其父加载器完成,只有当父加载器无法完成加载任务时,才会尝试自己加载任务。这种层次模型被称为双亲委派模型。之所以这么做的原因有两个:

1.是为了保证程序的安全运行。比如自己写的类不会被Bootstrap加载,防止核心库被随意篡改

2.避免类的重复加载。父加载器已经加载过了,子类就无需再去加载一遍了

注意:同一个类如果使用不同的加载器加载,那么这两个类是不相等的(即使这两个类来自同一个Class文件)。这里所说的“相等”,包括Class对象的equals()方法,isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。判断类是否相等使用的是类的定义加载器

    需要注意的父加载器不是父类,ExtClassLoader、AppClassLoader都继承自URLCLassLoader。

    还有,调用ExClassLoader的getParent()方法会发现结果是空指针,但是Bootstrap ClassLoader却可以当它的父加载器,因为Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用。

 

怎么判断一个类有没有被加载器加载过呢?

    每一个类在经过ClassLoader的加载之后,在虚拟机中都会有对应的Class实例,如果某个类C被类加载器CL加载,那么CL就被称为C的初始类加载器。JVM为每一个类加载器维护了一个列表,该列表中记录了将该类加载器作为初始类加载器的所有class,在加载了类的时,JVM使用这些列表来判断该类是否已经被加载过了,是否需要首次加载。

在这里插入图片描述
    根据JVM规范的规定,在类加载过程中,所有参与的类加载器,即使没有亲自加载过该类,也都会被标识为该类的初始类加载器,比如java.lang.String首先经过了BrokerDelegateClassLoader类加载器,依次又经过了应用程序类加载器、扩展类加载器、启动类加载器(根类),这些类加载器都是java.lang.String的初始 类加载器,JVM会在每一个类加载器维护的列表中添加该class类型。

    一个类的定义加载器是它引用的其它类的初始加载器。例如如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。都是一些概念而已,初始加载器就是最开始调用loadCLass的类加载器,类的定义加载器就是调用defineClass的加载器。
 

类加载器的命名空间:
每个类加载器有自己的命名空间,命名空间由所有以此加载器为初始加装载器的类组成。不同类加载器的命名空间关系:
1、同一个命名空间内的类是相互可见的,即可以互相访问。
2、父加载器的命名空间对子加载器可见。
3、子加载器的命名空间对父加载器不可见。
4、如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见(用户自定义的两个完全没关系的ClassLoader)。

 

来分析下ClassLoader的源码,看下它是如何加载一个类的:

需要先了解下ClassLoader中几个重要的方法:

    loadclass:判断是否已加载,使用双亲委派模型,请求父加载器,都为空,使用findclass

    findclass:根据名称或位置加载.class字节码,然后使用defineClass

    defineclass:解析定义.class字节流,返回class对象

 

源码流程如下:

    其实这里的c==null,准确的说是父加载器会抛出一个异常,然后子加载器就可以感知到父类加载失败了,最后会调用到自己写的加载器来进行类的加载。

    所以自定义类加载器的时候建议复写findClass方法,不要复写loadClass,这样就可以保证写出来的类是符合双亲委派模型的,不然会破坏双亲委派模型。

  此处如果resolve为true,可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

 

 

自定义ClassLoader:

为什么需要自定义ClassLoader:

    1.假如我们需要的类不在classPath下,需要实现自己的ClassLoader加载指定路径下的文件

    2.有时我们可能是从网络的输入流中读取类,这个时候可能就有一些加密和解密操作,这就需要自己的ClassLoader进行加密解密处理

    3.可以定义类的实现机制,实现类的热部署,,如OSGi中的bundle模块就是通过实现自己的ClassLoader实现的。

    如果自定义的ClassLoader在创建时没有指定parent,那么它的parent默认就是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载class文件。

public class MyClassLoader extends ClassLoader {

    private String classFilePath;


    public MyClassLoader(String classFilePath) {
        this.classFilePath = classFilePath;
    }

    @Override
    protected Class<?> findClass(String name) {
        byte[] classFileData;
        try {
            classFileData = loadClassData(name);
            return defineClass(name, classFileData, 0, classFileData.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 读取Class文件
     *
     * @param name
     * @return
     */
    private byte[] loadClassData(String name) throws FileNotFoundException {
        InputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        byte[] result = null;
        try {
            inputStream = new FileInputStream(new File(this.classFilePath + File.separator + name + ".class"));
            outputStream = new ByteArrayOutputStream();

            int i = 0;
            while ((i = inputStream.read()) != -1) {
                outputStream.write(i);
            }

            result = outputStream.toByteArray();
        } catch (FileNotFoundException e) {
            throw e;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


        return result;
    }


}

 

编译一个Class文件,用于验证MyClassLoader是否可以成功加载:

在H盘下新建一个叫ToLoad.java的类

public class ToLoad {

    static {
        System.out.println("Success loaded!!!");
    }

}

然后使用javac命令编译这个java文件

 

测试下自己写的ClassLoader:

public class MyTest {
    public static void main(String[] args) {
        MyClassLoader myClassLoader2 = new MyClassLoader("H:\\");
        Class c2 = null;
        try {
            c2 = myClassLoader2.loadClass("ToLoad");
            System.out.println("c2 ClassLoader:" + c2.getClassLoader());
            c2.newInstance();
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}

结果如下:

 

 

 

 

线程上下文类加载器:

有这么一段说明:

    Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

    这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含在classpath内。SPI接口中的代码经常需要加载这些具体的实现类。

    那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;而SPI的实现类是由应用程序类加载器(System ClassLoader)来加载的。所以启动类类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型Bootstrap Classloader无法委派AppClassLoader来加载类。

    所以JDK中引入了线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

 

来分析下获取JDBC连接的这段代码,就可以明白什么叫破坏双亲委派模型了:

    调用getConnection()方法进行连接的时候,传入的caller由Reflection.getCallerClass()得到,该方法可获取到调用本方法的class类,例如这里就是class Test。

    这里的调用者是DriverManager.java(位于/lib/rt.jar中),也就是说本来应该是得到BootStrap启动类加载器但是现在获得的却是AppClassLoader,这就违背了双亲委派模型!(默认情况下,Thread.currentThread().getContextClassLoader()获取到的也是应用程序类加载器,这样就可以加载classpath下的东西了)

 

 

ps: jar包加载的顺序

The order in which the JAR files in a directory are enumerated in the expanded class path is not specified and may vary from platform to platform and even from moment to moment on the same machine. A well-constructed application should not depend upon any particular order. If a specific order is required then the JAR files can be enumerated explicitly in the class path.

即同一个目录下,jvm加载jar包顺序是无法保证的,每个系统的都不一样,甚至同一个系统不同的时刻加载都不一样。

 

 

参考:

https://www.cnblogs.com/chenyangyao/p/5303462.html(虚拟机中栈帧的结构)

https://www.cnblogs.com/ityouknow/p/5603287.html(JVM类加载机制)

http://www.importnew.com/25295.html(不会执行类初始化的几种情况)

http://www.cnblogs.com/nyatom/p/9379013.html(JVM方法调用过程)

https://blog.csdn.net/weixin_33940102/article/details/92048847(jar包加载的顺序)

https://stackoverflow.com/questions/219585/including-all-the-jars-in-a-directory-within-the-java-classpath

 

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值