Java面试——JVM

1、Java代码的执行过程
  编译->加载->解释->执行。
  通过Java编译器将源代码(.java)编译(javac,即java compile)成字节码文件(.class),然后通过类加载器将编译好的字节码文件加载到JVM的内存(方法区)中,然后再通过解释器将加载的字节码解释成机器能够识别的机器码指令,最后由机器(即操作系统)执行这些指令。
  Java能够做到“一次编译,处处运行”,是因为Java编译器的实现是一样的,也就是说相同的Java源代码(.java文件)通过不同的JVM编译出来的字节码都是相同的(这一点通过在windows中编译好的.class文件在linux环境中依然可以正常执行可以得到印证),但是JVM的解释器在不同的操作系统上的实现却不同,因为要将字节码解释成不同的操作系统能够识别的机器码(这一点通过在windows和linux上需要安装不同环境的JDK可以得到印证)。
  类加载器应该是通过操作流的方式加载字节码文件的,即将字节码文件通过流从本地磁盘读入到JVM的方法区,既然类加载器操作的是流,也就可以加载其他的非字节码文件,这也是为何能够通过类加载器加载其他文件的原因。

2、JVM的内存区域
在这里插入图片描述
  方法区:方法区和堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码数据。当方法区无法满足内存分配,将抛出OOM异常。当前的一些框架,如Spring、Hibernate等,会使用CGlib技术对类进行增强,相应地会增加类的大小;还有一些应用,会动态生成JSP文件,JSP文件是需要编译成Class文件的,大量的文件也有溢出的可能;或者开发代码中往常量池添加过多的常量,也有可能造成常量池溢出。另外一种可能就是我们的应用本身的类就太多,而方法区设置的容量不足,也会容易溢出。设置方法区的大小,可通过配置-XX:PremSize 设置最小值,-XX:MaxPremSize设置最大值。方法区也称为永久代, HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区,这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。
  运行时常量池(Runtime Constant Pool):是方法区的一部分。class 文件中除了有类的版
本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  :堆是内存管理中最大的一块,是所有线程共享的一块区域,在虚拟机启动的时候创建,主要存储对象实例。这块内存的大小主要通过启动参数-Xmx进行配置,如果申请的对象实例大小超过该配置的参数,便出现OOM异常。内存泄漏也会导致该区域的OOM。堆又分为新生代和老年代。
  虚拟机栈:和线程的生命周期相同,一个线程每调用一个方法会向虚拟机栈中压入一个栈帧(栈帧的结构:局部变量表、操作数栈、动态链接、方法出口等),在该方法执行完毕后该栈帧会从虚拟机栈中移除。当虚拟机栈的深度大于JVM所允许的深度(可通过-Xss参数配置线程的栈内存大小)时会抛出StackOverflowError异常(例如递归调用时就会抛出该异常),若JVM允许动态扩展,但无法申请到足够的内存时会抛出OutOfMemoryError异常(通常见于多线程情况下,当我们设置的-Xss越大,开启的线程越多出现栈内存OOM的几率也就越大,原因是:操作系统分配给每个进程的内存是有限制的,譬如 32 位的 Windows 限制为 2GB。虚拟机提供了参数来控制 Java 堆(-Xmx)和方法区(-XX:MaxPremSize)的这两部分内存的最大值,剩余的内存为 2GB(操作系统限制)减去 Xmx(最大堆容量),再减去 -XX:MaxPremSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由各个线程的虚拟机栈和本地方法栈瓜分了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,在栈内存扩容时就越容易把剩下的内存耗尽从而导致栈内存溢出,此时可以通过减小堆内存(Xmx)大小或者减小栈内存大小(Xss)或者减少线程数(使用线程池)来进行调整)。
  栈帧(Frame):是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
  本地方法栈:通过本地库接口调用本地方法库中的方法,来完成对文件系统、线程或者其他本地方法的操作。整体来看过程大致是这样的:当我们执行某个类的某个方法的时候,先将这个方法从方法区压入到虚拟机栈中,然后执行这个方法,在执行过程中若该方法调用了操作文件系统或者线程等本地方法的时候会将这些本地方法压入到本地方法栈,本地方法栈再通过本地库接口和本地方法库进行通信实现对本地方法的调用(Java调用C或者C++的方法的原理与此类似)。与虚拟机栈一样,本地方法栈也会抛出OOM以及StackOverflowError异常。
  程序计数器:指向字节码指令执行的位置,占用内存空间很小,是唯一一个无OOM的内存区域。
  直接内存:直接内存(Direct Memory)又称堆外内存,不属于JVM的内存区域,即内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机);由于堆外内存是直接受操作系统管理(而不是虚拟机),这样做的结果就是能保持一个较小的堆内内存,降低垃圾回收的频率,减小对应用的影响。堆内内存由JVM管理,属于“用户态”;而堆外内存由操作系统管理,属于“内核态”。如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由操作系统写入磁盘,使用堆外内存则避免了在 Java 堆和 Native 堆中来回复制数据。但如果堆外内存申请超过物理内存的限制也会出现OOM异常。
  其中,方法区和堆是线程共享的,随着JVM的启动而创建,随着JVM的关闭而消亡;虚拟机栈、本地方法栈和程序计数器是线程私有的,随着线程的创建而创建,随着线程的结束而销毁。
  内存溢出:指内存空间不足,比如配置的堆空间太小不足以盛放进程中的对象或者栈内存扩展时申请不到足够的内存。
  内存泄露:是指不能回收的对象停留在堆中,随着时间推移便会消耗完堆空间触发OOM,比如在for循环中new对象但不释放。

