JAVA虚拟机

1.运行时数据区域

JAVA虚拟机_第1张图片


1.程序计数区
        当前线程所执行的字节码的行号指示器,是线程私有的。字节码解释器工作时,通过改变计数器值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖此计数器。 此内存区域是唯一一个没有规定任何OutOfMemoryError情况的区域
       若线程执行的是一个Native方法,计数器值则为空。

2.JAVA虚拟机栈
       JAVA虚拟机栈是线程私有的,生命周期与线程相同,描述的是JAVA方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口灯信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
       可通过虚拟机参数-Xss来指定每个线程的JAVA虚拟机栈大小;
       对此区域规定了两种异常:

  • 若线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  • 若虚拟机栈可动态扩展,如果扩展时无法申请到足够内存,将抛出OutOfMemoryError异常;

3.本地方法栈
       与JAVA虚拟机的作用类似,本地方法栈为虚拟机使用到的Native方法服务

4.JAVA堆
       所有的对象实例以及数组都要在堆上分配,是垃圾收集器管理的主要区域,由于现在收集器基本都采用分代收集算法,所有JAVA堆还可以细分为:

  • 新生代(Young Generation)
    • Eden
    • From Survivor
    • To Survivor
  • 老年代(Old Generation)

       线程共享的JAVA堆可能分配出多个线程私有的分配缓存区(Thread Local Allocation Buffer,TLAB),存储的仍是对象实例。
       JAVA堆可以处于物理上不连续的内存空间中,并且可以动态扩展其内存,扩展失败将会抛出OutOfMemoryError异常,可以通过-Xms-Xmx两个虚拟机参数来指定堆内存大小,第一个参数设置初始值,第二个参数设置最大值。

5.方法区
       用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。与JAVA堆一样是线程共享的内存区域,也不需要连续的内存,可动态扩展内存,扩展失败一样会抛出OutOfMemoryError异常。
       此区域的垃圾回收目标主要是针对常量池的回收和对类的卸载,不过回收条件相当苛刻,难以令人满意。在JDK1.7之前,HotSpot虚拟机把方法当成永久代进行垃圾回收,在JDK1.7之后,已经把原本放在永久代的字符串常量池移除。

6.运行时常量池
       运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。除了在编译期生成的常量,还允许动态生成,例如String类的intern()
       当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

7.直接内存
       直接内存并不是虚拟机运行时数据区的一部分,在JDK1.4中新加入的NIO类,引入了基于通道与缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在JAVA堆中的DirectByteBuffer对象作为这块内存区域的引用进行操作。
       这样能在一些场景中显著提高性能,因为避免了在JAVA堆和Native堆中来回复制数据。

2.垃圾收集

       垃圾回收主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。

2.1判断对象是否可被回收

1.引用计数法
       给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象均可被回收。但是两个对象出现循环引用的情况下,此时计数器永远不为0,导致无法对它们进行回收。因此JAVA虚拟机不使用引用计数法。

2.可达性分析
       通过"GC Roots"对象作为起始点进行搜索,搜索经过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的。

JAVA虚拟机_第2张图片

       JAVA虚拟机使用该算法来判断对象是否可被回收,在JAVA中GC Roots一般包含以下内容:

  • 虚拟机栈中引用的对象;
  • 本地方法栈中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中的常量引用的对象;

       即使在可达性分析算法中不可达的对象,也并非一定被回收,至少需要经过两次标记过程:

  • 若可达性分析不可用,将会被第一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,以下情况均视为没有必要;
    • 对象没有覆盖finalize方法;
    • finalize方法已经被虚拟机调用过;
  • 若有必要,则将对象放置在F-Queue队列中,然后由一个低优先级的Finalizer线程去执 行此方法(并不承诺会等待其结束);
  • GC对F-Queue队列中的对象进行第二次小规模的标记,若要"自救",则对象要在finalize方法中重新关联上引用链;
  • 若第二次标记仍为不可达,则移除"即将回收"队列,进行回收;

3.方法区回收
       方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。主要是对常量池中废弃常量和无用类的卸载。
       在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
       类需要同时满足下面三个条件才能算是"无用的类",但满足不一定会被回收卸载,可通过-Xnoclassgc参数来控制是否对类进行卸载:

  • 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例;
  • 加载该类的 ClassLoader 已经被回收;
  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法;

2.2垃圾收集算法

1.标记清除算法

JAVA虚拟机_第3张图片

       首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足有两个:

  • 效率问题,标记和清除两个过程的效率都不高;
  • 空间问题,会尝试大量不连续的内存碎片;

2.标记整理算法

JAVA虚拟机_第4张图片

       标记过程与"标记清除"一样,但后续步骤是让存活对象移向一端,然后直接清理掉端边界以外的内存。

3.复制算法

JAVA虚拟机_第5张图片

       将内存划分为大小相等的两块,每次只使用其他的一处。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉。实现简单,运行效率高,代价是将内存缩小了一半。
       现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。
       HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。

