Java虚拟机面试相关

引言:相信很多java的开发者都会对我们缩写的java文件究竟是怎么样被加载运行的过程,以及Java的内存回收机制怎么实现?很好奇,今天自己无意中看到几篇相关文章,下面谈谈自己对这个过程的理解:
本片文件主要从一下几个方面讨论Java的虚拟机
1:.java文件如果加载到JVM 中
2:JVM的内存模型
3:JVM的垃圾回收机制
4:常用垃圾回收器

1 :类加载的过程

类加载器的主要工作就是把类文件(.class)加载到JVM中。如下图所示,其过程分为三步:

  • 1:加载:定位要加载的类文件,并将其字节流装载到JVM中;
  • 2:链接:给要加载的类分配最基本的内存结构保存其信息,比如属性,方法以及引用的类。在该阶段,该类还处于不可用状态;具体的链接过程又由一下几个步骤组成:
    • A:验证:对加载的字节流进行验证,比如格式上的,安全方面的;
    • B:内存分配:为该类准备内存空间来表示其属性,方法以及引用的类;
    • C:解析:加载该类所引用的其它类,比如父类,实现的接口等。
  • 3:初始化:对类变量进行赋值。
    补充知识关于类加载器:
    java虚拟机中共有3种类加载器:他们分别是
  • 1:BoostrapClassLoader:启动类加载器(它是所有类加载器的父亲)
    当虚拟机运行时,这个类加载器便会被创建,它负责加载java的核心类库,如:java.lang;*Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。
  • 2:ExtentionClassLoader标准扩展类加载器
    Bootstrploader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader;ExtClassLoader是用Java写的,具体来说就sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要载%JAVA_HOME%/jre/lib/ext此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。
  • 3:ApplicationClassLoader应用程序类加载器;
    Bootstrp loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为 ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是 sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。
    注意:除了BootstrapClassLoader之外其他的类加载器都是java.lang.ClassLoader类的子类;
    类加载器的操作原则
    1.代理原则
    一个类加载器在加载一个类时会先请求它的父类加载器去加载,父类加载器在去请求它的父类加载器去加载;
    2.可见性原则
    每个类对类加载器的可见性是不一样的
    3.唯一性原则
    每一个类在一个加载器里最多加载一次。
    类的隐式和显式加载
    隐式加载:当一个类被引用,被继承或者被实例化时会被隐式加载,如果加载失败,是抛出NoClassDefFoundError。
    显式加载:使用如下方法,如果加载失败会抛出ClassNotFoundException。
    cl.loadClass(),cl是类加载器的一个实例;
    Class.forName(),使用当前类的类加载器进行加载。

2:Java虚拟机内存模型

jvm.png

