java虚拟机面试题(JVM)

1、JVM内存分哪几个区,每一个区的做用是什么? 

java虚拟机主要分为如下几个区:java

方法区:
1. 有时候也成为永久代,在该区内不多发生垃圾回收,可是并不表明不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载
2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
3. 该区域是被线程共享的。
4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具备动态性,也就是说常量并不必定是编译时肯定,运行时生成的常量也会存在这个常量池中。程序员

虚拟机栈:
1. 虚拟机栈也就是咱们日常所称的栈内存,它为java方法服务,每一个方法在执行的时候都会建立一个栈帧,用于存储局部变量表、操做数栈、动态连接和方法出口等信息。
2. 虚拟机栈是线程私有的,它的生命周期与线程相同。
3. 局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有多是指向对象起始地址的一个指针,也有多是表明对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间肯定
4.操做数栈的做用主要用来存储运算结果以及运算的操做数,它不一样于局部变量表经过索引来访问,而是压栈和出栈的方式
5.每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接.动态连接就是将常量池中的符号引用在运行期转化为直接引用。算法

本地方法栈
本地方法栈和虚拟机栈相似,只不过本地方法栈为Native方法服务。数组


java堆是全部线程所共享的一块内存,在虚拟机启动时建立,几乎全部的对象实例都在这里建立,所以该区域常常发生垃圾回收操做。缓存

程序计数器
内存空间小,字节码解释器工做时经过改变这个计数值能够选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都须要依赖这个计数器完成。该内存区域是惟一一个java虚拟机规范没有规定任何OOM状况的区域。数据结构

2、如何判断一个对象是否存活?(或者GC对象的断定方法) 

判断一个对象是否存活有两种方法:
1. 引用计数法
所谓引用计数法就是给每个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
引用计数法有一个缺陷就是没法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就形成没法完成垃圾回收,因此主流的虚拟机都没有采用这种算法。多线程

2.可达性算法(引用链法)
该算法的思想是:从一个被称为GC Roots的对象开始向下搜索,若是一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
在java中能够做为GC Roots的对象有如下几种:jvm

  • 虚拟机栈中引用的对象
  • 方法区类静态属性引用的对象
  • 方法区常量池引用的对象
  • 本地方法栈JNI引用的对象

虽然这些算法能够断定一个对象是否能被回收,可是当知足上述条件时,一个对象比不必定会被回收。当一个对象不可达GC Root时,这个对象并 不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收须要经历两次标记。
若是对象在可达性分析中没有与GC Root的引用链,那么此时就会被第一次标记而且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已被虚拟机调用过,那么就认为是不必的。
若是该对象有必要执行finalize()方法,那么这个对象将会放在一个称为F-Queue的对队列中,虚拟机会触发一个Finalize()线程去执行,此线程是低优先级的,而且虚拟机不会承诺一直等待它运行完,这是由于若是finalize()执行缓慢或者发生了死锁,那么就会形成F-Queue队列一直等待,形成了内存回收系统的崩溃。GC对处于F-Queue中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。函数

3、简述java垃圾回收机制?

在系统运行过程当中,会产生一些无用的对象,这些对象占据着必定的内存,若是不对这些对象清理回收无用对象的内存,可能会致使内存的耗尽,因此垃圾回收机制回收的是内存。同时GC回收的是堆区和方法区的内存。学习

  在java中,程序员是不须要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常状况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

4、java中垃圾收集的方法有哪些?

一、标记-清除:
这是垃圾收集算法中最基础的,根据名字就能够知道,它的思想就是标记哪些要被回收的对象,而后统一回收。这种方法很简单,可是会有两个主要问题:1.效率不高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,致使之后程序在分配较大的对象时,因为没有充足的连续内存而提早触发一次GC动做。

二、复制算法:
为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,而后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,而后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。可是这种方式,内存的代价过高,每次基本上都要浪费通常的内存。
因而将该算法进行了改进,内存区域再也不是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那分内存交Eden区,其他是两块较小的内存区叫Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,而后清除Eden区,若是此时存活的对象太多,以致于Survivor不够时,会将这些对象经过分配担保机制复制到老年代中。(java堆又分为新生代和老年代)

三、标记-整理
该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不一样之处就是在清除对象的时候现将可回收对象移动到一端,而后清除掉端边界之外的对象,这样就不会产生内存碎片了。

四、分代收集 
如今的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,因为对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,因此可使用标记-整理 或者 标记-清除。

5、java内存模型

java内存模型(JMM)是线程间通讯的控制机制.JMM定义了主内存和线程之间抽象关系。线程之间的共享变量存储在主内存(main memory)中,每一个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。Java内存模型的抽象示意图以下:

从上图来看,线程A与线程B之间如要通讯的话,必需要经历下面2个步骤:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 而后,线程B到主内存中去读取线程A以前已更新过的共享变量。

具体参考博客:http://blog.csdn.net/suifeng3051/article/details/52611310

6、java类加载过程?

  类加载过程

  类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序以下图所示:

  

  其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是肯定的,而解析阶段则不必定,它在某些状况下能够在初始化阶段以后开始,这是为了支持 Java 语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,由于这些阶段一般都是互相交叉地混合进行的,一般在一个阶段执行的过程当中调用或激活另外一个阶段。

  简要说明下 Java 中的绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对 Java 来讲,绑定分为静态绑定和动态绑定:

  • 静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它链接程序实现。针对 Java,简单的能够理解为程序编译期的绑定。Java 当中的方法只有 final,static,private 和构造方法是前期绑定的。
  • 动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在 Java 中,几乎全部的方法都是后期绑定的。

  讲述类加载过程当中每一个阶段所作的工做: 

  加载:   

