美文网首页
类加载的过程

类加载的过程

作者: 每日一坑 | 来源:发表于2019-03-11 17:21 被阅读0次

    https://crowhawk.github.io/2017/08/21/jvm_5/

    上面这个链接和书本的一起看感觉更清晰些,虽然大部分都差不多

    一个类的生命周期是: 加载,连接(验证,准备,解析),初始化,使用,卸载,

    解析有可能在初始化之前也有可能在初始化之后,(为了搭配动态绑定也是运行时绑定),

    何时进行初始化?

    加载是有虚拟机自己决定什么时候开始加载的,但是遇到下列的情况就要马上进行初始化:

    1:遇到 putstatic ,getstatic ,new ,invokestatic的字节码命令时  比如说,new一个实例化对象,调用别的静态方法, 设置或读取一个类的静态变量,(如果是final修饰的字段不算,在编译期已经加入了常量池)

    2.使用反射调用这个类时,如果还没进行初始化,要先初始化

    3:如果这个类触发了初始化了,但是父类还没初始化,要先初始化父类

    4:虚拟机启动时,要先指定一个要执行的主类(带mian方法的),会先初始化这个主类

    5:在动态语言中,如果一个java.lang.invoke.MethodHadle实例的解析结果是REF-gerstatic,REF-putstatic,REF-invokestatic的方法曲柄,这个方法曲柄对应的类没有进行初始化,则需要先触发其初始化

    (通过子类来访问父类的静态字段,只会触发父类的初始化,不会引起子类的初始化)

    加载:

    1:通过全限定名获取这个类的二进制字节流

    1.1从ZIP包读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。

    1.2从网络中获取,这种场景最典型的应用是Applet。

    1.3运行时计算生成,这种场景使用得最多得就是动态代理技术,在java.lang.reflect.Proxy中,是

    用了ProxyGenerator.generateProxyClass的代理类的二进制字节流。

    1.4由其他文件生成,典型场景是JSP应用,即由JSP文件生成对应的Class类。

    1.5从数据库读取,这种场景相对少见,例如有些中间件服务器(如SAPNetweaver)可以选 

    择把程序安装到数据库中来完成程序代码在集群间的分发。

    2:把这个二进制字节流代表的静态存储结构转存为方法区运行时的数据结构

    3:在内存中生成一个class对象,作为方法区这个类的各个数据的访问入口

    连接

    1.验证:是连接的第一步,保证calss文件中二进制字节流没有包含危及虚拟机的信息.

    1.1:文件格式校验  这个校验是基于字节流校验的,后续的三个校验就是在方法区的储存结构进行的,不会再直接操作字节流了,

    1.2:元数据检验:对字节流描述信息进行语义分析,保证其描述符合java语言规范的要求,主要对类的元数据信息进行语义校验

    1.3:字节码校验:通过数据流和控制流分析之后,确定语义是合法的,主要对类的方法体进行校验,保证类在运行时不会有危害虚拟机的操作

    1.4:符号引用校验:对类自身以外的信息(常量池中各个符号引用)进行匹配校验.发生在虚拟机把符号引用 转为直接引用的时候,在连接的第三阶段--解析时发生

    2.准备:

    为类变量分配内存并设置初始值(为0或者为null)的阶段,这些变量都在方法区中.不包括实例对象,实例对象会在对象实例化时随对象一起分配在java堆中.

    如果是final修饰的则没有初始值,直接为属性的那个值

    3:解析:是虚拟机将常量池中的符号引用转为直接引用

    符号引用(Symbol References): 符号引用以一组符号来描述所引用的目标,符号可以是

    任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局

    无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是

    它们能接受的符号引用必须一致,因为符号引用的字面量形式明确定义在Java虚拟机规范的

    Class文件格式中。

    直接引用(Direct References): 直接引用可以是直接目标的指针、相对偏移量或是一个

    能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局有关的,同一个符号引用在不同

    虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经

    在内存中存在

    4:初始化:这完全是虚拟机自己做的.程序员没办法影响到这初始化的过程,初始化阶段即虚拟机执行类构造器<clinit>()方法的过程。

    4.1.<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定,特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问

    4.2.<clinit>()方法与类的构造函数(或者说实例构造器<init>() 方法)不同,不需要显式的调用父类的()方法。虚拟机会自动保证在子类的<clinit>()方法运行之前,父类的<clinit>()方法已经执行结束。因此虚拟机中第一个执行<clinit>()方法的类肯定为java.lang.Object

    4.3.由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作

    4.4<clinit>()方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成<clinit>()方法

    4.5.接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法

    4.6.虚拟机会保证一个类的<clinit>()方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的<clinit>()方法,其它线程都会阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时的操作,就可能造成多个进程阻塞,在实际过程中此种阻塞很隐蔽

    相关文章

      网友评论

          本文标题:类加载的过程

          本文链接:https://www.haomeiwen.com/subject/onwbpqtx.html