JVM面试系列-02

2022年7月17日
大约 18 分钟

JVM面试系列-02

1. GC是什么?为什么要有GC?

GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。

Java开发人员不必担心内存管理问题,这是因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用一下方法:

System.gc() 
Runtime.getRuntime().gc() 

2. Java 中存在内存泄漏吗?

Java中存在内存泄漏。

Java有垃圾回收机制(GC)一般不会存在内存泄露问题,这也是Java被广泛应用于服务器端编程的一个重要原因;然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。

例如Hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。

3. 64 位 JVM 中 int 类型长度是多少?

Java中int类型变量的长度是一个固定值与平台无关,都是32位。

意思就是说在32位和64位的Java虚拟机中int类型的长度是相同的。

4. Serial 与 Parallel GC 之间有什么区别?

Serial和Parallel在GC执行的时候都会引起stop-the-world。

它们之间主要不同在于serial收集器是默认的复制收集器,执行GC的时候只有一个线程,而parallel收集器使用多个GC线程来执行。

5. Java 中 WeakReference 和 SoftReference 有什么区别?

WeakReference和SoftReference都有利于提高GC和内存的效率。

WeakReference一旦失去最后一个强引用,就会被 GC回收,而软引用虽然不能阻止被回收,但是可以延迟到JVM内存不足的时候。

6. JVM 中 -XX:+UseCompressedOops 选项有什么作用?

当将应用从32位的JVM迁移到64位的JVM时,由于对象的指针从32位增加到了64位,因此堆内存会突然增加,差不多要翻倍。这也会对CPU缓存(容量比内存小很多)的数据产生不利的影响。因为,迁移到64位的JVM主要动机在于通过压缩可以指定最大堆大小。

OOP可以节省一定的内存。通过-XX:+UseCompressedOops选项,JVM会使用32位的OOP,而不是64位的OOP。

new一个空对象在32位系统中占用内存大小是8字节(对象头,在堆中)+4字节(对象的引用地址,在栈中)=12字节;

new一个空对象在64位系统中占用内存大小是16字节(对象头,在堆中)+8字节(对象的引用地址,在栈中)=24字节。

7. JVM 中 32 位和 64 位的最大堆内存分别是多少?

理论上说上32位的JVM堆内存可以到达2^32,即4GB,但实际上会比这个小很多。

不同操作系统之间JVM堆内存不同,如Windows系统大约1.5GB,Solaris大约3GB。64位JVM允许指定最大的堆内存,理论上可以达到2^64,这是一个非常大的数字,实际上可以指定堆内存大小到100GB。甚至有的JVM,如Azul,堆内存到1000G都是可能的。

8. 描述一下 JVM 加载 class 文件的原理机制?

JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。

由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。

加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。

类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。

从Java2(JDK1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。

加载器的说明:

1、Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);

2、Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;

3、System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性。

4、java.class.path:所指定的目录中记载类,是用户自定义加载器的默认父加载器。

9. 说一说 Java 中方法区\永久代(线程共享)?

永久代(Permanent Generation)用于存储被 JVM 加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据。

HotSpot VM把GC分代收集扩展至方法区,即使用Java堆的永久代来实现方法区,这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载,因此收益一般很小)。

运行时常量池(Runtime Constant Pool)是方法区的一部分。

Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 Java虚拟机对Class文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

10. Java 中什么是新生代?

新生代是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。

新生代又分为Eden区、ServivorFrom、ServivorTo 三个区。

Eden区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。

ServivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者。

ServivorTo:保留了一次 MinorGC 过程中的幸存者。 MinorGC 的过程(复制->清空->互换)

MinorGC 采用复制算法。

1、Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo不够位置就放到老年区); 2、清空Eden和ServicorFrom中的对象; 3、ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。

11. Java 中什么是老年代?

老年代主要存放应用程序中生命周期长的内存对象。

老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

MajorGC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。ajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

12. Java 中什么是永久代?

永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而爆满,最终抛出OOM异常。

13. Java 中什么是可达性分析?

为了解决引用计数法的循环引用问题, Java 使用了可达性分析的方法。

通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。

需要注意的是不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

14. Java 中什么是 Serial 垃圾收集器?

Serial(英文连续)是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1之前新生代唯一的垃圾收集器。

Serial是一个单线程的收集器, 它不但只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。

Serial垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。

15. Java 中什么是 ParNew 垃圾收集器?

ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和Serial 收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。

ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。

【Parallel:平行的】

ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器。

16. Java 中什么是 Parallel Scavenge 收集器?

Parallel Scavenge收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。

17. Java 中什么是 CMS 收集器?

Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间, 和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS工作机制相比其他的垃圾收集器来说更复杂。

整个过程分为以下4个阶段:

初始标记

只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

并发标记

进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。

重新标记

为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。

并发清除

清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作, 所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。

18. Java 中G1 收集器有什么作用?

Garbage first垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与CMS收集器。

G1收集器两个最突出的改进是:

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

2、可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间, 优先回收垃圾最多的区域。

区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率

19. 描述一下 JVM 类加载的过程?

Java中的所有类都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,开发人员几乎不需要关心类的加载,因为这些都是隐式装载的,除非有特殊的用法,比如反射,就需要显式的加载所需要的类。

类装载方式,有两种 :

1、隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。 2、显式装载, 通过class.forname()等方法,显式加载需要的类。

Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

Java的类加载器有三个,对应Java的三种类:

1)Bootstrap Loader:启动类加载器,是虚拟机自身的一部分。负责将存放在\lib目录中的类库加载到虚拟机中。其无法被Java程序直接引用。 负责加载系统类 (指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类)。 2)ExtClassLoader:负责加载扩展类(就是继承类和实现类)。 3)AppClassLoader:负责加载用户类路径(ClassPath)上所指定的类库(程序员自定义的类)。

JVM中类的加载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。

由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化

类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。

当类被加载后就进入连接阶段,这一阶段包括:

验证:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备:为静态变量分配内存并设置默认的初始值。

解析:将符号引用替换为直接引用。

最后JVM对类进行初始化,包括:

1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类; 2)如果类中存在初始化语句,就依次执行这些初始化语句。

类的加载是由类加载器完成的,类加载器包括:启动类加载器(BootStrap)、扩展类加载器(Extension)、应用程序类加载器(Application)。

从Java 2(JDK 1.2)开始,类加载过程采取了双亲委派模型(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是启动类加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。

双亲委派模型:要求除了顶层的启动类加载器外,其余加载器都应当有自己的父类加载器。类加载器之间的父子关系,一般不会以继承的关系来实现,而是通过组合关系复用父加载器的代码。

工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。

每个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有到父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。

20. Java 中什么是 OSGI( 动态模型系统)?

OSGi(Open Service Gateway Initiative)技术是Java动态化模块化系统的一系列规范。OSGi一方面指维护OSGi规范的OSGI官方联盟,另一方面指的是该组织维护的基于Java语言的服务(业务)规范。简单来说,OSGi可以认为是Java平台的模块层。

OSGi服务平台向Java提供服务,这些服务使Java成为软件集成和软件开发的首选环境。Java提供在多个平台支持产品的可移植性。

OSGi技术提供允许应用程序使用精炼、可重用和可协作的组件构建的标准化原语,这些组件能够组装进一个应用和部署中。