JVM理论知识面试大全

1.1 JVM组成

  • 字节码指令集

  • 程序寄存器

  • 虚拟机栈

  • 虚拟机堆

  • 方法区

  • 垃圾回收器

1.2 Java程序运行过程

(1)Java源文件被编译器编译成字节码文件

(2)JVM将字节码文件编译成相应操作系统的机器码。

(3)机器码调用相应操作系统的本地方法库执行相应的方法。

img

1.3 JVM的内存区域

  • 线程私有区域

    • 程序计数器:该方法的程序计数器记录的是实时虚拟机字节码指令的地址
    • 虚拟机栈
    • 本地方法区
  • 线程共享区域

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

1.4 JVM的运行时内存

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

新生代:Eden区、ServivorTo区和ServivorFrom区

JVM新创建的对象(除了大对象外)会被存放在新生代,默认占 1/3堆内存空间。由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为Eden区、ServivorTo区和ServivorFrom区,如下所述。

(1)Eden区:Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代。大对象的定义和具体的JVM版本、堆大小和垃圾回收策略有关,一般为 2KB~128KB,可通过XX:PretenureSizeThreshold设置其大小。在Eden区的内存空间不足时会触发MinorGC,对新生代进行一次垃圾回收。

(2)ServivorTo区:保留上一次MinorGC时的幸存者。

(3)ServivorFrom区:将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者。

新生代的GC过程叫作MinorGC,采用复制算法实现,具体过程如下。

(1)把在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区。如果某对象的年龄达到老年代的标准(对象晋升老年代的标准由XX:MaxTenuringThreshold设置,默认为 15),则将其复制到老年代,同时把这些对象的年龄加 1;如果ServivorTo区的内存空间不够,则也直接将其复制到老年代;如果对象属于大对象(大小为 2KB~128KB的对象属于大对象,例如通过XX:PretenureSizeThreshold=2097152设置大对象为 2MB,1024×1024×2Byte=2097152Byte=2MB),则也直接将其复制到老年代。

(2)清空Eden区和ServivorFrom区中的对象。

(3)将ServivorTo区和ServivorFrom区互换,原来的ServivorTo区成为下一次GC时的ServivorFrom区。

老年代

老年代主要存放有长生命周期的对象和大对象。老年代的GC过程叫作MajorGC。在老年代,对象比较稳定,MajorGC不会被频繁触发。在进行MajorGC前,JVM会进行一次MinorGC,在MinorGC过后仍然出现老年代空间不足或无法找到足够大的连续空间分配给新创建的大对象时,会触发MajorGC进行垃圾回收,释放JVM的内存空间。

MajorGC采用标记清除算法,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放内存空间。

因为要先扫描老年代的所有对象再回收,所以MajorGC的耗时较长。MajorGC的标记清除算法容易产生内存碎片。在老年代没有内存空间可分配时,会抛出Out Of Memory异常。

永久代

永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。Class在类加载时被放入永久代。永久代和老年代、新生代不同,GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载的Class文件过多时会抛出Out Of Memory异常,比如Tomcat引用Jar文件过多导致JVM内存不足而无法启动。

需要注意的是,在Java 8中永久代已经被元数据区(也叫作元空间)取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据区并没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因此,元空间的大小不受JVM内存的限制,只和操作系统的内存有关。

在Java 8中,JVM将类的元数据放入本地内存(Native Memory)中,将常量池和类的静态变量放入Java堆中,这样JVM能够加载多少元数据信息就不再由JVM的最大可用内存(MaxPermSize)空间决定,而由操作系统的实际可用内存空间决定。

如何确定垃圾

  • 引用计数法
  • 可达性分析

1.5 Java中常用的垃圾回收算法

标记清除

其过程分为标记和清除两个阶段。在标记阶段标记所有需要回收的对象,在清除阶段清除可回收的对象并释放其所占用的内存空间。

缺点:没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内存碎片化的问题

复制算法