加载时类加载过程的第一个阶段,在加载阶段,虚拟机须要完成如下三件事情:

    • 经过一个类的全限定名来获取其定义的二进制字节流。
    • 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
    • 在 Java 堆中生成一个表明这个类的 java.lang.Class 对象,做为对方法区中这些数据的访问入口。

验证:
验证的目的是为了确保Class文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成如下四钟验证:

1. 文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
2. 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
3. 字节码验证:是整个验证过程当中最复杂的一个阶段,经过验证数据流和控制流的分析,肯定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
4. 符号引用验证:这个动做在后面的解析过程当中发生,主要是为了确保解析动做能正确执行。

  准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有如下几点须要注意:

    • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
    • 这里所设置的初始值一般状况下是数据类型默认的零值(如 0、0L、null、false 等),而不是被在 Java 代码中被显式地赋予的值。

假设一个类变量的定义为: 

public static int value = 3;

  那么变量 value 在准备阶段事后的初始值为 0,而不是 3,由于这时候还没有开始执行任何 Java 方法,而把 value 赋值为 3 的 putstatic 指令是在程序编译后,存放于类构造器 ()方法之中的,因此把 value 赋值为 3 的动做将在初始化阶段才会执行。

解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。在 Class 类文件结构一文中已经比较过了符号引用和直接引用的区别和关联,这里再也不赘述。前面说解析阶段可能开始于初始化以前,也可能在初始化以后开始,虚拟机会根据须要来判断,究竟是在类被加载器加载时就对常量池中的符号引用进行解析(初始化以前),仍是等到一个符号引用将要被使用前才去解析它(初始化以后)。

对同一个符号引用进行屡次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动做重复进行。

解析动做主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info 四种常量类型。

一、类或接口的解析:判断所要转化成的直接引用是对数组类型,仍是普通的对象类型的引用,从而进行不一样的解析。

二、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,若是有,则查找结束;若是没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,尚未,则按照继承关系从上往下递归搜索其父类,直至查找结束,查找流程以下图所示:

  

·

三、类方法解析:对类方法的解析与对字段解析的搜索步骤差很少,只是多了判断该方法所处的是类仍是接口的步骤,并且对类方法的匹配搜索,是先搜索父类,再搜索接口。

四、接口方法解析:与类方法解析步骤相似,知识接口不会有父类,所以,只递归向上搜索父接口就好了。  

  初始化

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的 Java 程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员经过程序指定的主观计划去初始化类变量和其余资源,或者能够从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。

这里简单说明下()方法的执行规则:

一、()方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在前面的静态语句中能够赋值,可是不能访问。

二、()方法与实例构造器()方法(类的构造函数)不一样,它不须要显式地调用父类构造器,虚拟机会保证在子类的()方法执行以前,父类的()方法已经执行完毕。所以,在虚拟机中第一个被执行的()方法的类确定是java.lang.Object。

三、()方法对于类或接口来讲并非必须的,若是一个类中没有静态语句块,也没有对类变量的赋值操做,那么编译器能够不为这个类生成()方法。

四、接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操做,所以接口与类同样会生成()方法。可是接口鱼类不一样的是:执行接口的()方法不须要先执行父接口的()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也同样不会执行接口的()方法。

五、虚拟机会保证一个类的()方法在多线程环境中被正确地加锁和同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其余线程都须要阻塞等待,直到活动线程执行()方法完毕。若是在一个类的()方法中有耗时很长的操做,那就可能形成多个线程阻塞,在实际应用中这种阻塞每每是很隐蔽的。

  总结

整个类加载过程当中,除了在加载阶段用户应用程序能够自定义类加载器参与以外,其他全部的动做彻底由虚拟机主导和控制。到了初始化才开始执行类中定义的 Java 程序代码(亦及字节码),但这里的执行代码只是个开端,它仅限于()方法。类加载过程当中主要是将 Class 文件(准确地讲,应该是类的二进制字节流)加载到虚拟机内存中,真正执行字节码的操做,在加载完成后才真正开始。

7、什么是类加载器,类加载器有哪些?

实现经过类的权限定名获取该类的二进制字节流的代码块叫作类加载器。
主要有一下四种类加载器:
1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,没法被java程序直接引用。
2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。通常来讲,Java 应用的类都是由它来完成加载的。能够经过 ClassLoader.getSystemClassLoader()来获取它。
4. 用户自定义类加载器,经过继承 java.lang.ClassLoader类的方式实现。

8、简述java内存分配与回收策率以及Minor GC和Major GC

    1. 对象优先在堆的Eden区分配。
    2. 大对象直接进入老年代.
    3. 长期存活的对象将直接进入老年代.当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC.Minor Gc一般发生在新生代的Eden区,在这个区的对象生存期短,每每发生Gc的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,通常状况下,触发老年代GC的时候不会触发Minor GC,可是经过配置,能够在Full GC以前进行一次Minor GC这样能够加快老年代的回收速度。

 

关于Java虚拟机学习,推荐《深刻理解Java虚拟机》 第二版 周志明著

  以及博客文章http://wiki.jikexueyuan.com/project/java-vm/