java面试题-java虚拟机(JVM)

来源:互联网 发布:mp3剪切合并软件 编辑:程序博客网 时间:2024/05/18 04:46

1、面试问题连击

GC部分问题连击

什么时候一个对象会被GC?
为什么要在这种时候对象才会被GC?
GC策略都有哪些分类?
这些策略分别都有什么优劣势?都适用于什么场景?
给你举个实际的场景,让你选择一个GC策略?
如果选出来了,继续问你,为什么要选择这个策略?
类加载机制问题连击
Java的类加载器都有哪些?
每个类加载器都加载哪些类?
这些类加载之间的父子关系是怎样的?
什么是双亲委派模型?
为什么Java的类加载器要使用双亲委派模型?
如何自定义自己的类加载器,自己的类加载器和Java自带的类加载器关系如何处理?
JVM内存问题连击
JVM内存分为哪几部分,这些部分分别都存储哪些数据?
一个对象从创建到销毁都是怎么在这些部分里存活和转移的?
内存的哪些部分会参与GC的回收?
Java的内存模型是怎么设计的?为什么要这么设计?
结合内存模型的设计谈谈volatile关键字的作用?
…………

2、 JVM的结构

JVM包括:类加载器classloader)、执行引擎(executionengine)和运行时数据区runtime data area就是常说的JVM内存)。

Java源代码文件被Java编译器编译为字节码文件,然后classloader把硬盘上的class文件加载到运行时数据区类加载器加载完毕后,交由执行引擎执行。JVM中的运行时数据区会用来存储程序执行期间需要用到的数据和相关信息


3. JVM类加载器

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

Bootstrap启动类加载器,是由C++编写的,是所有加载器的父类。当运行Java虚拟机时,这个类加载器被创建,它负责加载虚拟机的核心类库rt.jar,如java.lang.*等。

Extension是BootStrap的子类,ExtClassLoader标准扩展类加载器是它的实,从java.ext.dirs系统属性所指定的目录中加载类库用于加载除了基本API之外的一些拓展类
SystemAppClassLoader应用类加载器系统加载器的实现。用于加载应用程序和程序员自定义的类。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,是用户自定义加载器的默认父加载器。

自定义类加载器:一般只需要继承java.lang.ClassLoader并重写findClass方法。


4. java类加载过程

类加载器的主要工作是将类加载到虚拟机内存中,加载过程包括加载、链接和初始化。其中链接又分为验证、准备和解析。

加载阶段加载阶段可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器完成。类加载器通过类的全名产生对应类的字节流,再将字节流转换为方法区的运行时数据结构。创建代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。加载的方式有:

1.本地编译好的class文件直接加载。

2.使用java.net.URLClassLoader网络加载url指定的类。

3.从jar、zip等等压缩文件加载类,自动解析jar文件找到class文件去加载util类。

链接阶段

验证:验证阶段主要是确保Class文件字节流中包含的信息符合当前虚拟机的要求。包括文件格式验证、元数据验证、字节码验证、符号引用验证。

准备:准备阶段为方法区中的静态变量分配内存空间。并将其赋值为初始值。

解析:解析阶段把符号引用解析为直接引用。符号引用是一个字符串,它唯一标识一个类、字段、方法等目标。直接引用是指向方法区的指针,相对偏移量或是一个能间接定位到目标的句柄。还包含类或接口的解析、字段的解析、类方法解析、接口方法解析。

初始化阶段:初始化阶段是执行类构造器<clinit>()方法的过程。执行用户定义的程序代码。


5. 类加载双亲委派模式

当一个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,一直到启动类加载器。如果父类加载器可以完成类加载任务,就成功返回。如果启动类加载器加载失败,会使用拓展类加载器来尝试加载,继续失败则会使用AppClassLoader来加载,继续失败则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。

双亲委派模型的好处:

(1)避免用户自己编写的类动态替换Java的一些核心类,比如String。

(2)避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类。


6. JVM内存模型(运行时数据区)