我们所讲的Java Virtual Machine即JVM = 类加载器+运行时数据区+执行引擎
首先我们的java将写好的java文件经过java编译器编译之后成为class文件,由相应的类加载器进行加载到我们JVM中运行时数据区,在类加载器家在期间它会检查该类的合法性,并交由执行引擎进行执行;

  • 1:PC寄存器(The pc Register)

    • (1)每一个Java线程都有一个PC寄存器,用以记录当前执行到哪个指令。
    • (2)PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法是Java方法,则记录的是正在执行的虚拟机字节码地址,如该方法为native的,则计数器值为空。
    • (3)此内存区域是唯一一个在JVM中没有规定任何OutOfMemoryError情况的区域。
  • 2:JVM栈(Java Virtual MachineStacks)

    JVM 栈

  • (1)JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,与程PC寄存器一样,JVM栈的生命周期也是与线程相同。

  • (2)本质上来讲,它就是个栈。里面存放的元素叫栈帧,栈帧好像很复杂的样子,其实它很简单!它里面具体存放的是执行的函数的一些数据,无非就是局部变量表(保存函数内部的变量)、操作数栈(执行引擎计算时需要),方法出口等等。
    执行引擎每调用一个函数时,就为这个函数创建一个栈帧,并加入虚拟机栈。换个角度理解,每个函数从调用到执行结束,其实是对应一个栈帧的入栈和出栈。

  • (3)这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(如:将一个函数反复递归自己,最终会出现这种异常)。如果JVM栈可以动态扩展(大部分JVM是可以的),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

  • (4)由于JVM栈是线程私有的,因此其在内存分配上非常高效,并且当线程运行完毕后,这些内存也就被自动回收。

  • 3 :本地方法栈(Native Method Stacks)

    • (1)本地方法栈与虚拟机栈所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。
    • (2)和JVM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。
  • 4 :方法区(Method Area)

    • (1)在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量等。如,当程序中通过getName、isInterface等方法来获取信息时,这些数据都来源于方法区域。
    • (2)方法区域是全局共享的,比如每个线程度可以访问同一个类的静态变量对象。
    • (3)由于使用反射机制的原因,虚拟机很难推测哪个类信息不再使用,因此这块区域的回收很难!另外,对这块区域主要是针对常量池回收,值得注意的是JDK1.7已经把常量池转移到堆里面了。
    • (4)同样,当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。下面演示一下造成方法区内的OOM场景。
      执行之前,可以把虚拟机的参数-XXpermSize和-XX:MaxPermSize限制方法区大小。
      PS:永久代只有HotSpot虚拟机才有这个概念:并且在JDK1.8已经将永久代取消通过使用元空间替换;
  • 运行时常量池(Runtime Constant Pool)

    • (1)存放类中固定的常量信息、方法和Field的引用信息等,其空间从方法区域(JDK1.7后为堆空间)中分配。
    • (2)Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。
    • (3)当常量池无法在申请到内存时会抛出OutOfMemoryError异常,上面也分析过了。
  • 5 Java堆

    JVM堆.png

    • (1)Java堆(java heap)是JVM所管理的最大的一块内存。它是被所有线程共享的一块内存区域,在虚拟机启动时创建。
    • (2)几乎所有的实例对象都是在这块区域中存放。(JIT编译器貌似不是这样的)。
    • (3)Java堆是垃圾收集管理的主要战场。所有Java堆可以细分为:新生代和老年代。再细致分就是把新生代分为:Eden空间、FromSurvivor空间、To Survivor空间。
    • (4)根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
      PS: 堆和栈的区别
      这是一个非常常见的面试题,主要从以下几个方面来回答。
      • (1)各司其职
        最主要的区别就是栈内存用来存储局部变量和方法调用。而堆内存用来存储Java中的对象。无论是成员变量、局部变量还是类变量,它们指向的对象都存储在堆内存中。
      • (2)空间大小
        栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满并产生StackOverFlowError。
      • (3)独有还是共享
        栈内存归属于线程的私有内存,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见。而堆内存中的对象对所有线程可见,可以被所有线程访问。
      • (4)异常错误
        如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
        如果JVM栈可以动态扩展(大部分JVM是可以的),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
        而堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError

3:JVM的垃圾回收机制

Java 的垃圾回收机制常常用来和C以及C++做对比,C和C++的对象回收需要程序员自己去管理,对象的生命周期完全掌握在机开发者本身,而Java的的对象回收交由JVM的做,让程序员从繁琐的垃圾回收工作中解脱出来。这样做的带来的好处不言而喻,但同时也会缺点,比如:当我们的内存溢出时,定位原因是如果不是对JVM特别熟悉,那将比较麻烦,相反,C++或者C语言出现内存溢出的情况时比较好定位,因为内存的申请和释放都是由程序员负责的。下面我们从一下几个方面谈谈Java的垃圾回收

A:什么时候会触发垃圾回收

JVM的垃圾回收采用的分区分带机制,主要的战场在JVM的堆区域。我们刚刚也讲了,JVM的堆按照存储对象的生命周期不同分为了 年轻带和年老带;其中年轻带为了垃圾回收算法的运行又分为Edeg和survive区域(survive区域又分为from 和to 区域);触发Monir GC的条件是,Edeg区域存储空间满了时;触发Full GC的条件是升到年老带的对象大于老年带剩余存储空间时就会触发full GC或者小于时被HandlePromotionFailure参数强制full gc;gc与非gc时间耗时超过了GCTimeRatio的限制引发OOM,调优诸如通过NewRatio控制新生代老年代比例,通过MaxTenuringThreshold控制进入老年前生存次数等。

  • **补充OOM OutOfMemoryError **
    JVM的内存空间不足时并且垃圾回收器么没有可以回收的对象时就会抛出这个异常;
    JVM中除了程序计数器之外的每一个区域都有可能会跑出这个异常。
    造成的原因两种:
    • JVM的内存分配的过少
    • 应用程序占用的内存过多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
B:对什么东西进行回收?

从root对象开始所有,对不可达的对象进行标记,经过第一次清除之后仍然没有复活的对象。

C:怎么样进行回收?

