• 深入理解Java虚拟机笔记


    1. Java虚拟机所管理的内存

    2. 对象创建过程

    3. GC收集

    4. HotSpot算法的实现

    5. 垃圾收集器

    6. 对象分配内存与回收细节

    7. 类文件结构

    8. 虚拟机类加载机制

    9.类加载器

    1. Java虚拟机所管理的内存

    JVM在执行java程序的时候,它所管理的内存大致被划分为以下几种数据区域:

    1.程序计数器(Program Counter Register)                                                                                                                    

    •   线程私有,较小的内存空间,生命周期与线程相同。
    •        当前线程所执行的字节码行号指示器。
    •   若正在执行Native方法,这个计数器值为空(Undefined)。
    •   此内存区域是唯一JVM没有规定任何OutOfMemoryError情况区域。

    2.Java虚拟机栈(Java Virtual Machine Stacks)

    •   线程私有,生命周期与线程相同,每个Java方法执行同时创建一个栈帧(Stack Frame)来存储局部变量表、操作数栈等信息。
    •   每个Java方法从调用到执行完成,对应一个栈帧在Java虚拟机栈中入栈到出栈过程。
    •   局部变量表所需的内存空间在编译期间完成分配,方法运行期间不会改变局部变量表的大小。
    •   线程请求栈深度大于JVM允许深度,抛出StackOverFlowError异常。
    •   若Java虚拟机栈可以动态扩展,扩展时无法申请到足够内存抛出OutOfMemoryError异常。

    3.本地方法栈(Native Method Stack)

    •   线程私有,作用与Java虚拟机栈相似,抛出异常也一样。
    •   区别是Java虚拟机栈为执行Java方法(也就是字节码)服务,而本地方法栈为执行本地方法服务。

    4.Java 堆(Java Heap)

    •   线程共享,最大的内存空间,JVM启动时创建,存放几乎所有对象实例。
    •   为了更好的回收内存细分为:新生代,老年代;其中新生代再细致一点分为Eden空间、From Survivor空间、To Survivor空间。
    •   为了更好的分配内存:线程共享的Java堆中划分出多个线程私有的本地线程分配缓存区(Thread Local Allocation Buffer,TLAB)。
    •   堆中内存无法分配足够的内存给实例,并且堆也无法再扩展时,抛出OutOfMemoryError异常。

    5.方法区(Method Area)

    •   别名 Non-Heap(非堆)、永久代(HotSpot虚拟机使用永久代实现方法区,GC收集器就能像管理Java堆一样管理方法区)。
    •   线程共享,堆的一个逻辑部分,存储着已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据。
    •   方法区回收目标主要针对常量池的回收和对类型的卸载。
    •   方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
    •   运行时常量池是方法区一部分,Class文件中的常量池在类加载后进入方法区中的运行时常量池。

    个人理解内存溢出和内存泄漏问题:

    •   内存溢出:剩余内存空间小于请求分配空间时。
    •   内存泄漏:已经分配后的内存,在没有用之后却无法回收。

    JVM运行时数据区:

       

     

    2. 对象创建过程

      HotSpot虚拟机在Java堆中对象分配、布局与访问:

    • 虚拟机遇到了实例化一个对象的字节码消息时。首先检查是否能在量池中定位到一个类的符号引用。
    • 若是定位到了,就检查这个符号引用代表的类是否已被加载、解析和初始化过,若没有则先进行类加载。
    • 类加载完成后,JVM给对象分配内存,对象所需内存大小在类加载完成后便可以完全确定。
    • 给对象分配内存的方式: “指针碰撞”(Bump the Pointer)、 “空闲列表”(Free List)。
    • 解决并发情况下给对象分配空间问题:1.对分配内存空间的动作进行同步处理——JVM采用CAS配上失败重试的方式保证更新操作的原子性。2.本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。
    • 内存分配完 成后,JVM需要将分配到的内存空间(不包括对象头)都初始化为零值(保证了对象的实例字段不赋初值就可以直接使用)。
    • 对对象头进行必要的设置,例如如何才能找到类的元数据信息(这个对象是哪个类的实例)、对象的哈希码、对象的GC分代年龄等信息。
    • 到此新的对象已经产生,但是从Java程序视角来看,执行new指令后会接着执行<init>方法初始化,才产生出真正的可用对象。

      个人理解类的元数据信息:类的元数据即是我们的POJO,也就是我们用来实例化对象的类。

    给对象分配内存的方式:

    • 指针碰撞(Bump the Pointer):Java堆内存绝对规整。用过内存放一边,空闲内存放另一边,中间放着指针作为分界点指示器。分配内存时将指针向空闲内存移动对象大小的距离。适用于标记-整理算法和复制算法GC后的堆。
    • 空闲列表(Free List):JVM维护一个列表,记录哪些内存块可用,分配的时候从列表中找到足够大的空间划分给对象实例,并更新列表上的记录。适用于标记清除算法GC后的堆。

    解决并发情况下给对象分配空间问题:

    • 对分配内存空间的动作进行同步处理——JVM采用CAS配上失败重试的方式保证更新操作的原子性。
    • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB,个人理解为线程私有的缓冲区):JVM运行时多线程,每个线程在Java堆中预先分配一小块内存。哪个线程要分配内存,就在哪个线程的TLAB分配,只有TLAB用完并分配新的TLAB才需要同步锁定。

    对象的内存布局:

      1.对象头(Header):

        一部分存储对象自身运行时数据(官方称为Mark Word),如哈希码(HashCode)、GC分代年龄、锁状态标志等等。

        另一部分是类型指针,对象指向它的类元数据指针(通过这个指针JVM知道这个对象是哪个类的实例)。

        若对象是一个Java数组,在对象头中还需要有一块来记录数组长度的数据(JVM可以通过POJO的元数据信息确定对象大小,但是从数据的元数据中无法确定数组大小)。

      2.实例数据(Instance Data):存储对象真正有效信息,如代码中定义的各种类型的字段内容。

      3.对齐填充(Padding):HotSpot VM需要对象大小必须是8字节整数倍。当实例数据部分没有对齐,需要通过对齐填充补全。

         

    对象访问定位

    • 句柄访问:在Java堆中划分一块内存作为句柄池,reference中储存对象的句柄地址,句柄中包含了对象实例数据和对象类型数据(加载进JVM的类信息,也就是类的元数据信息)各自的具体地址信息,相当于二级指针。优势是reference中存储的是稳定的句柄地址,如在对象被移动(如复制算法)时只改变句柄中实例数据指针。
    • 直接指针访问(这是HotSpot采用的方式):reference中存储的就是对象实例地址,再通过对象实例中的类型指针(上文对象内存布局提到的方法头中的信息),去访问对象类型数据。优势是速度更快。

          

    3. GC收集

      需要需要进行GC的内存区域:Java堆和方法区。(程序计数器、Java虚拟机栈、本地方法栈是线程私有。方法结束时或线程结束时,内存自然就跟随着回收了)

    1.Java堆

    对象存活的判定算法:

    • 引用计数法(Reference Counting):给对象添加一个引用计数器,每当一个地方引用它,计数加1;引用失效,计数减1。计数器为0的对象不会再被使用。缺陷是很难解决对象之间的相互循环引用。
    • 可达性分析算法(Reachability Analysis):通过一系列的GC Roots的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

          

    可作为GC Roots的对象包括:全局性的引用(方法区中类静态属性和常量所引用的对象)。执行上下文(Java栈的栈帧中局部变量表和本地方法栈中JNI(一般说的Native方法)引用的对象)。

    引用(判定一个对象是否被引用,是两种存活判定算法的基础)的四种类型:

    • 强引用(Strong Reference):指在程序代码中普遍存在的,类似“ Object obj = new Object()”这类引用,只要强引用存在,垃圾收集器不会回收被引用的对象。
    • 软引用(Soft Reference):引用有用但并非必须的对象,在系统将要发生内存溢出异常前,会将这些对象进行二次回收。
    • 弱引用(Weak Reference):引用非必须对象,只能存活到下个GC前,GC时无论内存是否足够都必须回收。
    • 虚引用(Phantom Reference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。唯一目的是能在对象再被回收时收到一个系统通知。

    2.方法区

    方法区中主要GC两部分内容:废弃常量和无用的类。

    • 废弃常量:以常量池中的字面量回收为例,比如字符串“abc”进入常量池,但是当前没有一个String对象引用它就是废弃常量。
    • 无用的类:满足三个条件1.该类的实例都被回收。2.加载该类的ClassLoader已经被回收。3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    垃圾收集算法

    标记——清除算法(Mark-Sweep)

    • 最基础的收集算法,适用于老年代GC。
    • 原理:算法分为标记阶段和清除阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
    • 有两个主要不足:1.效率问题,标记和清除这两个过程效率都不高。2.空间问题:清除后产生大量不连续的内存碎片,导致分配大对象时,无法找到足够的连续空间而提前触发另一次GC。

          

    复制算法(Copying)

    • 适用于新生代GC(对象存活率较低)。
    • 原理:将内存按容量划分为大小相等的两块,每次只用其中一块,当这一块内存用完后将存活对象复制到另一块(移动堆顶指针按顺序分配内存),把已使用过的内存清洗掉。
    • 效率高,但是代价高(将内存缩小为原来的一半)。
    • 当今的复制算法:将堆内存分为一块较大的Eden空间和两块较小的Survivor空间(大小比例8:1:1),每次使用Eden和其中一块Survivor空间(主要写在Eden空间),GC时将还存活的对象复制到另外一块Survivor空间上,清洗掉使用过的空间。若Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。

          

    标记——整理算法(Mark-Compact)

    • 适用于老年代GC。
    • 原理:首先标记出所有需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

          

    分代收集算法(Generational Collection)

    • 根据对象存活周期不同将内存划分几块,根据各个年代的特点采用最适当收集算法。
    • 一般把Java堆分为新生代和老年代。新生代存活率低就用复制算法。老年代中对象存活率高,使用标记清除或标记整理算法。

          

    4. HotSpot算法的实现

      HotSpot用可达性分析算法来查看对象是否被引用,既然用的可达性分析算法那就得枚举GC Roots。

    枚举根节点的难点:

    • 引用太多,逐个查找能作为GC Roots的引用耗费太多时间。
    • 在用可达性分析算法时必须确保“一致性”(不可以出现分析时,对象引用关系还在不断变化),所以必须停止所有Java执行线程(Stop the World)。

    快速准确完成GC Roots枚举(也是选取SafePoint)

    • 目前主流Java虚拟机使用的都是准确式GC(可以直接识别出是哪些是指针还是非指针),HotSpot使用一组OopMap(Ordinary Object Pointer Map)的数据结构来达到目的。在类加载完成时,HotSpot就把对象内什么偏移量是什么数据类型都计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

    安全点(SafePoint):

    • SafePoint是由一组OopMap的数据结构在特定位置下记录下某些引用的位置。
    • 从线程角度看,SafePoint可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停。
    • 安全点机制保证了程序执行的时候,在不太长的时间就会遇到可进入gc的安全点。

    安全区域(Safe Region):

    • 是指一段代码片段,引用关系不会发生变化(可以看做扩展的SafePoint)。
    • 线程处于sleep状态或者blocked状态的时候,这时线程无法响应jvm的中断请求,就需要安全区域。
    • 线程执行到安全区域中,标识自己进入安全区域。离开时,首先检查JVM是否完成GC,完成了就继续执行,否则等待直到收到可以离开的信号为止。

    GC发生时让所有执行线程中断方式:

    • 抢先式中断(Preemptive Suspension):GC时,首先把所有线程全部中断,如果发现有线程不在安全点上,就恢复这个线程,让线程跑到安全点上再中断。
    • 主动式中断(Voluntary Suspension):在SafePoint设置中断标志,当GC需要中断线程时,设置中断标志位真,各个执行线程主动去轮询这个标志,发现是中断标志位真时就中断挂起。

    5. 垃圾收集器

    个人理解并发和并行:

    • 并发:多条线程在同一时段内进行(有可能有线程切换)
    • 并行:多条线程在同一时刻同时进行

    HotSpot 虚拟机的垃圾收集器

    Serial 收集器 / Serial Old 收集器(不谈)

    • 单线程新生代收集器,在进行GC时需要暂停其他所有工作线程(Stop The World),直到收集结束。
    • 新生代采用复制算法(Stop The World),老年代采用标记—整理算法(Stop The World)。
    • 简单而高效(与其他收集器单线程相比),Serial是Client模式下默认的新生代收集器。

        

    ParNew收集器

    • 多线程新生代收集器,是Serial收集器的多线程版本。
    • 新生代采用复制算法(Stop The World),老年代采用标记—整理算法(Stop The World)。
    • ParNew收集器是运行在Server模式下首选新生代收集器。
    • 除了Serial收集器外,只有它能与CMS收集器配合工作。

        

    Parallel Scavenge 收集器 / Parallel Old 收集器(不谈)

    • 多线程的新生代收集器,类似ParNew收集器,达到了一个可控制吞吐量。
    • 新生代采用复制算法(Stop The World),老年代采用标记—整理算法(Stop The World)。
    • 目标:达到一个可控制的吞吐量(吞吐量=运行代码时间/(运行代码时间+垃圾收集时间))。
    • GC停顿时间和吞吐两不可能同时调优,若是GC停顿时间越少,每次回收的新生代空间越小,导致GC收集发生的更快,原来10秒1次每次100毫秒,现在5秒1次每次70毫秒。吞吐量下降。

        

    CMS收集器(Concurrent Mark Sweep)

    • 并发低停顿的老年代收集器。适用于互联网站或者B/S系统的服务端上。
    • 目标:获取最短回收停顿时间。
    • 基于标记—清除算法实现的老年代GC。
    • GC 步骤:

        1.初始标记(CMS initial mark):stop the world,只是标记一下GC Roots能直接关联到的对象,速度快。

        2.并发标记(CMS concurrent mark):从GC Roots进行可达性分析。

        3.重新标记(CMS remark):修正并发标记期间因用户程序继续运作而导致的标记变动的那一部分标记记录,比初始标记时间长,远比并发标记时间短。

        4.并发清除(CMS concurrent sweep):基于标记结果,直接清理对象。

    • 三个明显的缺点:

        1.CMS收集器对CPU资源非常敏感,当CPU不足4个时,CMS对用户程序的影响就可能变得很大,从而导致应用程序变慢、总吞吐量降低。

        2.CMS收集器无法处理“浮动垃圾”(并发标记阶段用户线程还在运行着,就还会有新的垃圾产生,这部分垃圾出现在标记过程后,只能下次CG再清理掉,就称为浮动垃圾),因此CMS在GC时需要预留一部分空间给程序运作使用,要是预留的空间无法满足程序需要就会出现“Concurrent Mode Failure”失败,这时虚拟机启动后备预案:临时启动Serial Old收集器来重新进行老年代的GC。

        3.CMS是基于“标记—清除”算法实现的收集器,收集结束后会有大量的碎片空间产生,往往出现还有很大空闲空间却无法找到足够大的连续空间来分配给对象。CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数,用于CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程无法并发,停顿时间变长。虚拟机提供了另一个参数-XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的。

        

    G1收集器(Garbage—First)

    • 面向服务端应用的垃圾收集器。独立管理整个GC堆。
    • 特点:

        1.并行与并发:并发标记(并发),最终标记(并行),缩短StopTheWorld停顿时间。

        2.分代收集:保留分代概念。

        3.空间整合:整体看是基于“标记—整理”,局部看是基于”复制算法” ,总之在GC时不会产生内存空间碎片。

        4.可预测的停顿:不仅追求低停顿,还建立可预测的停顿时间模型(指定在M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)。

    • 内存划分与回收:使用G1收集器时,将Java堆划分为多个大小相等的独立区域(Region),并跟踪各个Region里面的垃圾堆积的价值大小(回收获得空间与所需时间),在后台维护一个优先列表,每次根据允许(设定)的收集时间,优先回收价值最大的Region。
    • GC步骤:

        1.初始标记:耗时短,单线程,StopTheWorld。标记GC Roots直接关联对象,并修改TAMS值(Next Top at Mark Start),让下一阶段并发运行时,能在可用的Region中创建新对象。

        2.并发标记:耗时长,并发。从GC Roots进行可达性分析。

        3.最终标记:并行,StopTheWorld。修正并发标记期间因用户程序运作而导致标记变动的那一部分标记记录。

        4.筛选回收:对各个Region的回收价值和成本进行排序,根据用户希望的GC停顿时间制定回收计划。

        

     最后用两张图总结一下各个GC收集器之间的关系:

    6. 对象分配内存与回收细节

    • 注意:对象内存分配,一般在堆上分配(有可能经过JIT编译后被拆散为标量类型并间接地在栈上分配)

    对象优先在Eden分配

    • 大多情况,对象在新生代Eden区分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC

    两种进入老年代的对象:

    • 大对象(很长的字符串以及数组)直接进入老年代:虚拟机提供一个参数,令大于这个设置值的对象直接在老年待分配。目的是避免在新生代区发生大量复制。
    • 长期存活的对象进入老年代:JVM给每个对象定义一个对象年龄计数器。若对象在Eden出生,经过了Minor GC后存活并且移入Survivor空间,对象年龄加1,当年龄增加到一定程度(默认15岁),就晋升到老年代。
    • 注:虚拟机并不永远要求对象年龄必须达到参数设置值才能晋升老年代,若Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半。年龄大于等于此年龄的对象直接进入老年代。

    Minor GC之前的空间分配担保问题

    1. 发生Minor GC之前,JVM先检查老年代的最大可用连续空间是否大于新生代所有对象总空间。
    2. 若大于,则确保Minor GC安全。
    3. 若不大于则会查看HandlePromotionFailure设置值是否允许担保失败。
    4. 如果允许担保失败,就查看老年代连续可用空间是否大于历次晋升老年代的对象平均大小。若大于则尝试进行Minor GC。
    5. 若小于或者HandlePromotionFailure不允许担保失败,则改为一次Full GC。
    6. 若第4步Minor GC失败,则重新发起一次Full GC。

       

    7. 类文件结构

      注:任何一个Class文件都对应着唯一个类或接口的定义信息。但是类或接口并不一定都得定义在Class文件里(譬如类或接口可以通过类加载器直接生成)

    Class文件存储结构

    • Class文件格式采用一种类似于C语言结构体的伪结构来存储数据。
    • 伪结构只有两种数据类型:无符号数和表。
    • 无符号数:基本数据类型(列如u1,u2分别代表1个字节,2个字节),无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
    • 表:由多个无符号数或者其他作为数据项构成的复合数据类型(所有表习惯用_info结尾)。表用来描述有层次关系的复合结构数据,Class文件本质就是一张表。
    • 注:无符号数和表在描述同一类型但是数量不确定的多个数据,经常会使用一个前置的容量计数器加若干个连续的数据项形式。

    魔数与Class文件版本

    • 魔数(Magic Number):Class文件的前4个字节,作用是确定这个文件是否为一个能被JVM接受的Class文件。很多文件存储标准都是用魔数来标识身份,Class文件的魔数是“0xCAFEBABE”。
    • 次版本号(Minor Version):Class文件的第5和第6个字节
    • 主版本号(Major Version):Class文件的第7和第8个字节
    • 注:高版本的JDK能向下兼容以前版本的Class文件,不能运行以后版本的JDK文件。

    常量池:

    • 常量池容量计数值(constant_pool_count):Class文件的第9和第10个字节。容量计数从1开始计数(满足后面某些指向常量池索引值的数据却不需要引用任何一个常量池项目,这种情况就将索引值置0)
    • 常量池(constant_pool):是一张表,紧接在常量池容量计数值后。
    • 主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
    • 字面量:如文本字符串“abc”、声明为final的常量值等。
    • 符号引用:包括了1.类和接口的全限定名。2.字段的名称和描述符。3.方法的名称和描述符。

    8. 虚拟机类加载机制

    • java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现。

    类的生命周期

    • 加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)。
    • 验证、准备、解析这3个部分统称为连接。
    • 加载、验证、准备、初始化、卸载,这些阶段通常是相互交叉地混合式进行的。
    • 解析阶段在某些情况(如实现多态)可以在初始化之后再开始,这是为了支持Java语言的运行时绑定(也称动态绑定或晚期绑定)。

    类加载的过程

    加载

    加载阶段JVM需要做的三件事:

    • 通过一个类的全限定名来获取定义此类的二进制字节流。
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在内存中生成一个代表这个类的java.lang.Class对象(对于HotSpot虚拟机而言,Class对象是存放在方法区中),这个对象将作为程序访问方法区中的这些类型数据的外部接口。
    • 非数组类的加载:既可以使用引导类加载器完成,也可以用用户自定义类加载器完成。
    • 数组类的加载:数组类本身不通过类加载器创建。它是由JVM直接创建的。

    验证

    验证是连接阶段的第一步,这阶段的目的是为了确保Class文件字节流中包含的信息符合当前JVM的要求,不会危害JVM的安全(Class文件并不一定要求用Java源码编译而来,可以由任何途径产生)。

    验证阶段的完成的4个检验动作:

    • 文件格式验证:验证字节流是否符号Class文件格式规范,并且能否被当前虚拟机处理(直接操作字节流)。目的是保证输入的字节流能正确的解析并存储于方法区内(只有通过此验证,字节流才能进入方法区进行存储)。
    • 元数据验证:对字节码描述信息进行语义分析,保证其信息符合Java语言规范要求。目的是对类的元数据信息进行语义校验。
    • 字节码验证:目的是通过数据流和控制流分析,确定程序语义是合法、符合逻辑的(对类的方法体进行校验分析)。
    • 符号引用验证:这个动作在解析阶段发生(发生在JVM将符号引用转化为直接引用时),目的是确保解析动作能正常执行。

    准备

    • 准备阶段是正式为类变量分配内存并设置类变量初始值(数据类型的零值)的阶段,类变量所使用的内存都将在方法区中进行分配(实例变量将在对象实例化时随着对象一起分配在Java堆中。)。
    • 注意:1.全局变量(成员变量)分为类变量(static修饰)和实例变量。2.通常情况是初始值是零值,但若是常量比如(public static final int value = 123;)则初始值为123。

    解析

    • 解析阶段是JVM将常量池中的符号引用替换为直接引用的过程。
    • 个人理解:Class文件的常量池中的各种表里的字段就是符号引用,符号引用替换为直接引用过程就是表中的字段去找到方法区中类存储数据的实际字段。
    • 符号引用(Symbolic References):是用一组符号来描述所引用的目标,可以是任何形式的字面量,只要能无歧义的定位到目标即可。引用的目标不一定加载到内存中。
    • 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。引用的目标必定在内存中存在。

    初始化

    • 初始化阶段是执行类构造器<clinit>()方法的过程。
    • 注:clinit()方法是类加载中初始化阶段的方法,init()是类实例化时调用的方法。
    • <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生。
    • JVM在执行子类的<clinit>()方法之前,父类的<clinit>()方法已经执行完毕。
    • JVM保证一个类的<clinit>()方法在多线程环境中会正确的加锁、同步。

    9. 类加载器

    • 定义:在JVM外部实现的代码模块,目的是实现类加载中加载阶段的第一个目标:通过一个类的全限定名来获取描述此类的二进制字节流。
    • 作用:1.实现类的加载动作。  2.对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。
    • 注:即使两个类来自同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,两个类一定不等。

    类加载器的分类

    Java虚拟机角度

    • 虚拟机内部:启动类加载器(Bootstrap ClassLoader),由C++实现。
    • 虚拟机外部,所有其他的类加载器,Java实现,全继承自抽象类java.lang.ClassLoader。

    Java开发人员角度:

    • 启动类加载器(Bootstrap ClassLoader):负责加载存放在Javajdk1.8.0_161jrelib目录中的、或者是被-Xbootclasspath参数指定路径中的,并且是被虚拟机识别的类库。启动类加载器无法被Java程序直接引用,用户编写自定义类加载器时若需要把请求委派给引导类加载器,直接用null代替即可。
    • 扩展类加载器(Extension ClassLoader):负责加载Javajdk1.8.0_161jrelibext目录中的类库,或者是被java.ext.dirs系统变量所指定的路径中的所有类库。
    • 应用程序类加载器(Application ClassLoader):也称系统类加载器,负责加载用户类路径(ClassPath)上所指定的类库。若没有自定义过自己的类加载器,那么默认类加载器就是它。
    • 若有必要可以加入自己的类加载器。
    • 双亲委派模型(Parents Delegation Model):

    • 双亲委派模型结构说明:除了顶层的启动类加载器外,其余加载器都有自己的父类加载器。这些类加载器的父子关系是使用组合的关系来复用父加载器的代码。
    • 双亲委派模型工作过程:一个类加载器收到了类加载请求,它不会自己尝试加载这个类,而是把这个请求委派给父加载器去完成,每个层次的加载器都是如此,因此请求最终应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求时,子加载器会尝试自己加载。

      

     

  • 相关阅读:
    Android webview 应用
    Android 访问权限设置
    Android应用----如何让应用全屏
    PHP基础
    递归在PHP中的应用举例
    软工实践个人总结
    2020软件工程实践第2次结对编程作业
    2020软件工程第一次结对作业
    2020软件工程实践第一次个人编程作业
    A brief introduction of myself
  • 原文地址:https://www.cnblogs.com/JimKing/p/8688842.html
Copyright © 2020-2023  润新知