java内存包括:程序计数器(Program Counter Register),JVM栈(Java Virtual Machine Stacks),本地方法栈(Native Method Stacks),方法区(Method Area),Java堆(java Heap)和直接内存(Direct Memomry)。直接内存并不是JVM运行时数据区的一部分。

其中程序计数器、JVM栈、本地方法栈是线程私有的,即每个线程各自都有一份。方法区、java堆、直接内存是各个线程共享的。

通常我们定义基本数据类型的变量、对象的引用、函数调用的现场保存在JVM栈空间。而通过new关键字和构造器创建的对象则放在堆空间。

程序计数器(Program Counter Register Register)

程序计数器是用于存放下一条指令所在单元的地址。如果该线程正在执行一个Java方法,则计数器记录的是正在执行的虚拟机字节码地址,如果执行native方法,则计数器值为空。

由于多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器。

JVM栈(Java Virtual Machine Stacks)

JVM栈是线程私有的,生命周期与线程相同。当线程运行完毕后,相应内存就会自动回收。JVM栈描述的是java方法执行的内存模型。每个方法在执行的同时会创建一个栈帧(StackFrame),用于存储局部变量表、操作数栈和方法出口等信息。

这个区域定义了两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果JVM栈可以动态扩展(大部分JVM是可以的),当扩展无法申请到足够内存则抛出OutOfMemoryError异常。

本地方法栈(Native Method Stacks)

本地方法栈与JVM栈很相似,区别在于JVM栈用于执行Java方法,本地方法栈用于执行Native方法。JVM栈一样,这个区域同样会抛出StackOverflowError和OutOfMemoryError异常。

Java堆(java Heap

Java堆是JVM所管理的最大的一块内存。是全局共享的一块内存区域,在虚拟机启动时创建。几乎所有的对象实例都堆中分配。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作GC堆。

Java堆可以细分为:新生代(young)和老年代(old)。新生代又分为:Eden空间、From Survivor空间、To Survivor空间等。在堆中没有足够的内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区(Method Area)

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。由于使用反射机制的原因,虚拟机很难确定哪个类不再使用,因此这块区域难以回收。

当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。

运行时常量池(Runtime Constant Pool)

常量池是方法区的一部分,用于存放类的元数据、字符串池、类的静态变量等信息。在JDK1.7中已经将元数据转移到了直接内存中;字符串池和类的静态变量分配到了java堆中。JDK1.8后已经将永久代(perm)移除,改用元空间(Metaspace),类的元数据将放入元空间中,字符串池和类的静态变量等信息放入Java堆中。元空间分配在直接内存中,因此元空间依赖于内存大小。当然也可以自定义元空间大小。


7. JVM如何判断哪些对象需要回收

JVM中使用可达性分析(Reachability Analysis)来判定对象是否存活。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。可作为GC Roots的对象包括下面几种:

1.虚拟机栈(栈帧中的本地变量表)中引用的对象。

2.方法区中类静态属性引用的对象。

3.方法区中常量引用的对象。

4.本地方法栈中JNI(即一般说的Native方法)引用的对象。

对象状态:

1.可达状态:在一个对象创建后,有一个以上的引用变量引用它。 

2.可恢复状态:如果程序中某个对象不再有任何的引用变量引用它,它将先进入可恢复状态。在这个状态下,系统的垃圾回收机制准备回收该对象的所占用内存,在回收之前,系统会调用finalize()方法进行资源清理,如果资源整理后重新让一个以上引用变量引用该对象,则这个对象会再次变为可达状态;否则就会进入不可达状态。 

3.不可达状态:当对象的所有关联都被切断,且系统调用finalize()方法进行资源清理后依旧没有使该对象变为可达状态,则这个对象将永久性失去引用并且变成不可达状态,系统才会真正的去回收该对象所占用的资源。


8. Java中的四种引用类型

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这4种引用强度依次逐渐减弱。

1.强引用就是指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

2.软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行第二次回收。通过SoftReference类来实现软引用SoftReference p = new SoftReference(new Object())

3.弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。通过WeakReference类来实现弱引用WeakReference p = new WeakReference(new Object())

4.虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现。


9. 垃圾回收算法

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

标记-清除算法是最基础的收集算法,分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记-清除算法实现起来比较简单,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致要为大对象分配空间时无法找到足够的空间而提前触发GC。并且标记和清除两个过程的效率都不高

Copying(复制)算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把第一块内存上的空间清理掉,这样就不会出现内存碎片的问题,并且运行高效。

但是该算法导致能够使用的内存缩减到原来的一半。而且该算法的效率跟存活对象的数目有很大的关系,如果存活对象很多,那么复制算法的效率将会大大降低。

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

为了解决复杂算法的缺陷,充分利用内存空间,提出了标记-整理算法。

该算法标记阶段标记出所有需要被回收的对象,但是在完成标记之后不是直接清理可回收对象,而是将存活的对象都移向一端,然后清理掉端边界以外的内存。

Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。分代收集根据对象存活周期的不同将内存划分为几块。一般是将堆划分为老年代(old)和新生代(young),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以在不同区域采取不同的收集算法。

对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象。在默认情况下Eden空间和两块Survivor空间比例为8:1:1,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和s1空间。在进行了一次GC之后,使用的便是Eden和s2空间了,下次GC时会将存活对象复制到s1空间。

当对象在Survivor区躲过一次GC,其对象年龄便会加1,默认情况下,对象年龄达到15时,就会移动到老年代中。一般来说,大对象会被直接分配到老年代,所谓的大对象是指需要大量连续存储空间的对象。当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和JVM的相关参数。

对于老年代,因为每次回收都只回收少量对象,所以老年代一般使用的是标记整理算法。


10. 垃圾回收器

Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,采取复制算法。Serial收集器简单而高效,但在它进行垃圾收集时,必须暂停其他所有的工作线程,直到收集结束。到目前为止,它依然是虚拟机运行在Client模式下的默认新生代收集器。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,使用多条线程进行垃圾收集,其余的行为都与Serial收集器完全一样。ParNew目前是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew都一样。而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。与Parallel Scavenge收集器组合使用。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记—清除”算法实现,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)、并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要暂停所有的工作线程。初始标记只是标记一下GC Roots能直接关联到的对象。并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的标记记录。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。CMS主要优点在名字上已经体现出来了:并发收集、低停顿。但是CMS有以下3个明显的缺点:

CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量会降低。

CMS收集器无法处理浮动垃圾(Floating Garbage)。由于在垃圾收集阶段用户线程还需要运行,那就需要预留足够的内存空间给用户线程使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

CMS是一款基于“标记—清除”算法实现的收集器,就会在收集结束时产生空间碎片。空间碎片过多时对大对象的分配往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象而触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX +UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,但是会使得停顿时间变长。

G1收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,G1是一款面向服务端应用的垃圾收集器。它能充分利用多CPU、多核环境实现并行与并发,包含分代收集、空间整合,并且它能建立可预测的停顿时间模型。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。


11. JVM何时触发GC操作

GC是后台一个低优先级的守护进程。在内存中低到一定限度时才会自动运行,因此垃圾回收的时间是不确定的。

Minor GC:从年轻代空间(包括 Eden 和 Survivor )回收内存。MinorGC触发条件:

(1)当Eden区满时,触发Minor GC。

Major GC:从老年代空间回收内存。Major GC触发条件:

(1)老年代空间不足。

(2)方法区空间不足。

(3)通过Minor GC后进入老年代的平均大小大于老年代的可用内存。

Full GC:清理整个堆空间,包括年轻代和老年代。Full GC触发条件:

(1)调用System.gc时,建议系统执行Full GC,但是不是必然执行。

(2)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。


12. 遇到java.lang.OutOfMemoryError时应该怎样处理

Java堆内存的OOM异常是实际应用中常见的内存溢出异常。当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着“Java heap space”。要解决这个区域的异常,一般是先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照(通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照)进行分析,确认内存中的对象是否是必要的,也就是要分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。找到泄露对象是通过怎样的路径与GC Roots相关联导致垃圾收集器无法自动回收它们的,然后定位出泄露代码的位置。如果不存在泄露,就是内存中的对象确实都必须的,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。


