听我一言:99%程序员都扛不住这JVM面试43问

最近很多粉丝朋友让我安排一波面试精选例题解析,小编搜罗全网最终找出了一些能帮助大家进阶提升的JVM面试题解析,第一波是43问,后续还会持续更新!

JVM面试43问,细节诠释经典!

1,Java中会存在内存泄漏吗,请简单描述。

会。自己实现堆载的数据结构时有可能会出现内存泄露,可参看effective java.

2,64位JVM中,int的长度是多数?

Java中 , int类型变量的长度是一个固定值,与平台无关,都是32位。意思就是说,在32位和64位的Java虚拟机中,int类型的长度是相同的。

3,Serial 与Parallel GC之间的不同之处?

Serial与Parallel在GC执行的时候都会引起stop-the-world。它们之间主要不同serial收集器是默认的复制收集器,执行GC的时候只有一个线程,而parallel收集器使用多个GC线程来执行。

4,32位和64位的JVM , int类型变量的长度是多数?

32位和64位的JVM中,int类型变量的长度是相同的,都是 32位或者4个字节。

5,Java中WeakReference与SoftReference的区别?

虽然WeakReference与SoftReference都有利于提高GC和内存的效率,但是WeakReference,一旦失去最后一个强引用,就会被GC回收,而软引用虽然不能阻止被回收,但是可以延迟到JVM内存不足的时候。

6,JVM选项-XX:+UseCompressedOops有什么作用?为什么要使用

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

7,怎样通过Java程序来判断JVM是32位还是64位?

你可以检查某些系统属性如sun.arch.data.model或 os.arch来获取该信息。

8,32位JVM和64位JVM的最大堆内存分别是多数?

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

9,JRE、JDK、JVM及JIT之间有什么不同?

JRE代表Java运行时(Java run-time ),是运行Java引用所必须的。JDK代表Java开发工具(Java development kit ),是Java程序的开发工具,如Java编译器,它也包含JRE。M代表Java虚拟机(Java virtual machine ),它的责任是运行Java应用。JIT代表即时编译(Jjust In Time compilation ),当代码执行的次数超过一定的阈值时,会将Java字节码转换为本地代码,如,主要的热点代码会被准换为本地代码,这样有利大幅度提高Java应用的性能。

10,解释Java堆空间及GC?

当通过Java命令启动Java进程的时候,会为它分配内存。内存的一部分用于创建堆空间,当程序中创建对象的时候,就从对空间中分配内存。GC是JVM内部的一个进程,回收无效对象的内存用于将来的分配。

11,JVM内存区域

 

JVM内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA堆、方法区】、直接内存。

线程私有数据区域生命周期与线程相同,依赖用户线程的启动/结束而创建/销毁(在Hotspot VM内,每个线程都与操作系统的本地线程直接映射,因此这部分内存区域的存/否跟随本地线程的生/死对应)。

线程共享区域随虚拟机的启动/关闭而创建/销毁。

直接内存并不是JMM运行时数据区的一部分,但也会被频繁的使用:在JDK1.4引入的NIO提供了基于Channel与Buffer的I0方式可以使用Native函数库直接分配堆外内存,然后使用DirectByteBuffer对象作为这块内存的引用进行操作(详见:Java l/O扩展),这样就避免了在Java堆和Native堆中来回复制数据,因此在一些场景中可以显著提高性能。

 

12,程序计数器(线程私有)

一块较小的内存空间,是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有"的内存。

正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是Native方法,则为空。这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。

13,虚拟机栈(线程私有)

是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧( Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

栈帧(Frame )是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

 

14,本地方法区(线程私有)

本地方法区和Java Stack作用类似,区别是虚拟机栈为执行Java方法服务,而本地方法栈则为Native方法服务,如果一个VM实现使用C-linkage模型来支持Native调用,那么该栈将会是一个C栈,但HotSpot VM直接就把本地方法栈和虚拟机栈合二为一。

15,你能保证GC 执行吗?

不能,虽然你可以调用System.gc()或者Runtime.gc(),但是没有办法保证GC的执行。

16,怎么获取Java程序使用的内存?堆使用的百分比?

可以通过java.lang.Runtime类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余空间。Runtime.freeMemory()方法返回剩余空间的字节数,Runtime.totalMemory0)方法总内存的字节数,