复制算法首先将内存划分为两块大小相等的内存区域,即区域 1和区域 2,新生成的对象都被存放在区域 1中,在区域 1内的对象存储满后会对区域 1进行一次标记,并将标记后仍然存活的对象全部复制到区域 2中,这时区域 1将不存在任何存活的对象,直接清理整个区域 1的内存即可

缺点:浪费内存

标记整理

标记整理算法结合了标记清除算法和复制算法的优点,其标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存

分代收集算法

无论是标记清除算法、复制算法还是标记整理算法,都无法对所有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾回收。因此,针对不同的对象类型,JVM采用了不同的垃圾回收算法,该算法被称为分代收集算法。

新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收时都有大量的对象被回收;采用复制算法

老年代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。老年代主要存放生命周期较长的对象和大对象,因而每次只有少量非存活的对象被回收,因而在老年代采用标记清除算法。

img

1.6 Java中的4种引用类型

(1)强引用

(2)软引用

(3)弱引用

(4)虚引用

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

新生代与复制算法

新生代主要存储短生命周期的对象,因此在垃圾回收的标记阶段会标记大量已死亡的对象及少量存活的对象,因此只需选用复制算法将少量存活的对象复制到内存的另一端并清理原区域的内存即可。

老年代与标记整理算法

老年代主要存放长生命周期的对象和大对象,可回收的对象一般较少,因此JVM采用标记整理算法进行垃圾回收,直接释放死亡状态的对象所占用的内存空间即可。

1.8 垃圾收集器

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

Serial垃圾收集器基于复制算法实现,它是一个单线程收集器,在它正在进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束。

Serial垃圾收集器采用了复制算法,简单、高效,对于单CPU运行环境来说,没有线程交互开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器是Java虚拟机运行在Client模式下的新生代的默认垃圾收集器。

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

ParNew垃圾收集器是Serial垃圾收集器的多线程实现,同样采用了复制算法,它采用多线程模式工作,除此之外和Serial收集器几乎一样。ParNew垃圾收集器在垃圾收集过程中会暂停所有其他工作线程,是Java虚拟机运行在Server模式下的新生代的默认垃圾收集器。

ParNew垃圾收集器默认开启与CPU同等数量的线程进行垃圾回收,在Java应用启动时可通过-XX:ParallelGCThreads参数调节ParNew垃圾收集器的工作线程数。

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

Parallel Scavenge收集器是为提高新生代垃圾收集效率而设计的垃圾收集器,基于多线程复制算法实现,在系统吞吐量上有很大的优化,可以更高效地利用CPU尽快完成垃圾回收任务。

Parallel Scavenge通过自适应调节策略提高系统吞吐量,提供了三个参数用于调节、控制垃圾回收的停顿时间及吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数,控制吞吐量大小的-XX:GCTimeRatio参数和控制自适应调节策略开启与否的UseAdaptiveSizePolicy参数。

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

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

新生代的Serial垃圾收集器和老年代的Serial Old垃圾收集器可搭配使用,分别针对JVM的新生代和老年代进行垃圾回收,其垃圾收集过程如图 1-15所示。在新生代采用Serial垃圾收集器基于复制算法进行垃圾回收,未被其回收的对象在老年代被Serial Old垃圾收集器基于标记整理算法进行垃圾回收。

img

图1-15

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

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

新生代的Parallel Scavenge垃圾收集器和老年代的Parallel Old垃圾收集器的搭配运行过程如图 1-16所示。新生代基于Parallel Scavenge垃圾收集器的复制算法进行垃圾回收,老年代基于Parallel Old垃圾收集器的标记整理算法进行垃圾回收。

img

CMS垃圾收集器

CMS(Concurrent Mark Sweep)垃圾收集器是为老年代设计的垃圾收集器,其主要目的是达到最短的垃圾回收停顿时间,基于线程的标记清除算法实现,以便在多线程并发环境下以最短的垃圾收集停顿时间提高系统的稳定性。

CMS的工作机制相对复杂,垃圾回收过程包含如下4个步骤。

(1)初始标记:只标记和GC Roots直接关联的对象,速度很快,需要暂停所有工作线程。