13.JVM常见参数:

-Xmx指定java程序的最大堆内存

-Xms指定最小堆内存, 通常设置成跟最大堆内存一样,减少GC

-Xmn设置年轻代大小。整个堆大小=年轻代大小加老年代大小。所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-Xss指定线程的最大栈空间此参数决定了java函数调用的深度, 值越大调用深度越深, 若值太小则容易出栈溢出错误。

-XX:PermSize指定永久区的初始值默认是物理内存的1/64在Java1.8移除, 由元数据区代替,-XX:MetaspaceSize指定

-XX:MaxPermSize:指定永久区的最大值, 默认是物理内存的1/4在java1.8由-XX:MaxMetaspaceSize指定元数据区的大小

-XX:NewRatio=n设置老年代与年轻代的比值。-XX:NewRatio=2,表示老年代与年轻代的比值为2:1

-XX:SurvivorRatio=n设置Eden区与Survivor区的大小比值,-XX:SurvivorRatio=8表示Eden区与Survivor区的大小比值是8:1:1。


14.Java内存模型

java内存模型(Java Memory Model),不同于Java运行时数据区,JMM是定义JVM在计算机内存中的工作方式,即在虚拟机将变量存储到内存和从内存中读取数据的细节。在多核系统中,处理器一般会设置缓存来加速数据的读取,缓存极大的提升了程序的性能,却也带来了“缓存一致性”的问题。java的内存模型采用的是共享内存的线程通信机制,线程的共享变量存储在主内存中,而每个线程都有一个私有的工作内存,工作内存中存储了共享变量的副本。线程对变量的所有操作都要在工作内存中进行,不能直接访问主内存。线程间变量传递均需主内存间接完成。

要完成主内存与工作内存的交互操作,需遵守一定的规则。java内存模型定义了8种原子性的操作:lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)。内存交互时,必须使用以上几种操作搭配完成,且这8种操作要满足一定规则,如read和load,store和write必须成对出现;对变量实施use、store时,必须先执行assign和load操作。

15. Java 内存模型中的原子性、可见性和有序性。

原子性(Atomicity)原子性是指在一个操作中,cpu不可以在中途暂停然后再调度。就是这个操作是不可分割的。非原子操作都会存在线程安全问题。java内存模型定义了8种原子性的操作,这样可以保证对基本数据类型的访问读写是原子性的。

可见性(Visibility):可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果其它线程能够立即得知这个修改JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。

有序性(Ordering):Java内存模型中,如果在同一线程观察,所有操作都是有序的。而如果在一个线程观察另一线程,所有操作都是无序的。要完成主内存与工作内存的交互操作,需遵守一定的规则,这些难以记忆的规则有一个等效判定的原则“先行发生原则”。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。

1. 程序次序规则(Pragram Order Rule):在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作。

2. 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。后面是指时间上的先后顺序。

3.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作。

4.线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。

5.线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测。

6.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

7.对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。

8.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

16. volatile关键字

编译器能够自由的对所有的操作进行优化优化而重排序。在单线程环境中,程序执行结果不会受到指令重排序的影响。在多线程环境下,并不希望发生指令重排序而影响并发结果。volatile是java提供的轻量的同步机制来保证线程之间操作的有序性。它有两个特性: 

1. 保证修饰的变量对所有线程的可见性。 
2. 禁止指令的重排序优化。

加入volatile关键字时所生成的汇编代码会多出一个lock前缀指令,lock前缀指令相当于一个内存屏障,内存屏障会提供3个功能:

1.确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面

2.强制将对缓存的修改操作立即写入主存;

3.写操作会导致其他CPU中对应的缓存行无效。

volatile关键字修饰的变量保证了变量的可见性和有序性,但是并不能保证变量操作的原子性,因此不能完全保证线程的安全性。



原创粉丝点击