Runtime.maxMemory()返回最大内存的字节数。

17,Java中堆和栈有什么区别?

JVM中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个JVM的所有线程共享。

18,描述一下JVM加载class文件的原理机制

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

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

加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对

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

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

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

加载器的说明:

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

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

3.System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。

19,GC是什么?为什么要有Gc?

GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到―动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc()或Runtime.getRuntime().gc(),但JVM可以屏蔽掉显示的垃圾回收调用。

垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java 诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东。移动智能终端用户通常觉得iOS的系统比 Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。

20,堆( Heap-线程共享)-运行时数据区

是被线程共享的一块内存区域,创建的对象和数组都保存在Java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代VM采用分代收集算法,因此Java堆从 GC的角度还可以细分为:新生代(Eden区、From Survivor区和To Survivor区)和老年代。

21,方法区/永久代(线程共享)

即我们常说的永久代(Permanent Generation),用于存储被JM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据.HotSpot VM把GC分代收集扩展至方法区,即使用Java堆的永久代来实现方法区,这样HotSpot 的垃圾收集器就可以像管理Java堆一样管理这部分内存而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载,因此收益一般很小)。

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

22,JVM运行时内存

Java堆从GC的角度还可以细分为:新生代(Eden区、 From Survivor区和To Survivor区)和老年代。

 

23,新生代

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

Eden区

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

servivorFrom

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

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

1 : eden、servicorFrom复制到ServicorTo,年龄+1

首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区);

2∶清空eden、 servicorFrom

然后,清空Eden和ServicorFrom中的对象﹔3 : ServicorTo和ServicorFrom互换

最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。

24,老年代

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

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

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

25,永久代

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

26,JAVA8与元数据

在Java8中,永久代已经被移除,被一个称为"元数据区"(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于︰元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入

nativememory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

27,引用计数法

在Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为О,则说明对象不太可能再被用到,那么这个对象就是可回收对象。

28,可达性分析

为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。通过一系列的"GC roots"对象作为起点搜索。如果在"GC roots和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

29,标记清除算法(Mark-Sweep )

最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图

 

从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

30,复制算法( copying )

为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:

 

这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。

31,标记整理算法(Mark-Compact)

结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图︰

 

32,分代收集算法

分代收集法是目前大部分JMM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(YoungGeneration)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

33,新生代与复制算法

目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1∶1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden 空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。

 

34,老年代与标记复制算法

而老年代因为每次只回收少量对象,因而采用Mark-Compact算法。

1.JAVA虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。

⒉对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。

3.当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,EdenSpace和From Space区的存活对象会被挪到To Space,然后将Eden Space和FromSpace进行清理。

4.如果To Space无法足够存储某个对象,则将这个对象存储到老生代。5.在进行GC后,使用的便是Eden Space和To Space 了,如此反复循环。

6.当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。

35,JAVA强引用

在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JM也不会回收。因此强引用是造成Java 内存泄漏的主要原因之一。

36,JAVA软引用

软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

37,JAVA弱引用

弱引用需要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JM的内存空间是否足够,总会回收该对象占用的内存。

38,JAVA虚引用

虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

39,分代收集算法

当前主流VM垃圾收集都采用"分代收集"(Generational Collection)算法,这种算法会根据对象存活周期的不同将内存划分为几块,如JVM中的新生代、老年代、永久代,这样就可以根据各年代特点分别采用最适当的GC算法

40,在新生代-复制算法

每次垃圾收集都能发现大批对象已死,只有少量存活.因此选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集

41,在老年代-标记整理算法

因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记—清理"或"标记―整理"算法来进行回收.不必进行内存复制,且直接腾出空闲内存。

42,分区收集算法

分区算法则将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收.这样做的好处是可以控制一次回收多少个小区间,根据目标停顿时间,每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。

43,GC垃圾收集器

Java堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;年老代主要使用标记-整理垃圾回收算法,因此java虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器,JDK1.6中Sun HotSpot 虚拟机的垃圾收集器如下︰

 

总结:

JVM这期后续还有42道,喜欢的朋友们可以关注小编,近期持续分享面试干货!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值