(2)并发标记:和用户线程一起工作,执行GC Roots跟踪标记过程,不需要暂停工作线程。

(3)重新标记:在并发标记过程中用户线程继续运行,导致在垃圾回收过程中部分对象的状态发生变化,为了确保这部分对象的状态正确性,需要对其重新标记并暂停工作线程。

(4)并发清除:和用户线程一起工作,执行清除GC Roots不可达对象的任务,不需要暂停工作线程。

CMS垃圾收集器在和用户线程一起工作时(并发标记和并发清除)不需要暂停用户线程,有效缩短了垃圾回收时系统的停顿时间,同时由于CMS垃圾收集器和用户线程一起工作,因此其并行度和效率也有很大提升。CMS收集器的工作流程如图1-17所示。

img

G1垃圾收集器

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

◎ 基于标记整理算法,不产生内存碎片。

◎ 可以精确地控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾回收。

1.9 JVM的类加载过程

加载

指JVM读取Class文件,并且根据Class文件描述创建java.lang.Class对象的过程。类加载过程主要包含将Class文件读取到运行时区域的方法区内,在堆中创建java.lang.Class对象,并封装类在方法区的数据结构的过程,在读取Class文件时既可以通过文件的形式读取,也可以通过jar包、war包读取,还可以通过代理自动生成Class或其他方式读取。

验证

主要用于确保Class文件符合当前虚拟机的要求,保障虚拟机自身的安全,只有通过验证的Class文件才能被JVM加载。

准备

主要工作是在方法区中为类变量分配内存空间并设置类中变量的初始值。初始值指不同数据类型的默认值,这里需要注意final类型的变量和非final类型的变量在准备阶段的数据初始化过程不同。

解析

JVM会将常量池中的符号引用替换为直接引用。

初始化

主要通过执行类构造器的方法为类进行初始化。方法是在编译阶段由编译器自动收集类中静态语句块和变量的赋值操作组成的。JVM规定,只有在父类的方法都执行成功后,子类中的方法才可以被执行。在一个类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成方法。

在发生以下几种情况时,JVM不会执行类的初始化流程。

◎ 常量在编译时会将其常量值存入使用该常量的类的常量池中,该过程不需要调用常量所在的类,因此不会触发该常量类的初始化。

◎ 在子类引用父类的静态字段时,不会触发子类的初始化,只会触发父类的初始化。

◎ 定义对象数组,不会触发该类的初始化。

◎ 在使用类名获取Class对象时不会触发类的初始化。

◎ 在使用Class.forName加载指定的类时,可以通过initialize参数设置是否需要对类进行初始化。

◎ 在使用ClassLoader默认的loadClass方法加载类时不会触发该类的初始化。

img

1.10 类加载器

(1)启动类加载器:负责加载Java_HOME/lib目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库。

(2)扩展类加载器:负责加载Java_HOME/lib/ext目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库。

(3)应用程序类加载器:负责加载用户路径(classpath)上的类库。

除了上述 3种类加载器,我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

img

1.11 JVM类加载机制-双亲委派机制

JVM通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的Class文件在父类的类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFoud异常。

双亲委派类加载机制的类加载流程如下,如图1-22所示。

(1)将自定义加载器挂载到应用程序类加载器。

(2)应用程序类加载器将类加载请求委托给扩展类加载器。

(3)扩展类加载器将类加载请求委托给启动类加载器。

(4)启动类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由扩展类加载器加载。

(5)扩展类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由应用程序类加载器加载。

(6)应用程序类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由自定义加载器加载。

(7)在自定义加载器下查找并加载用户指定目录下的Class文件,如果在自定义加载路径下未找到目标Class文件,则抛出ClassNotFoud异常。

img

图1-22

双亲委派机制的核心是保障类的唯一性和安全性。例如在加载rt.jar包中的java.lang.Object类时,无论是哪个类加载器加载这个类,最终都将类加载请求委托给启动类加载器加载,这样就保证了类加载的唯一性。如果在JVM中存在包名和类名相同的两个类,则该类将无法被加载,JVM也无法完成类加载流程。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值