0%

Principle of JVM

JVM原理速记复习Java虚拟机总结思维导图面试必备

JVM

Java虚拟机


一、运行时数据区域


线程私有



  • 程序计数器



    • 记录正在执行的虚拟机字节码指令的地址(如果正在执行的是Native方法则为空),是唯一一个没有规定OOM(OutOfMemoryError)的区域。


  • Java虚拟机栈



    • 每个Java方法在执行的同时会创建一个栈桢用于存储局部变量表、操作数栈、动态链接、方法出口等信息。从方法调用直到执行完成的过程,对应着一个栈桢在Java虚拟机栈中入栈和出栈的过程。(局部变量包含基本数据类型、对象引用reference和returnAddress类型)


  • 本地方法栈



    • 本地方法栈与Java虚拟机栈类似,它们之间的区别只不过是本地方法栈为Native方法服务。



线程公有



  • Java堆(GC区)(Java Head)



    • 几乎所有的对象实例都在这里分配内存,是垃圾收集器管理的主要区域。分为新生代和老年代。对于新生代又分为Eden空间、From Survivor空间、To Survivor空间。


  • JDK1.7 方法区(永久代)



    • 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

      对这块区域进行垃圾回收的主要目的是对常量池的回收和对类的卸载,但是一般难以实现。

      HotSpot虚拟机把它当做永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素的影响,并且每次Full GC之后永久代的大小都会改变,所以经常抛出OOM异常。

      从JDK1.8开始,移除永久代,并把方法区移至元空间。

    • 运行时常量池



      • 是方法区的一部分

        Class文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。

        允许动态生成,例如String类的intern()



  • JDK1.8 元空间



    • 原本存在方法区(永久代)的数据,一部分移到了Java堆里面,一部分移到了本地内存里面(即元空间)。元空间存储类的元信息,静态变量和常量池等放入堆中。


  • 直接内存



    • 在NIO中,会使用Native函数库直接分配堆外内存。



二、HotSpot虚拟机


对象的创建



  • 当虚拟机遇到一条new指令时



  1. 检查参数能否在常量池中找到符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始过,没有的话先执行相应的类加载过程。

  2. 在类加载检查通过之后,接下来虚拟机将为新生对象分配内存。

  3. 内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。

  4. 对对象头进行必要的设置。

  5. 执行构造方法按照程序员的意愿进行初始化。


对象的内存布局




    1. 对象头




      1. 第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向实现戳等。



      1. 第二部分是类型指针,即对象指向它的类元数据的指针(如果使用直接对象指针访问),虚拟机通过这个指针来确定这个对象是哪个类的实例。



      1. 如果对象是一个Java数组的话,还需要第三部分记录数据长度的数据。




    1. 实例数据



    • 是对象真正存储的有效信息,也就是在代码中定义的各种类型的字段内容。



    1. 对齐填充



    • 不是必然存在的,仅仅起着占位符的作用。

      HotSpot需要对象的大小必须是8字节的整数倍。



对象的访问定位



  • 句柄访问



    • 在Java堆中划分出一块内存作为句柄池。

      Java栈上的对象引用reference中存储的就是对象的句柄地址,而句柄中包含了到对象实例数据的指针和到对象类型数据的指针。

      对象实例数据在Java堆中,对象类型数据在方法区(永久代)中。

      优点:在对象被移动时只会改变句柄中的实例数据指针,而对象引用本身不需要修改。


  • 直接指针访问(HotSpot使用)



    • Java栈上的对象引用reference中存储的就是对象的直接地址。

      在堆中的对象实例数据就需要包含到对象类型数据的指针。

      优点:节省了一次指针定位的时间开销,速度更快。



三、垃圾收集


概述



  • 垃圾收集主要是针对Java堆和方法区。

    程序计数器、Java虚拟机栈个本地方法栈三个区域属于线程私有,线程或方法结束之后就会消失,因此不需要对这三个区域进行垃圾回收。


判断对象是否可以被回收



  • 第一次标记(缓刑)



    • 引用计数算法



      • 给对象添加一个引用计数器,当对象增加一个引用时引用计数值++,引用失效时引用计数值–,引用计数值为0时对象可以被回收。




