美文网首页
Java面试攻略——JVM

Java面试攻略——JVM

作者: 史旭凯 | 来源:发表于2020-05-27 14:48 被阅读0次

    1.1JVM的运行机制

      JVM是运行Java字节码的虚拟机,包括一套字节码指令集,一组程序寄存器,一个虚拟机栈,一个虚拟机堆,一个方法区和一个垃圾回收器。JVM运行在操作系统上,不与硬件设备直接交互
      Java源文件在通过编译器之后被编译成相应的.Class文件(字节码文件),.Class文件又被JVM中的解释器编译成机器码在不同的操作系统上运行(Windows,Linux,Mac)上运行。每种操作系统的解释器都是不同的,但基于解释器实现的虚拟机是相同的,这也是Java能够跨平台的原因。在一个Java进程开始运行后,虚拟机就开始实例化了,有多个进程启动就会实例化多个虚拟机实例。进程退出或者关闭,则虚拟机实例消亡,在多个虚拟机实例之间不能共享数据
      Java程序的具体运行过程如下
        1.java源文件被编译器译成字节码文件。
        2.JVM将字节码文件编译成相应操作系统的机器码
        3.机器码调用相应操作系统本地方法库执行相应的方法
       Java虚拟机包括一个类加载器子系统,运行时数据区,执行引擎和本地接口库。JVM运行在操作系统之上,通过本地接口库调用本地方法库与操作系统进行交互如下图所示


    JVM.jpg

    其中

    • 类加载器子系统用于将编译好的.Class文件加载到JVM中
    • 运行时数据区用于存储在JVM运行过程中产生的数据,包括程序计数器,方法区,本地方法区,虚拟机栈和虚拟机堆
    • 执行引擎包括即时编译器和垃圾回收器,即时编译器用于将Java字节码编译成具体的机器码,垃圾回收器用于回收在运行过程中不再使用的对象
    • 本地接口库用于调用操作系统的本地方法库完成具体的指令操作

    1.2线程

      在多核操作系统上,JVM允许在一个进程内同时并发执行多个线程。JVM中的线程与操作系统中的线程是相互对应的,在JVM线程的本地存储,缓冲区分配,同步对象,栈,程序计数器等准备工作都完成时,JVM会调用操作系统的接口创建一个与之对应的原生线程;在JVM线程运行结束时,原生线程随之被收回。操作系统负责调度所有线程并为其分配CPU时间片,在原生线程初始化完毕时,就会调用Java线程的run()执行该线程;在线程结束时,会释放原生线程和Java线程所对应的资源

    1.3JVM的内存区域

      JVM的内存区域分为线程私有区域(程序计数器,虚拟机栈,本地方法区),线程共享区域(堆,方法区)和直接内存


    JVM内存区域.png

      线程私有区域的生命周期与线程相同,随线程的启动而创建,随线程的结束而销毁。在JVM内部,每个线程都与操作系统的本地线程直接映射,因此线程私有内存区域的存在与否和本地线程的启动和销毁对应。
      线程的共享区域随虚拟机的启动而创建,随虚拟机的关闭而销毁。
      直接内存也叫堆外内存,它并不是JVM运行时数据区的一部分,但在并发编程中被频繁使用。JDK的NIO模块提供基于Channel与Buffer的I/O操作方式就是基于堆外内存实现的,NIO模块通过调用Native函数库直接在操作系统上分配堆外内存,然后使用DirectByteBuffer对象作为这块内存的引用堆内存进行操作,Java进程可以通过堆外内存技术避免在Java堆和Native堆中来回复制数据带来的资源浪费和性能消耗,因此堆外内存在高并发应用场景下被广泛应用(Netty,Flink,HBase,Hadoop都有用到堆外内存)

    1.3.1程序计数器:线程私有,无内存溢出问题

      程序计数器是一块很小的内存空间,用于存储当前运行的线程所执行的字节码的行号指示器。每个运行中的线程都有一个独立的程序计数器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟机字节码指令地址;如果该方法执行的是Native方法,则程序计数器的值为空
      程序计数器属于线程私有的内存区域,它是唯一没有内存溢出的区域。

    1.3.2虚拟机栈:线程私有,描述Java方法的执行过程

      虚拟机站是描述Java方法的执行过程中的内存模型,它在当前栈帧中存储了局部变量表,操作数栈,动态链接,方法出口等信息。同时,栈帧用来存储部分,运行时数据及其数据结构,处理动态链接方法的返回值和异常分派。

    1.3.3本地方法区:线程私有

      本地方法区和虚拟机栈作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务

    1.3.4堆:也叫做运行时数据区,线程共享

      在JVM运行的过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存区域。由于现代JVM采用分代收集算法,因此Java堆从GC的角度还可以细分为:新生代,老年代和永久代

    1.3.5方法区:线程共享

      方法区也被称为永久代,用于存储常量,静态变量,类信息,即时编译器编译后的机器码,运行时常量池等数据


    方法区.png

      JVM把GC分代收集扩展至方法区,即使用Java堆的永久代来实现方法区,这样JVM的垃圾收集器就可以像管理Java堆一样管理这部分内存。永久代的内存回收主要针对常量池的回收和类的卸载,因此可回收的对象很少。
      常量被存储在运行时常量池中,是方法区的一部分,静态变量也属于方法区的一部分。在类信息中不但保存了类的版本,字段,方法,接口等描述信息,还保存了常量信息。
      在即时编译后,代码的内容将在执行阶段(类加载完成后)被保存在方法区的运行时常量池中。Java虚拟机对Class文件每一部分的格式都有明确的规定,只有符合JVM规范的class文件才能通过虚拟机检查,然后被装载执行。

    1.4JVM运行时内存

      JVM的运行时内存也叫做JVM堆,从GC的角度可以将JVM堆分为新生代,老年代和永久代。其中新生代默认占1/3堆内存空间,老年代默认占2/3堆内存空间,永久代占非常少的堆内存空间。新生代又分为Eden区,SurvivorFrom(S1)区和SurvivorTo(S2)区,Eden区默认占8/10新生代空间,SurvivorFrom区和SurvivorTo区默认分别占1/10新生代空间。


    JVM堆.png

    1.4.1新生代:Eden区,SurvivorFrom(S1)区和SurvivorTo(S2)区

      JVM新创建的对象(除了大对象)会被存放在新生代,默认占1/3堆内存空间。由于JVM会频繁的创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为Eden区,SurvivorFrom(S1)区和SurvivorTo(S2)区
      (1) Eden区: Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代。大对象的定义和具体的JVM版本、堆大小和垃圾回收策略有关,一般为 2KB ~ 128KB, 可通过相关JVM配置其大小 。在Eden区的内存空间不足时会触发MinorGC,对新生代进行一次垃圾回收。
      (2) SurvivorTo区:保留上一次MinorGC时的幸存者
      (3) SurvivorFrom区:将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者
      新生代的GC过程叫作MinorGC,采用复制算法实现,具体过程如下。
      (1)把在Eden区和SurvivorFrom区中存活的对象复制到SurvivorTo区。如果某对象的年龄达到老年代的标准(对象晋升老年代的标准默认为15),则将其复制到老年代,同时把这些对象的年龄加1;如果SurvivorTo区的内存空间不够,则也直接将其复制到老年代;如果对象属于大对象(大小为2KB~ 128KB的对象属于大对象),则也直接将其复制到老年代。
      (2)清空Eden区和SurvivorFrom区中的对象。
      (3)将SurvivorTo区和SurvivorFrom区互换,原来的SurvivorTo区成为下一次GC时的SurvivorFrom区。

    1.4.2老年代

      老年代主要存放长生命周期的对象和大对象。老年代的GC过程叫作MajorGC。在老年代,对象比较稳定, MajorGC不会被频繁触发。在进行MajorGC前, JVM会进行次MinorGC,在MinorGC过后仍然出现老年代且当老年代空间不足或无法找到足够大的连续内存空间分配给新创建的大对象时,会触发FullGC进行垃圾回收,释放JVM的内存空间。
      MajorGC采用标记清除算法,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放内存空间。
      因为要先扫描老年代的所有对象再回收,所以MajorGC的耗时较长。MajorGC的标记清除算法容易产生内存碎片。在老年代没有内存空间可分配时,会抛出Out Of Memory异常。

    1.4.3永久代

      永久代指内存的永久保存区域,主要存放Class和Meta (元数据)的信息。Class在类加载时被放入永久代。永久代和老年代、新生代不同, GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载的Class文件过多时会抛出Out Of Memory异常,比如Tomcat引用Jar文件过多导致JVM内存不足而无法启动。
      需要注意的是,在Java 8中永久代已经被元数据区(也叫作元空间)取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据区并没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因此,元空间的大小不受JVM内存的限制,只和操作系统的内存有关。
      在Java8中, JVM将类的元数据放入本地内存(Native Memory)中,将常量池和类的静态变量放入Java堆中,这样JVM能够加载多少元数据信息就不再由JVM的最大可用内存(MaxPermsize)空间决定,而由操作系统的实际可用内存空间决定。

    1.5垃圾回收算法

    1.5.1如何确定垃圾

      Java采用引用计数法和可达性分析法来去确定对象是否应该被回收。


    垃圾确定.png

        1.引用计数法
          在Java中如果要操作对象,就必须先获取该对象的引用,因此可以通过引用计数法来判断一个对象是否可以被回收。在为对象添加一个引用时,引用计数加1;在为对象删除一个引用时,引进计数减1;如果一个对象的引用计数为0,则表示此刻该对象没有被引用,可以被回收。
          引用计数法容易产生循环引用问题。循环引用指两个对象相互引用,导致它们的引用一直存在,而不能被回收
        2.可达性分析法
          为了解决引用计数法的循环引用问题, Java还采用了可达性分析来判断对象是否可以被回收。具体做法是首先定义此GC Roots对象,然后以这些GC Roots对象作为起点向下搜索,如果在GC Roots和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍然是不可达的,则将被垃圾收集器回收。
          Java中可作为GC Roots的对象包括下面几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈JNI(Native方法)引用的对象

    1.5.2垃圾收集算法

      Java中常用的垃圾回收算法有标记清除,复制,标记整理和分代收集四种垃圾回收算法


    垃圾收集算法.png

      1.标记—清除算法
      算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象、
      不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
      2.复制算法
      他将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可
      不足:将内存缩小为了原来的一半
      实际中我们并不需要按照1:1比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor
      当另一个Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代
      3.标记—清除—整理算法
      让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
      4.分代收集算法
      只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收

    1.6Java中的4种引用类型

      Java中一切皆对象,对象的操作是通过该对象的引用实现的,Java中的引用类型分为四种,分别为强引用,软引用,弱引用和虚引用


    4种引用类型.png

      1.强引用:我们平时new了一个对象就是强引用,即使在内存不足的情况下也不会回收这种对象。因此,强引用是造成Java内存泄漏的主要原因
      2.软引用:内存空间足够垃圾回收器不会回收它,如果内存不足了,就会回收这些对象的内存
      3.弱引用:在垃圾回收时一定会被回收
      4.虚引用:任何时刻都可能被回收,主要用来检测GC活动

    1.7分代收集算法与分区收集算法

    1.7.1分代收集算法

      JVM根据对象存活周期的不同将内存划分为新生代,老年代,和永久代,并根据各代的特点分别采用不同的GC算法
      1.新生代与复制算法
        新生代主要存储短生命周期的对象,因此在垃圾回收的标记阶段会标记大量已死亡的对象及少量存活的对象,因此只需选用复制算法将少量存活的对象复制到内存的另一端并清理原内存区域的内存即可
      2.老年代与标记整理算法
        老年代主要存放长生命周期的对象和大对象,可回收的对象一般比较少,因此JVM采用标记整理算法进行垃圾回收,直接释放死亡状态的对象所占用的内存空间

    1.7.2分区收集算法

      分区收集算法将整个堆空间划分为连续的大小不同的区域,在每个小区域内单独进行内存使用和垃圾回收,这样做的好处是可以根据每个小区域内存的大小灵活使用和释放内存
      分区收集算法可以根据系统可接受停顿时间,每次都快速回收若干个小区域中的内存,以缩短垃圾回收时系统的停顿时间,最后以多次并行累加的方式逐步完成整个内存区域的垃圾回收。如果垃圾回收机制一次回收整个堆内存,则需要更长的系统停顿时间,长时间的系统停顿将影响系统的稳定性

    1.8垃圾收集器

      Java堆内存分为新生代和老年代:新生代主要存储短生命周期的对象,适合使用复制算法进行垃圾回收;老年代主要存储长生命周期的对象,适合使用标记整理算法进行垃圾回收。因此, JVM针对新生代和老年代分别提供了多种不同的垃圾收集器,针对新生代提供的垃圾收集器有Serial, ParNew,Parallel Scavenge,针对老年代提供的垃圾收集器有Serial Old, Parallel Old,CMS,还有针对不同区域的G1分区收集算法,如图


    Java垃圾收集器.png

    1.8.1 Serial垃圾收集器:单线程,复制算法

      Serial垃级收集器基于复制算法实现,它是一个单线程收集器,在它正在进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束。
      Serial垃圾收集器采用了复制算法。其特点是:简单、高效,对于单CPU运行环境来说,没有线程交互开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器是Java虚拟机运行在Client模式下的新生代的默认垃圾收集器。

    1.8.2 ParNew垃圾收集器:多线程,复制算法

      ParNew垃圾收集器是Serial垃圾收集器的多线程实现,同样采用了复制算法,它采用多线程模式工作,除此之外和Serial收集器几乎一样。ParNew垃圾收集器在垃圾收集过程中会暂停所有其他工作线程,是Java虚拟机运行在Server模式下的新生代的默认垃圾收集器。
      ParNew垃圾收集器默认开启与CPU同等数量的线程进行垃圾回收,在Java应用启动时可通过-XX:ParallelGCThreads参数调节ParNew垃圾收集器的工作线程数。

    1.8.3 Parallel Scavenge垃圾收集器:多线程,复制算法

      Parallel Scavenge垃圾收集器是为提高新生代垃圾收集效率而设计的垃圾收集器,基于多线程复制算法实现,在系统吞吐量上有很大的优化,可以更高效地利用CPU尽快完成垃圾回收任务。
      Parallel Scavenge通过自适应调节策略提高系统吞吐量,提供了三个参数用于调节、控制垃圾回收的停顿时间及吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数,控制吞吐量大小的-XX:GCTimeRatio参数和控制自适应谈节策略开启与否的UseAdaptiveSizePolicy参数。

    1.8.4 Serial Old垃圾收集器:单线程,标记整理算法

      Serial Old垃圾收集器是Serial垃圾收集器的老年代实现,同Serial一样采用单线程执行,不同的是, Serial Old针对老年代长生命周期的特点基于标记整理算法实现。Serial Old垃圾收集器是JVM运行在Client模式下老年代默认垃圾收集器


    Serial垃圾收集器.png

    1.8.5 Parallel Old垃圾收集器:多线程,标记整理算法

      Parallel Old垃圾收集器采用多线程并发进行垃圾回收,它根据老年代长生命周期的特点,基于多线程的标记整理算法实现。Parallel Old垃圾收集器在设计上优先考虑系统吞吐量,其次考虑停顿时间等因素,如果系统对吞吐量的要求较高,则可以优先考虑新生代的Parallel Scavenge垃圾收集器和老年代的Parallel Old垃圾收集器的配合使用。


    Parallel Scavenge和Parallel Old.png

    1.8.6 CMS垃圾收集器

    CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停,它的收集周期是这样:
      初始标记(CMS-initial-mark) -> 并发标记(CMS-concurrent-mark) -> 重新标记(CMS-remark) -> 并发清除(CMS-concurrent-sweep) ->并发重设状态等待下次CMS的触发(CMS-concurrent-reset)。  
      1、初始标记:独占PUC,仅标记GCroots能直接关联的对象
      2、并发标记:可以和用户线程并行执行,标记所有可达对象
      3、重新标记:独占CPU(STW),对并发标记阶段用户线程运行产生的垃圾对象进行标记修正
      4、并发清理:可以和用户线程并行执行,清理垃圾
      其中的1,3两个步骤需要暂停所有的应用程序线程的。第一次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第二次暂停是在并发标记之后, 暂停所有应用程序线程,重新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致)。第一次暂停会比较短,第二次暂停通常会比较长,并且 remark这个阶段可以并行标记。
      而并发标记、并发清除、并发重设阶段的所谓并发,是指一个或者多个垃圾回收线程和应用程序线程并发地运行,垃圾回收线程不会暂停应用程序的执行,如果你有多于一个处理器,那么并发收集线程将与应用线程在不同的处理器上运行,显然,这样的开销就是会降低应用的吞吐量。Remark阶段的并行,是指暂停了所有应用程序后,启动一定数目的垃圾回收进程进行并行标记,此时的应用线程是暂停的
      CMS收集器是基于标记清除算法实现的,整个过程分为4个步骤:
      1.初始标记2.并发标记3.重新标记4.并发清除
      优点:并发收集、低停顿
      缺点:
      1.CMS收集器对CPU资源非常敏感,CMS默认启动的回收线程数是(CPU数量+3)/4,
      2.CMS收集器无法处理浮动垃圾,可能出现Failure失败而导致一次Full G场地产生.在最后一步并发清理过程中,用户县城执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次gc的时候清理掉,这部分垃圾叫浮动垃圾
      3.CMS是基于标记清除算法实现的. CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC,为了解决这个问题CMS提供了一个开关参数,用于在CMS顶不住,要进行FullGC时开启内存碎片的合并整理过程,但是内存整理的过程是无法并发的,空间碎片没有了但是停顿时间变长了
      CMS出现FULL GC的原因:
      1、年轻带晋升到老年带没有足够的连续空间,很有可能是内存碎片导致的
      2、在并发过程中JVM觉得在并发过程结束之前堆就会满,需要提前触发FullGC


    CMS垃圾收集器.png

    1.8.7 G1垃圾收集器

      G1垃圾收集器为了避免全区域垃圾收集引起的系统停顿,将堆内存划分为大小固定的几个独立区域,独立使用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域。G1垃圾收集器通过内存区域独立划分使用和根据不同优先级回收各区域垃圾的机制,确保了G1垃圾收集器在有限时间内获得最高的垃圾收集效率。相对于CMS收集器, G1垃圾收集器两个突出的改进。

    • 基于标记整理算法,不产生内存碎片。
    • 可以精确地控制停顿时间,在不牺牲吞吐量的前提下实现最短停顿垃圾回收。

    1.9 JVM类加载机制

    1.9.1 JVM的类加载阶段

      JVM的类加载分为5个阶段:加载,验证,准备,解析,初始化。在类初始化完成后就可以使用该类信息在类不再被需要时从JVM卸载

    类的生命周期.jpg
    1.加载
      指JVM读取Class文件,并且根据Class文件描述创建java.lang.Class对象的过程。类加载过程主要包含将Class文件读取到运行时区域的方法区内,在堆中创建java.lang. Class ,并封装类在方法区的数据结构的过程,在读取Class文件时既可以通过文件的形式读取,也可以通过jar包、war包读取,还可以通过代理自动生成Class或其他方式读取。
    2.验证
      主要用于确保Class文件符合当前虚拟机的要求,保障虚拟机自身的安全,只有通过验证的Class文件才能被JVM加载。
    3.准备
      主要工作是在方法区中为类变量分配内存空间并设置类中变量的初始值。初始值指不同数据类型的默认值,这里需要注意final类型的变量和非final类型的变量在准备阶段的数据初始化过程不同。比如一个成员变量的定义如下:
    public static long value = 1000;
    

      在以上代码中,静态变量value在准备阶段的初始值是0,将value设置为1000的动作是在对象初始化时完成的,因为JVM在编译阶段会将静态变量的初始化操作定义在构造器中。但是,如果将变量value声明为final类型:

    public static final int value =1000;
    

    则JVM在编译阶段后会为final类型的变量value生成其对应的ConstantValue属性, 虚拟机在准备阶段会根据ConstantValue属性将value贼值为1000
    4.解析
      JVM会将常量池中的符号引用替换为直接引用
    5.初始化
      主要通过执行类构造器的<client>方法为类进行初始化。<client>方法是在编译阶段由编译器自动收集类中静态语句块和变量的赋值操作组成的。JVM规定,只有在父类<client>方法都执行成功后,子类中的<client>方法才可以被执行。在一个类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成<client>方法。
      在发生以下几种情况时, JVM不会执行类的初始化流程。

    • 常量在编译时会将其常量值存入使用该常量的类的常量池中,该过程不需要调用常量所在的类,因此不会触发该常量类的初始化。
    • 在子类引用父类的静态字段时,不会触发子类的初始化,只会触发父类的初始化
    • 定义对象数组,不会触发该类的初始化。
    • 在使用类名获取Class对象时不会触发类的初始化。
    • 在使用Class.forName加载指定的类时,可以通过initialize参数设置是否需要对类进行初始化。
    • 在使用ClassLoader默认的loadClass方法加载类时不会触发该类的初始化。

    1.9.2 类加载器

      JVM提供三种类加载器
      (1)启动类加载器(Bootstrap ClassLoader) : 这个类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar ,名字不符合类库不会加载) 类库加载到虚拟机内存中。启动类加载器无法被 java 程序直接引用,如需要,直接使用 null 代替即可。
      (2)扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader 实现,它负责加载<JAVA_HOME>\lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

       (3)应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher$AppClassLoader 实现。这个这个类加载器是 ClassLoader 中的getSystemClassLoader() 方法的返回值,所以一般称它为系统类加载器。它负责加载用户路径(ClassPath)上所指定的类库,开发者可以使用这个类加载器,如果应用程序没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    我们的应用程序都是由这3中类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如下图所示:


    类加载器.png

    1.9.3 双亲委派机制

       JVM通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类而是把该类加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有类加载请求都被委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的Class文件在父类的类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFoud异常
       双亲委派机制得工作过程:
       1.类加载器收到类加载的请求;
       2.把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;
       3.启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通知子加载器进行加载。
       4.重复步骤三;
       双亲委派机制的作用(唯一性,安全性)
       1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
       2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

    相关文章

      网友评论

          本文标题:Java面试攻略——JVM

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