4.分代收集算法
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。一般将堆分为新生代和老年代。

  • 新生代使用:复制算法

  • 老年代使用:标记 - 清除 或者 标记 - 整理 算法

2.3垃圾收集器

JAVA虚拟机_第6张图片

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

  • 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
  • 串行、并行、并发:
    • 串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序(Stop The World);
    • 并行指的是多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态;
    • 并发指的是垃圾收集线程和用户线程同时执行;
    • 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。

1.Serial 收集器

JAVA虚拟机_第7张图片

       Serial收集器是单线程收集器,仅使用一条收集线程去完成垃圾收集工作, 在进行垃圾收集时,必须暂停其他所有的工作线程
       优点:简单高效,对单CPU环境, Serial收集器没有线程交互的开销,因此拥有最高的单线程收集效率。虚拟机运行在Client模式下的默认新生代收集器。

2.ParNew 收集器

JAVA虚拟机_第8张图片

       ParNew收集器是Serial收集器的多线程版本。是Server模式下的虚拟机首选的新生代收集器,除性能原因外,主要是因为除了Serial收集器,只有它能与CMS收集器配合工作。
       默认开启线程数与CPU的数量相同,可以使用 -XX:ParallelGCThreads参数来设置线程数。

3.Parallel Scavenge 收集器

       Parallel Scavenge收集器同样是并行的多线程收集器,其他收集器的关注点在于缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控吞吐量。

吞吐量 = 运行用户代码使用的CPU时间 / ( 用户代码时间 + 垃圾收集时间 )  

       停顿时间短适合需要与用户交互的程序,良好的响应速度能提升用户体验;高吞吐量可以高效率地利用CPU时间,尽快完成程序运算,主要适合后台运算而不需要太多交互的任务。
       缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
       通过参数-XX:+UseAdaptiveSizePolicy开启GC自适应调节策略,无需手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调节这些参数以提供最合适的停顿时间或者最大的吞吐量。

4.Serial Old 收集器

JAVA虚拟机_第9张图片

       是Serial收集器的老年代版本,单线程收集器,使用"标记-整理"算法。主要意义在于给Client模式下的虚拟机使用;如果在Server模式下,主要还有两大用途:

  • JDK1.5及以前,它与Parallel Scavenge收集器搭配使用;
  • 作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用;

5.Parallel Old 收集器

JAVA虚拟机_第10张图片

       Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理算法",在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge + Parallel Old收集器。

6.CMS收集器

JAVA虚拟机_第11张图片

       CMS(Concurrent Mark Sweep)以获取最短回收停顿时间为目标的收集器,基于"标记-清除"算法,整个垃圾收集过程分为4个步骤:

  • 初始标记:需要"Stop The World",仅标记GC Roots直接关联的对象,速度很快;
  • 并发标记:进行GC Roots Tracing的过程,在整个过程中耗时最长,无需停顿,可与用户线程并发执行;
  • 重新标记:修正并发标记期间,因用户线程继续运行而导致标记产生变动,需要"Stop The World";
  • 并发清理:无需停顿;

       缺点:

  • CMS收集器对CPU资源非常敏感:通过牺牲吞吐量来达到降低停顿时间,导致CPU利用率不够;

  • 无法处理浮动垃圾,可能出现Concurrent Mode Failure:并发清理阶段用户线程产生的新垃圾无法标记,也就无法再当次垃圾收集中处理掉它们。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

  • CMS基于"标记-清除"算法实现,收集结束会有大量空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

    • -XX:UseCMSCompactAtFullCollection参数,用于在要进行FullGC时开启内存碎片的合并整理过程;
    • -XX:CMSFullGCsBeforeCompation参数,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0);

7.G1 收集器
       G1是面向服务端应用的垃圾回收器,在多CPU和大内存的场景下有很好的性能。它将整个JAVA堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但两者不再是物理隔离的,它们都是一部分Region(不需要连续)的集合。G1可以直接对新生代和老年代一起回收。

JAVA虚拟机_第12张图片

       G1跟踪各个Region里面的垃圾堆的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
       在G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,虚拟机在进行可达性分析时都是使用Remembered Set来避免全堆扫描。通过对引用对象进行写操作时暂停,并检查两个对象是否处于不同的Region,即是否老年代引用了年轻代的对象,如果是,则将引用信息记录到被引用对象所属的Region的Remembered Set中。
JAVA虚拟机_第13张图片

       如果不计算维护Remembered Set的操作,G1收集器的运作可分为以下4个步骤:

  • 初始标记;
  • 并发标记;
  • 最终标记:为了修正并发标记期间,用户线程继续运作而导致的标记产生变化,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。需停顿用户线程,可并行执行;
  • 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

       G1收集器具有以下特点:

  • 空间整合:G1从整体看来是基于"标记-整理"算法,从局部(两个Region之间)看是基于复制算法实现的。运行期间不会产生空间碎片;
  • 可预测停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒;

3.内存分配与回收策略

1.对象优先在Eden分配
       大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC;

2.大对象直接进入老年代
       大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间分配给大对象。
       -XX:PretenureSizeThreshold参数,另大于此设定值的对象直接在老年代分配,避免在Eden和Survivor区之间的大量内存复制。