3、JVM运行时内存
在这里插入图片描述
  Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、Survivor From 区和 Survivor To 区)和老年代。
  新生代:用来存放新生的对象。一般占据堆的 1/3 空间。由于应用会频繁创建对象,所以新生代触发 MinorGC 进行垃圾回收会相对比较频繁。新生代又分为 Eden 、Servivor From、Servivor To 三个区。
   Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老
年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
   Servivor From区:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
   Servivor To区: 本次 MinorGC 过程中保留的幸存者。
  MinorGC 的过程:标记->复制->清空->互换,MinorGC 采用复制算法,过程如下:
   ①首先,把 Eden 和 Servivor From 区中标记为存活的对象复制到 Servivor To 区域(如果有对象的年龄已经达到了老年的标准,则复制到老年代区),同时把这些对象的年龄+1(如果 Servivor To 空间不足也放到老年区);
   ②然后清空 Eden 和 Servivor From 区中的对象;
   ③最后,Servivor To 和 Servivor From 互换,原 Servivor To 成为下一次 GC 时的 Servivor From区;
  老年代:主要存放应用程序中生命周期较长的内存对象。老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有被标记的对象。MajorGC 的耗时比较长,因为要先扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM 异常。
  永久代:指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在 JVM 中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory,字符串常量池和类的静态变量放入 Java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。

4、垃圾回收与其算法
  垃圾回收要做的三件事:①明确哪些内存需要回收②怎么回收③什么时候回收
  哪些内存需要回收:需要先将垃圾对象标记出来,有两种方式:①引用计数②可达性分析
   引用计数:引用和对象是有关联的。如果要操作对象则必须通过引用进行。因此,一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单地说,即一个对象如果没有任何与之关联的引用,即它的引用计数为 0,则说明这个对象不太可能再被用到,那么此对象就是可回收对象。但这种方式存在循环引用的问题,即当两个对象相互引用,但没有其他对象引用它们的时候,它们的引用计数都是1,但却应该是可回收的
   可达性分析:以一系列称为GC Roots(GC Roots有三种:①VM栈中的引用②方法区中的静态引用③JNI(本地方法接口)中引用)的根节点作为起点,向下搜索,当一个对象到任何GC Roots都没有引用链相连时则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程,两次标记后仍然是可回收对象,则将面临回收。
  怎么回收:通过垃圾收集器收集垃圾(垃圾收集器有Serial、ParNew、Parallel Scavenge、Serial Old、Paralle Old、CMS(Concurrent Mark Sweep)等),这些垃圾收集器通过以下几种方式回收垃圾:
   标记清除:最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。这种算法效率低、内存碎片多,可能发生大对象不能找到可利用空间的问题
在这里插入图片描述
   复制:为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,然后把已使用的内存清掉。这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,复制效率会大大降低。
在这里插入图片描述
   标记整理:结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
在这里插入图片描述
   分代收集:分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
    ①新生代与复制算法:目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space和To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将这两块空间中还存活的对象复制到另一块 Survivor 空间中。
在这里插入图片描述
    ②老年代与标记整理算法:而老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。
   分区收集:分区算法则将整个堆空间划分为连续的不同的小区间, 每个小区间独立使用、独立回收。这样做的好处是可以控制一次回收多少个小区间,每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。
  垃圾回收的过程大致如下:
   1. 处于方法区的永生代(Permanet Generation)用来存储 class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
   2. 对象的内存分配主要在新生代的 Eden Space,少数情况会直接分配到老生代。
   3. 当新生代的 Eden Space 空间不足时就会发生一次 Minor GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,如果 To Space 没有足够空间存储某个对象,则将这个对象存储到老生代。然后将 Eden Space 和 From Space 进行清理,并将原来的From Space变为To Space,原来的To Space 变为From Space。
   4. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中。
  什么时候回收:根据JVM参数的配置,当满足某个条件时就会触发垃圾回收,JVM参数的配置说明参考:JVM配置说明

5、GC垃圾收集器
  Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制算法;年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器:
在这里插入图片描述
  ①Serial 垃圾收集器:Serial(连续)是最基本的垃圾收集器,使用复制算法,曾经是JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial
垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
  ②ParNew 垃圾收集器:ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。ParNew虽然是除了多线程外和Serial 收集器几乎完全一样,但是ParNew垃圾收集器是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。(JVM的Client模式和Server模式最大的区别在于编译器的不同,Server模式编译的更彻底因此启动的时候较慢,但是启动之后性能更好适合做后台服务)
  ③Parallel Scavenge 收集器:Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
  ④Serial Old 收集器:Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器是 Client 模式下年老代默认的垃圾收集器。 在 Server 模式下,主要有两个用途:
   a、在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用
   b、作为年老代中使用 CMS 收集器的后备垃圾收集方案
  ⑤Parallel Old 收集器:Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。在 JDK1.6 之前,新生代使用 Parallel Scavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
  ⑥CMS 收集器:即Concurrent mark sweep,是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高(高并发场景)的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
   a、初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程
   b、并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程
   c、重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程
   d、并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除和用户线程一起并发工作,所以总体上来看 CMS 收集器的内存回收和用户线程是一起并发地执行的
  ⑦G1 收集器:即Garbage first,是目前垃圾收集器理论发展的最前沿成果,采用分区收集算法。G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。相比于 CMS 收集器,G1 收集器两个最突出的改进是:
   a、基于标记-整理算法,不产生内存碎片
   b、可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收

6、JAVA 中的四种引用类型
  强引用:在 Java 中最常见的就是强引用,把一个对象赋值给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用时,它处于可达状态,是不可能被垃圾回收机制回收的,即
使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之
一。
  软引用:软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,在缓存中适合使用软引用。
  弱引用:弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
  虚引用:虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态,在垃圾回收之前会把这个对象置于引用队列中。

7、JVM类加载机制
  JVM 类加载分为五个阶段:加载、验证、准备、解析、初始化。
在这里插入图片描述
  加载:加载是类加载过程中的第一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。注意这里不一定非要从一个 Class 文件获取,也可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),还可以在运行时计算生成(动态代理)或由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
  验证:这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  准备:准备阶段是正式为类变量分配内存并设置类变量的初始值的阶段,即在方法区中分配这些类变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

public static int v = 8080;

  实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是程序被编译后,存放于类构造器<client>方法之中。
  但是注意如果声明为:

public static final int v = 8080;

  在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v 赋值为 8080。
  解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info等类型的常量。
  初始化:初始化阶段是类加载的最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。初始化阶段是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变
量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子<client>方法执行之前,父类的<client>方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()方法。
  虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,JVM 提供了 3 种类加载器:
  启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的且被虚拟机认可(按文件名识别,如 rt.jar)的类。
  扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
  应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)上的类库。
  JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader 实现自定义的类加载器。
在这里插入图片描述
  双亲委派:当一个类收到了类加载请求,他首先不会自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次的类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
在这里插入图片描述
  另外还有一种场景是:由于类加载器的执行顺序是自下而上的,即自定义类加载器先执行,然后依次是应用类加载器、扩展类加载器、启动类加载器,如果我们在应用程序中自定义了一个类,这个类的包名和类名都和JDK(以JDK为例)提供的类完全相同,比如说我们自定义(注意不是继承,String是不可继承的)了一个类java.lang.String,如果不是通过双亲委派模型的方式来加载,那么就会先将我们自定义的这个java.lang.String加载,在启动类加载器加载rt.jar时发现java.lang.String已经加载过了,就不会再加载,就会使String的不可继承性失去了意义。那为什么类加载器不由上到下执行呢?如果由上到下执行,我们就无法实现自定义(注意不是继承)非final的类了。也就是说双亲委派模型是为了解决final类的加载问题的,即类加载器在加载类时除了查看该类是否在该加载器的路径下,还需要判断该类是否为final的,如果是final的则由父类加载器完成,否则交由子类加载器自己完成,以此来保证final类的安全性(个人理解)。
在这里插入图片描述
8、JVM中的线程
  JVM中的Java线程和原生操作系统的线程有直接的映射关系。当Java线程本地存储、缓
冲区分配、同步对象、栈、程序计数器等准备好以后(Thread.start()方法执行完毕),就会创建一个操作系统原生线程。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕(即start()方法执行完毕),才会调用 Java 线程的 run() 方法(不一定是立即调用,要在抢占到CPU资源后才会调用)。当线程结束时,原生线程随之被回收,会释放原生线程和 Java 线程的所有资源。也就是说我们调用Thread类的start()方法是为了初始化线程,在线程初始化成功之后,操作系统才会真正的创建一个原生线程,待该原生线程抢占到CPU资源后才会真正执行该线程的run()方法。
  JVM 后台运行的系统线程主要有下面几个:
  虚拟机线程:这个线程会等待 JVM 到达安全点操作出现,即该线程会在JVM到达某个安全点时执行相应的安全性操作,这些操作必须要在一个独立的线程(虚拟机线程就是这个独立的线程)中执行。比如当堆修改无法进行时,虚拟机线程就会唤醒GC线程进行垃圾回收,从而保证 JVM 位于安全状态。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除等。可以把虚拟机线程看作JVM的守护神,当发现JVM陷于某种不安全状态时,它会执行相应的操作(比如唤醒GC线程进行垃圾回收)来保证JVM的安全性,而不至于使JVM挂掉;
  周期性任务线程:这些线程负责定时器事件的执行,用来调度周期性任务;
  GC 线程:这些线程负责 JVM 中不同的垃圾回收活动;
  编译器线程(解释器):这些线程在运行时将字节码动态编译成本地平台相关的机器码;
  信号分发线程:这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值