但是它难以解决对象之间的相互循环引用的情况,此时这个两个对象引用计数值为1,但是永远无法用到这两个对象。

  • 可达性分析算法(Java使用)

    • 以一系列GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连是,则证明此对象不可用,可以被回收。

      GC Roots对象包括



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

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

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

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



      • 第二次标记



        • 当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过。

          如果对象在finalize方法中重新与引用链上的任何一个对象建立关联则将不会被回收。

        • finalize()



          • 任何一个对象的finalize()方法都只会被系统调用一次。

            它的出现是一个妥协,运行代价高昂,不确定性大,无法保证各个对象的调用顺序。

            finalize()能做的所有工作使用try-finally或者其他方式都可以做的更好,完全可以忘记在这个函数的存在。




      方法区的回收



      • 在方法区进行垃圾回收的性价比一般比较低。

        主要回收两部分,废弃常量和无用的类。


      满足无用的类三个判断条件才仅仅代表可以进行回收,不是必然关系,可以使用-Xnoclassgc参数控制。



      1. 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例。

      2. 加载该类的ClassLoader已经被回收。

      3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问到该类的方法。


      引用类型




        1. 强引用



        • 使用new一个新对象的方式来创建强引用。

          只要强引用还存在,被引用的对象则永远不会被回收。



        1. 软引用



        • 使用SoftReference类来实现软引用。

          用来描述一些还有用但是并非必须的对象,被引用的对象在将要发生内存溢出异常之前会被回收。



        1. 弱引用



        • 使用WeakReference类来实现弱引用。

          强度比软引用更弱一些,被引用的对象在下一次垃圾收集时会被回收。



        1. 虚引用



        • 使用PhantomReference类来实现虚引用。

          最弱的引用关系,不会对被引用的对象生存时间构成影响,也无法通过虚引用来取得一个对象实例。

          唯一目的就是能在这个对象被收集器回收时收到一个系统通知。



      垃圾收集算法




        1. 标记 - 清除



        • 首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象并取消标记。



      不足:



      1. 效率问题,标记和清除两个过程的效率都不高。

      2. 空间问题,标记清除之后会产生大量不连续的内存碎片,没有连续内存容纳较大对象而不得不提前触发另一次垃圾收集。




        1. 标记 - 整理



        • 和标记 - 清除算法一样,但标记之后让所有存活对象都向一段移动,然后直接清理掉端边界以外的内存。

          解决了标记 - 清除算法的空间问题,但需要移动大量对象,还是存在效率问题。



        1. 复制



        • 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用多的内存空间一次清理掉。

          代价是将内存缩小为原来的一般,太高了。



      现在商业虚拟机都采用这种算法用于新生代。

      因为新生代中的对象98%都是朝生暮死,所以将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间。

      当回收时,如果另外一块Survivor空间没有足够的空间存放存活下来的对象时,这些对象将直接通过分配担保机制进入老年代。




        1. 分代收集



        • 一般把Java堆分为新生代和老年代。

          在新生代中使用复制算法,在老年代中使用标记 -清除 或者 标记 - 整理 算法来进行回收。



      HotSpot的算法实现



      • 枚举根节点(GC Roots)



        • 目前主流Java虚拟机使用的都是准确式GC。

          GC停顿的时候,虚拟机可以通过OopMap数据结构(映射表)知道,在对象内的什么偏移量上是什么类型的数据,而且特定的位置记录着栈和寄存器中哪些位置是引用。因此可以快速且准确的完成GC Roots枚举。


      • 安全点



        • 为了节省GC的空间成本,并不会为每条指令都生成OopMap,只是在“特定的位置”记录OopMap,这些位置称为安全点。



      程序执行只有到达安全点时才能暂停,到达安全点有两种方案。



      1. 抢断式中断(几乎不使用)。GC时,先把所有线程中断,如果有线程不在安全点,就恢复该线程,让他跑到安全点。

      2. 主动式中断(主要使用)。GC时,设置一个标志,各个线程执行到安全点时轮询这个标志,发现标志为直则挂起线程。


      但是当线程sleep或blocked时无法响应JVM的中断请求走到安全点中断挂起,所以引出安全区域。



      • 安全区域



        • 安全区域是指在一段代码片段之中,引用关系不会发生变化,是扩展的安全点。



      线程进入安全区域时表示自己进入了安全区域,这个发生GC时,JVM就不需要管这个线程。

      线程离开安全区域时,检查系统是否完成GC过程,没有就等待可以离开安全区域的信号为止,否者继续执行。


      垃圾收集器



      • 新生代




          1. serial收集器



          • 它是单线程收集器,只会使用一个线程进行垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程。




      优点:对比其他单线程收集器简单高效,对于单个CPU环境来说,没有线程交互的开销,因此拥有最高的单线程收集效率。


      它是Client场景下默认新生代收集器,因为在该场景下内存一般来说不会很大。

      1. parnew收集器
    • 它是Serial收集器的多线程版本,公用了相当多的代码。

      在单CPU环境中绝对不会有比Serial收集器更好的效果,甚至在2个CPU环境中也不能百分之百超越。


      它是Server场景下默认的新生代收集器,主要因为除了Serial收集器,只用它能与CMS收集器配合使用。

      1. parallel scavenge收集器
    • “吞吐优先”收集器,与ParNew收集器差不多。

      但是其他收集器的目标是尽可能缩短垃圾收集时用户线程停顿的时间,而它的目标是达到一个可控制的吞吐量。这里的吞吐量指CPU用于运行用户程序的时间占总时间的比值。



      • 老年代




          1. serial old收集器



          • 是Serial收集器老年代版本。




      也是给Client场景下的虚拟机使用的。

      1. parallel old收集器
    • 是Parallel Scavenge收集器的老年代版本。

      在注重吞吐量已经CPU资源敏感的场合,都可以优先考虑Parallel Scavenge和Parallel Old收集器。

      1. cms收集器
    • Concurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的收集器。

    • 运作过程

        1. 初始标记(最短)。仍需要暂停用户线程。只是标记一下GC Roots能直接关联到的对象,速度很快

          1. 并发标记(耗时最长)。进行GC Roots Tracing(根搜索算法)的过程。

          2. 重新标记。修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。比初始标记长但远小于并发标记时间。

          3. 并发清除

          1 和4 两个步骤并没有带上并发两个字,即这两个步骤仍要暂停用户线程。

    • 优缺点

      • 并发收集、低停顿。

        1. CMS收集器对CPU资源非常敏感。虽然不会导致用户线程停顿,但是占用CPU资源会使应用程序变慢。

        2. 无法处理浮动垃圾。在并发清除阶段新垃圾还会不断的产生,所以GC时要控制“-XX:CMSinitiatingOccupancyFraction参数”预留足够的内存空间给这些垃圾,当预留内存无法满足程序需要时就会出现”Concurrent Mode Failure“失败,临时启动Serial Old收集。

        3. 由于使用标记 - 清除算法,收集之后会产生大量空间碎片。




          1. g1收集器



          • Garbage First是一款面向服务端应用的垃圾收集器

          • 运作过程




              1. 初始标记






        1. 并发标记

        2. 最终标记

        3. 删选标记


        五、类加载机制


        概述



        • 虚拟机把描述类的数据从Class问价加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

          Java应用程序的高度灵活性就是依赖运行期动态加载和动态连接实现的。


        类的生命周期



        • 加载 -> 连接(验证 -> 准备 -> 解析) -> 初始化 -> 使用 - >卸载


        类初始化时机



        • 主动引用



          • 虚拟机规范中没有强制约束何时进行加载,但是规定了有且只有五种情况必须对类进行初始化(加载、验证、准备都会随之发生)




        1. 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时没有初始化。

        2. 反射调用时没有初始化。

        3. 发现其父类没有初始化则先触发其父类的初始化。

        4. 包含psvm(mian()方法)的那个类。

        5. 动态语言支持时,REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄。



        • 被动引用



          • 除上面五种情况之外,所有引用类的方式都不会触发初始化,称为被动引用。




        1. 通过子类引用父类的静态字段,不会导致子类的初始化。

        2. 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承Object的子类,其中包含数组的属性和方法,用户只能使用public的length和clone()。

        3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。


        类加载过程




          1. 加载




            1. 通过类的全限定名来获取定义此类的二进制字节流。





        1. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

        2. 在内存中生成一个代表这个类的java.lang.Class对象(HotSpot将其存放在方法区中),作为方法区这个类的各种数据的访问入口。




          1. 验证



          • 为了确保Class文件的字节类中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。可以通过-Xverify:none关闭大部分类验证。




        1. 文件格式验证。确保输入字节流能正确的解析并存储于方法区,后面的3个验证全部基于方法区的存储结构进行,不会再操作字节流。

        2. 元数据验证。对字节码描述信息进行语义分析,确保其符合Java语法规范。(Java语法验证)

        3. 字节码验证。最复杂,通过数据流和控制流分析,确定程序语义时合法的、符合逻辑的。可以通过参数关闭。(验证指令跳转范围,类型转换有效等)

        4. 符号引用验证。将符号引用转化为直接引用,发生在第三个阶段——解析阶段中发生。




          1. 准备



          • 类变量是被static修饰的变量,准备阶段为类变量分配内存并设置零值(final直接设置初始值),使用的是方法区的内存。



          1. 解析



          • 将常量池内的符号引用替换为直接引用的过程。

            其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的动态绑定。

            解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、和调用点限定符。



          1. 初始化



          • 初始化阶段才真正执行类中定义的Java程序代码,是执行类构造器()方法的过程。

            在准备阶段,类变量已经给过零值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其他资源。



            • ()



              • 类构造器方法。是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的的语句合并产生的。






        1. 不需要显式调用父类构造器,JVM会保证在子类clinit执行之前,父类的clinit已经执行完成。

        2. 接口中不能使用静态语句块但仍可以有类变量的赋值操作。当没有使用父接口中定义的变量时子接口的c

  • 接口中不能使用静态语句块但仍可以有类变量的赋值操作。当没有使用父接口中定义的变量时子接口的clinit不需要先执行父接口的clinit方法。接口的实现类也不会执行接口的clinit方法。
  • 虚拟机会保证clinit在多线程环境中被正确的加锁、同步。其他线性唤醒之后不会再进入clinit方法,同一个类加载器下,一个类型只会初始化一次。

    • <init>()

      • 对象构造器方法。Java对象被创建时才会进行实例化操作,对非静态变量解析初始化。

      • 会显式的调用父类的init方法,对象实例化过程中对实例域的初始化操作全部在init方法中进行。



      • 类(加载) 器


        • 类与类加载器



          • 类加载器实现类的加载动作。

            类加载器和这个类本身一同确立这个类的唯一性,每个类加载器都有独立的类命名空间。在同一个类加载器加载的情况下才会有两个类相等。

            相等包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()、instanceof关键字。


        • 类加载器分类



          • 启动类加载器



            • 由C++语言实现,是虚拟机的一部分。负责将JAVA_HOME/lib目录中,或者被-Xbootclasspath参数指定的路径,但是文件名要能被虚拟机识别,名字不符合无法被启动类加载器加载。启动类加载器无法被Java程序直接引用。


          • 扩展类加载器



            • 由Java语言实现,负责加载JAVA_HOME/lib/ext目录,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。


          • 应用程序类加载器



            • 由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称他为系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库,一般情况下这个就是程序中默认的类加载器。


          • 自定义类加载器



            • 由用户自己实现。





        1. 如果不想打破双亲委派模型,那么只需要重写findClass方法即可。

        2. 否则就重写整个loadClass方法。



        • 双亲委派模型



          • 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。父子不会以继承的关系类实现,而是都是使用组合关系来服用父加载器的代码。

            在java.lang.ClassLoader的loadClass()方法中实现。

          • 工作过程



            • 一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成(它的搜索范围中没有找到所需要的类)时才尝试自己加载


          • 好处



            • Java类随着它的类加载器一起具备了一种带有优先级的层次关系,从而使得基础类库得到同意。




        四、内存分配与回收策略


        Minor GC 和 Full GC



        • Minor GC



          • 发生在新生代的垃圾收集动作,因为新生代对象存活时间很短,因此Minor GC会频繁执行,执行速度快。

          • 时机



            • Eden不足



        • Full GC



          • 发生在老年区的GC,出现Full GC时往往伴随着Minor GC,比Minor GC慢10倍以上。

          • 时机




              1. 调用System.gc()



              • 只是建议虚拟机执行Full GC,但是虚拟机不一定真正去执行。

                不建议使用这种方式,而是让虚拟机管理内存。



              1. 老年代空间不足



              • 常见场景就是大对象和长期存活对象进入老年代。

                尽量避免创建过大的对象以及数组,调大新生代大小,让对象尽量咋新生代中被回收,不进入老年代。



              1. JDK1.7 之前方法区空间不足



              • 当系统中要加载的类、反射的类和常量较多时,永久代可能会被占满,在未配置CMS GC的情况下也会执行Full GC,如果空间仍然不够则会抛出OOM异常。

                可采用增大方法区空间或转为使用CMS GC。



              1. 空间分配担保失败



              • 发生Minor GC时分配担保的两个判断失败



              1. Concurrent Mode Failure



              • CMS GC 并发清理阶段用户线程还在执行,不断有新的浮动垃圾产生,当预留空间不足时报Concurrent Mode Failure错误并触发Full GC。





        内存分配策略




          1. 对象优先在Eden分配



          • 大多数情况下,对象在新生代Eden上分配,当Eden空间不够时,发起Minor GC,当另外一个Survivor空间不足时则将存活对象通过分配担保机制提前转移到老年代。



          1. 大对象直接进入老年代



          • 配置参数-XX:PretenureSizeThreshold,大于此值得对象直接在老年代分配,避免在Eden和Survivor之间的大量内存复制。



          1. 长期存活对象进入老年代



          • 虚拟机为每个对象定义了一个Age计数器,对象在Eden出生并经过Minor GC存活转移到另一个Survivor空间中时Age++,增加到默认16则转移到老年代。



          1. 动态对象年龄绑定



          • 虚拟机并不是永远要求对象的年龄必须到达MaxTenuringThreshold才能晋升老年代,如果在Survivor中相同年龄所有对象大小总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象直接进入老年代。



          1. 空间分配担保



          • 在发生Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代的所有对象,如果条件成立,那么Minor GC可以认为是安全的。

            可以通过HandlePromotionFailure参数设置允许冒险,此时虚拟机将与历代晋升到老年区对象的平均大小比较,仍小于则要进行一次Full GC。

            在JDK1.6.24之后HandlePromotionFailure已无作用,即虚拟机默认为true。