3.长期存活对象进入老年代
       虚拟机给每个对象定义了对象年龄计数器,对象在Survivor区中每经历一次Minor GC,年龄就增加一岁,增加到一定程度,就将会晋升到老年代中。
       -XX:MaxTenuringThreshold参数,设置对象晋升老年代的年龄阈值;

4.动态年龄判断
       如果在Survivor空间中,相同年龄对象大小的总和大于Survivor空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

5.空间分配担保
       在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
       如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。

6.Full GC的触发条件

  • 调用 System.gc():只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行;
  • 老年代空间不足:老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等;
  • 空间分配担保失败:使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC;
  • Concurrent Mode Failure:执行 CMS GC 的过程中同时有新对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的预留空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC;

4.类加载机制

       类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。类是在运行期间第一次使用时动态加载的,而不是编译时期一次性加载。因为如果在编译时期一次性加载,那么会占用很多的内存

JAVA虚拟机_第14张图片

1.类加载的时机
       Java虚拟机规范中并没有进行强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):

主动引用

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候;
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化;
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

被动引用
       以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

  • 通过子类引用父类的静态字段,不会导致子类初始化;

      System.out.println(SubClass.value);// value 字段在 SuperClass 中定义  
    
  • 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法;

      SuperClass[] sca = new SuperClass[10];  
    
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化;

      System.out.println(ConstClass.HELLOWORLD);  
    

2.类加载过程
       包含了加载、验证、准备、解析和初始化这 5 个阶段;

2.1加载
       加载时类加载过程的一个阶段,在加载阶段,虚拟机需要完成以下3件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流;
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构;
  • 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口;

       对于数组类而言,本身不通过类加载器创建,它是由Java虚拟机直接创建的。

       其中二进制字节流可以从以下方式中获取:

  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

2.2验证
       为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。大致上回完成下面4个阶段的检验动作:

  • 文件格式验证:
    • 是否以魔数0xCAFEBABE开头;
    • 主、次版本号是否在当前虚拟机处理范围之内;
    • 常量池的常量中是否有不支持的常量类型(检查常量tag标志);
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量;
    • Class文件中各个部分及文件本身是否有被删除或附加的其他信息;
  • 元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合JAVA语言规范的要求
    • 此类是否有父类;
    • 此类的父类是否继承了不允许被继承的类(被final修饰的类);
    • 若此类不是抽象类,是否实现了其父类或接口中要求实现的所有方法;
    • 此类中的字段、方法是否与父类产生矛盾;
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的;
  • 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,此动作将在连接的解析阶段发生;
    • 符号引用中通过字符串描述的全限定名是否能找到相应的类;
    • 指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
    • 符号引用中的类、字段、方法的访问性是否可被当前类访问;

2.3准备
       准备阶段是正式为类变量(被static修饰的变量)分配内存并设置类变量初始值的阶段,不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
       注意,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次
       初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123

public static int value = 123;  

       如果类变量是常量,那么会按照表达式所指定的值来进行初始化,而不是赋值为 0

public static final int value = 123;  

2.4解析
       解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持JAVA的动态绑定。

2.5初始化
       初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 () 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
        () 方法具有以下特点:

  • 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问;

      public class Test {  
          static {  
               i = 0;                // 给变量赋值可以正常编译通过  
              System.out.print(i);  // 这句编译器会提示“非法向前引用”  
          }  
          static int i = 1;  
      }  
    
  • 与类的构造函数(或者说实例构造器 ())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 () 方法运行之前,父类的 () 方法已经执行结束。因此虚拟机中第一个执行 () 方法的类肯定为 java.lang.Object;

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

      static class Parent {  
          public static int A = 1;  
          static {  
              A = 2;  
          }  
      }  
    
      static class Sub extends Parent {  
          public static int B = A;  
      }  
    
      public static void main(String[] args) {  
          System.out.println(Sub.B);  // 2  
      }  
    
  • () 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 () 方法;

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

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

5.类与类加载器

       对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性。比较两个类相等需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。

类加载器分类
       从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;
  • 所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader;

       从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

  • 启动类加载器(Bootstrap ClassLoader):此类加载器负责将存放在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
  • 扩展类加载器(Extension ClassLoader):这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 /lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型

JAVA虚拟机_第15张图片

       双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。

       双亲委派模型的工作过程:一个类加载器首先将类加载请求传送到父类加载器,因此所有类加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈无法完成类加载请求时(它的搜索范围中没有找到所需的类)时,子加载器才尝试自己加载。
       双亲委派模型的好处:使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
       例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
       双亲委派模型的实现:实现双亲委派的代码都集中在java.lang.ClassLoaderloadClass()方法之中,实现逻辑清晰易懂:

  • 先检查类是否已经被加载过,若没有加载则调用父加载器的loadClass();
  • 若父加载器为空,则默认使用启动类加载器作为父加载器;
  • 如果父加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载;

你可能感兴趣的:(JAVA虚拟机)