JVM为了方便对内存进行管理回收,采用了分带回收机制,对不同区域的对象,根据其特带你采用了不同垃圾回收算法。

  • 引用计数法

    • A:概念:
      引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
    • B:引用计数法的问题
      • 引用和去引用伴随加法和减法,影响性能
      • 很难处理循环引用
  • 根搜索算法

    • A:概念:
      跟搜索算法,其实就是每一个现在存储的对象都会有一个root对象能够指向它,因此,我们在判断一个对象是否是可回收对象时,我们可以从这个root 对象开始搜索,那些到达不了的对象就是可以进行回收的对象。根搜索算法中不可达的对象并非“非死不可”,这时候它们暂时处于“缓刑”阶段,真正宣告一个对象死亡,至少要经历两次标记过程;第一次标记后,会将这些标记的对象存放到F-Queue的队列之,之后JVM会对该队列中的所有对象进行一个finalize方法的调用,来给这些对象一次复活的机会;如果通过调用finalize方法之后发现该对象由重新和root 引用链上的对象连接上了,就表示该对象重新复活了。F-Queue中的所有对象调用玩finalize方法之后,JVM 会重新对该队列中的对象再次进行一个根搜索并进行标记;finalize()方法只会被调用一次,首先该对象要重写finalize方法,其次该方法对象的该方法之前没有被调用过。
    • B:根搜索算法法的特点
      • 可以处理循环引用
      • 需要维护一个root对象;
  • 1:标记-清除法

    • A:概念:标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。


      Paste_Image.png

      Paste_Image.png
  • B:特点:

    • 简单可行,
    • 操作复杂,耗时,需要一个一个进行标记,一个一个进行回收;
    • 可能会造成内存碎片;每一个一块连续的内存供其它对象使用;但其实总的剩余内存空间是充足的;因此可能会再次触发GC;
  • 2:标记压缩法
    标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。

    Paste_Image.png

    3:标记复制法

    • A:概念:与标记-清除算法相比,复制算法是一种相对高效的回收方法
      将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收;
    • B:特点:
      • 优点高效,不会造成内存碎片。
      • 不适用于存活对象较多的场合 如老年代;
      • 复制算法的最大问题是:空间浪费 整合标记清理思想


        Paste_Image.png

        Paste_Image.png
D :方法评价综述,以及补充概念:
  • 1:A:引用计数

    • 没有被Java采用
    • 标记-清除
    • 标记-压缩
    • 复制算法 :新生代
  • B:依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。根据不同代的特点,选取合适的收集算法

    • 少量对象存活,适合复制算法
    • 大量对象存活,适合标记清理或者标记压缩
  • 2:可触及的
    概念:从根节点可以触及到这个对象

  • 3:可复活的
    一旦所有引用被释放,就是可复活状态
    因为在finalize()中可能复活该对象

  • 4:不可触及的
    在finalize()后,可能会进入不可触及状态
    不可触及的对象不可能复活:可以回收

  • 5:经验:
    避免使用finalize(),操作不慎可能导致错误。
    优先级低,何时被调用, 不确定
    何时发生GC不确定
    可以使用try-catch-finally来替代它

  • 6:可作为根(GC roots)的对象

    • 栈中引用的对象
    • 方法区中静态成员或者常量引用的对象(全局对象)
    • JNI方法栈中(即一般说的Native方法)引用对象
  • 7:全局停顿:

    • A:概念:Java中一种全局暂停的现象
      全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
      多半由于GC引起,排查方法
      • Dump线程
      • 死锁检查
      • 堆Dump
    • B:GC时为什么会有全局停顿?
      类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。
    • C:危害
      长时间服务停止,没有响应
      遇到HA系统,可能引起主备切换,严重危害生产环境。

4:常用的垃圾回收器及其特点

Java的垃圾收集器根据不同的区域是否可以开启多个线程是否可以和用户线程并发进行如下划分;两个垃圾回收器之间的连线表示这两个垃圾收集器可以组合使用。


image.png

年轻带:serial ,ParNew , Parallel Scavenge
老年代:CMS,serial Old(MSC) Paralle Old

1:serial 垃圾搜集器

jdk1.3之前垃圾回收器的唯一选择,采用的单线非并行的机制,主要作用于 年轻带。
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。

2:ParNew 垃圾收集器

是serial垃圾收集器的多线程版本,可以同时运行多个垃圾回收线程。也作用于年轻带
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。

3:Parallel Scavenge 垃圾收集器

可以和用户线程并发执行并且可以开启多线程的垃圾回收的垃圾收集器;
采用的是复制算法;
主要适合在后台运算而不需要太多交互的任务。

4:Serial Old 垃圾收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用

5:Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

6:CMS垃圾收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器是基于“标记—清除”算法实现的

7:G1垃圾收集器

G1(Garbage-First)是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点。

  • 并行与并发
    G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

  • 分代收集
    与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

  • 空间整合
    与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

  • 可预测的停顿
    这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
    参考文章:
    [http://blog.csdn.net/seu_calvin/article/details/51404589]
    [http://blog.csdn.net/radic_feng/article/details/6897898]
    [https://www.jianshu.com/p/50d5c88b272d]

你可能感兴趣的:(Java虚拟机面试相关)