JVM知识点梳理

 

1、JVM总体概述

  • JVM总体上是由类装载子系统(ClassLoader)、运行时数据区、执行引擎(Execution Engine)、垃圾收集(GC)这四个部分组成。
  • 其中我们最为关注的运行时数据区,也就是JVM的内存部分则是由方法区(Method Area也成非堆)、JAVA堆(Java Heap)、虚拟机栈(JVM Stack)、程序计数器、本地方法栈(Native Method Stack)这几部分组成。

2、JVM大致执行流程

3、Java代码编译

  • 也正如前面所说,Java代码的编译和执行的整个过程大概是:开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。
  • (1)Java代码编译是由Java源码编译器来完成,也就是Java代码到JVM字节码(.class文件)的过程。 流程图如下所示:
  • Javaèææºåºå±åçåæµç¨ï¼çæä½ å°±ææ¡60%JVM
  • Java源码编译机制
  • java 源码编译由以下三个过程组成:
    • ①分析和输入到符号表
    • ②注解处理
    • ③语义分析和生成class文件
  • 最后生成的class文件由以下部分组成:
    • ①结构信息:包括class文件格式版本号及各部分的数量与大小的信息
    • ②元数据:对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池
    • ③方法信息:对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息

4、类加载机制

  • Class Loader类加载器负责加载.class文件,class文件在文件开头有特定的文件标示,并且ClassLoader负责class文件的加载等,至于它是否可以运行,则由Execution Engine决定
    • Javaèææºåºå±åçåæµç¨ï¼çæä½ å°±ææ¡60%JVM

5、jvm运行时数据区(在这里把class大卸八块分别放到不同的区域内)

  • JVM内存结构分为:方法区(method Area)、java堆(java heap)、虚拟机栈(jvm stack)、本地方法栈(Native Method Stack)(java中的jni调用)、 程序计数器(Program Counter Register)
  • 栈管运行,堆管存储。JVM调优主要是优化Java堆和方法区(no-heap)
  • 方法区(method)(线程共享)
    •  方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”
    • 它保存方法代码(编译后的java代码)和符号表。它用于存储已被JVM加载的类信息、静态(static)变量、final类型的常量、属性和方法信息,运行时常量池等数据。JVM用持久代(Permanet Generation)来存放方法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。
    • 运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。相较于Class文件常量池,运行时常量池更具动态性,在运行期间也可以将新的变量放入常量池中,而不是一定要在编译时确定的常量才能放入。最主要的运用便是String类的intern()方法。
  •  Java堆(Java Heap)(线程共享)
    • Java堆是各线程共享的内存区域,在JVM启动时创建,这块区域是JVM中最大的, 用于存储应用的对象和数组(所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制)
    • 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序
    • 对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确的释放本内存空间
    • 但由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。这是由new分配的内存,一般速度比较慢,而且容易产生内存碎片的原因
    • 一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的,堆内存分为三部分:新生代、老年代、永久代。
    • 堆的大小受限于计算机系统中有效的虚拟内存
    • Jdk1.8及之后:无永久代,改用元空间代替(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。
  • 虚拟机栈(jvm stack)(线程私有)
    • Java栈是线程私有的,是在线程创建时创建(也就是在执行方法是会为该方法创建一个栈帧),它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致。
    • 栈是什么(什么是栈帧
      • 栈存储的主要是栈帧,栈帧是一种数据结构,用于虚拟机进行方法的调用和执行。是一片逻辑连续的内存空间
      • 先执行那个方法就会先给这个方法创建一个栈帧,然后进行压栈,执行完之后再进行出栈
        • 所以每一个方法被调用直至执行完成的过程,就对应着一个栈帧在栈中从入栈到出栈的过程。
      • 每个方法执行的时候都会创建一个栈帧,栈帧中主要存储4类数据
        • 局部变量表:输入参数和输出参数以及方法内的变量还有存放八大基本类型的值和引用类型的地址(地址指向堆中的实际位置)
        • 操作数栈:记录出栈和入栈的操作;
        • 动态链接:就相当于在一个方法中注入了另一个方法的引用,当用到另一个方法时需要根据给他起的别名来转换为实际内存地址
        • 方法出口等信息:Java虚拟机根据不同数据类型有不同的底层return指令。当被调用方法执行某条return指令时,会选择相应的return指令来让值返回(如果该方法有返回值的话)
    • 栈的存储
      • 基础数据类型直接在栈空间分配,方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收。引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量 。方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收。局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。this在堆空间分配。数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小。
    • 栈运行原理
      • 栈中的数据都是以栈帧的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在栈中从入栈到出栈的过程。
  • 本地方法栈(Native Method Stack)(线程私有)
    • 本地方法栈和JVM栈发挥的作用非常相似,也是线程私有的,区别是JVM栈为JVM执行Java方法(也就是字节码)服务,而本地方法栈为JVM使用到的Native方法服务。它的具体做法是在本地方法栈中登记native方法,在执行引擎执行时加载Native Liberies.有的虚拟机(比如Sun Hotpot)直接把两者合二为一。
    • 能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。
  •  程序计数器(Program Counter Register)(线程私有)
    • cpu执行的最小单位是线程,cpu是以时间片的方式去执行
    • 程序计数器是一块很小的内存空间,作用是为了线程切换可以恢复到正确执行位置,所以每个线程都需要一个独立的计数器来记录指令执行到的位置
    • 可以看作是当前线程所执行的字节码的行号指示器,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令。

6、执行引擎(Execution Engine)

  • 执行引擎以指令为单位读取Java字节码。它就像一个CPU一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。
  • 不过Java字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被JVM执行的语言。字节码可以通过以下两种方式转换成合适的语言
    • 解释器: 一条一条地读取,解释并执行字节码执行,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行语言的一个缺点。
    • 即时编译器:用来弥补解释器的缺点,执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行。执行本地代码比一条一条进行解释执行的速度快很多,编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

7、垃圾收集(Garbage Collection, GC)

  • 什么是垃圾收集
    • 垃圾收集即垃圾回收,简单的说垃圾回收就是回收内存中不再使用的对象。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用对象),则没有被任何指针给指向,因此占用的内存也可以被回收掉。
    • 垃圾回收分两步
      • 查找内存中不再使用的对象(GC判断策略)
      • 释放这些对象占用的内存(GC收集算法)
  • 1》GC判断策略
    • 1) 引用计数算法
      • 引用计数(Reference Count)方式是GC算法中最简单也最容易实现的一种。它给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
      • 优点:用计数收集器执行简单,判定效率高,交织在程序运行中。当对象不再被引用的瞬间就会被释放
      • 缺点:无法释放循环引用的对象。
    • 2)可达性分析算法
      • 该算法的基本思想就是通过一系列被称为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到任何一个GC Root没有任何引用链相连时,则证明此对象不可用。
      • 在Java语言里,可作为GC Roots的对象包括以下几种:扩展链接

        • 虚拟机栈(栈帧中的本地变量表)中引用的对象;

        • 方法区中类静态属性引用的对象;
        • 方法区中常量应用的对象;
        • 本地方法栈中JNI(Native方法)引用的对象。
      • 在根搜索算法中不可达的对象,也并非是“非死不可”的,因为要真正宣告一个对象死亡,至少要经历两次标记过程:第一次是标记没有与GC Roots相连接的引用链;第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过)。
      • 优点: 可以解决放循环引用的对象的清空。缺点: 实现较为复杂

  • 2》GC收集算法
    • 垃圾收集算法中很多都是基于可达性分析进行是现实的。
    • 标记-清除算法(Mark-Sweep)
      • 标记-清楚算法采用从根集合(GC Roots)进行扫描,首先标记出所有需要回收的对象(根搜索算法),标记完成后统一回收掉所有被标记的对象
      • 优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
      • 缺点:
        • 效率问题:标记阶段和清除阶段都需要对全部对象进行遍历。标记和清除过程的效率都不高
        • 空间问题:标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。因此本次分配还是会失败的
    • 标记—整理算法
      • 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。
      • 优点: 不会再有碎片的问题了。
      • 缺点: GC暂停的时间会增长。(因为要移动对象)
    •  复制算法(Copying)
      • 复制算法是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象复制到另外一块上面, 然后把已使用过的内存空间一次清理掉。
      • 优点:

        • 1.标记阶段和复制阶段可以同时进行。

        • 2.每次只对一块内存进行回收,运行高效。

        • 3.只需移动栈顶指针,按顺序分配内存即可,实现简单。

        • 4.内存回收时不用考虑内存碎片的出现(得活动对象所占的内存空间之间没有空闲间隔)。

      • 缺点 可一次性分配的最大内存缩小了一半。
    • 分代收集算法(Generational Collection)
      • 分代收集算法是目前大部分JVM的垃圾收集器采用的算法,它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域
      • 一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是元空间(Metaspace),JDK8之后-废弃永久代(PermGen)迎来元空间(Metaspace)
      • 老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
      • 年轻代的(Young Generation)回收算法(以复制回收算法为主)
        • 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区(设置为8:1:1的原因也是为了尽量让对象在年轻代就被回收点,而不是接入老年代

        • 所有新生成的对象首先都是放在年轻代的伊甸区(人类偷吃禁果的地方)。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象

        • 伊甸区(Eden),s0区,s1区不断的进行复制回收,Eden区满了复制到s0区,s0满了复制到s1区。。。

          • 1》如果大对象Eden装不下那就直接分配到老年代
          • 2》如果在Eden区的内存不够分配下一个对象了,这时会对Eden区触发一次Minor GC,把还存活的对象复制到s0区,然后清空Eden区
          • 3》如果s0区也满了,则会对eden区和s0区进行一次Minor GC,把存活对象复制到另一个s1区,然后清空eden和这个s0区
          • 4》经过第三步GC,此时s0区是空的,然后将s0区和s1区交换,即保持s1区为空, 如此往复
          • Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而且,Survivor区总有一个是空的同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。所以上面年轻代的比例8:1:1是默认的但是可以手动调节

          • 5》当s1区不足以存放 eden和s0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Major GC,也就是新生代、老年代都进行回收。
          • 6》新生代发生的GC叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
      • 年老代(Old Generation)的回收算法(以标记-整理收集算法为主)
        • 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
        • 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC,Major GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
        • 因为Major GC的效率比Minor GC 要慢很多(估计10倍不止),所以理想情况下,所有的对象都最好不要进入老年代,而是在年轻代就被回收掉
      • 总结:对新生代进行垃圾回收叫做minor GC,对老年代进行垃圾回收叫做major GC,同时对新生代、老年代和永久代进行垃圾回收叫做full GC
        • 许多major GC是由minor GC触发的,所以很难将这两种垃圾回收区分开
        • major GC和full GC通常是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是major GC
      • 持久代(Permanent Generation)
        • 持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃 圾收集影响比较大的。持久代大小通过-XX:MaxPermSize=<N>进行设置。
        • 永久代也称方法区。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过根搜索算法来判断,但是对于无用的类则需要同时满足下面3个条件:

          • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
          • 加载该类的ClassLoader已经被回收;
          • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
      • 元空间(mate space)
        • 在java8的新版本中,持久代已经更名为了元空间(meta space)。
        • JDK1.8中,转而使用元空间。而元空间是直接存在外部内存中,不在java虚拟机中的,因此元空间依赖于内存大小。当然你也可以自定义元空间大小。
        • 为什么叫元空间,是因为这里面存储的是类的元数据信息
          • 元数据(Meta Date),关于数据的数据或者叫做用来描述数据的数据或者叫做信息的信息。
          •  这些定义都很是抽象,我们可以把元数据简单的理解成,最小的数据单位。元数据可以为数据说明其元素或属性(名称、大小、数据类型、等),或其结构(长度、字段、数据列),或其相关数据(位于何处、如何联系、拥有者)
      • 永久代和元空间总结:
        • 永久代
          • 用于存放被虚拟机加载的类的元数据信息:如常量、静态变量、即时编译器编译后的代码。也称之为方法区
          • 也有人说永久代是在堆中,这些都没错,因为逻辑上永久代(方法区)和堆是分开各自独立的,但是物理空间上两者是在一起的都在jvm虚拟机内存中,两者都是线程共享的,所以说方法区在堆内存上也没有错
        • 元空间:
          • 元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
          • 但可以通过以下参数来指定元空间的大小:-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
      • liunx查看heap使用情况

8、垃圾收集器

  • 1) Serial收集器
  • Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它在进行垃圾收集时,会暂停所有的工作进程,用一个线程去完成GC工作。特点:简单高效,适合jvm管理内存不大的情况(十兆到百兆)。
  • 2) ParNew收集器
    • ParNew收集器其实是Serial的多线程版本,回收策略完全一样,所以它配合多核心的cpu效果更好,如果是一个cpu,他俩效果就差不多。(可用-XX:ParallelGCThreads参数控制GC线程数)
  • 3) CMS (Concurrent Mark Sweep)收集器
    • CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上的并发收集器, 虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如Taobao),又称多并发低暂停的收集器。
    • 由他的英文组成可以看出,它是基于标记-清除算法实现的。整个过程分4个步骤:
      • 初始标记(CMS initial mark):仅只标记一下GC Roots能直接关联到的对象, 速度很快

      • 并发标记(CMS concurrent mark: GC Roots Tracing过程)

      • 重新标记(CMS remark):修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录

      • 并发清除(CMS concurrent sweep: 已死对象将会就地释放)

    • 可以看到,初始标记、重新标记需要STW(stop the world 即:挂起用户线程)操作。因为最耗时的操作是并发标记和并发清除。所以总体上我们认为CMS的GC与用户线程是并发运行的
    • 优点:并发收集、低停顿
    • 缺点:
      • CMS默认启动的回收线程数=(CPU数目+3)*4
        当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低.

      • 无法清除浮动垃圾(GC运行到并发清除阶段时用户线程产生的垃圾),因为用户线程是需要内存的,如果浮动垃圾施放不及时,很可能就造成内存溢出,所以CMS不能像别的垃圾收集器那样等老年代几乎满了才触发,CMS提供了参数-XX:CMSInitiatingOccupancyFraction来设置GC触发百分比(1.6后默认92%),当然我们还得设置启用该策略-XX:+UseCMSInitiatingOccupancyOnly

      • 因为CMS采用标记-清除算法,所以可能会带来很多的碎片,如果碎片太多没有清理,jvm会因为无法分配大对象内存而触发GC,因此CMS提供了-XX:+UseCMSCompactAtFullCollection参数,它会在GC执行完后接着进行碎片整理,但是又会有个问题,碎片整理不能并发,所以必须单线程去处理,所以如果每次GC完都整理用户线程stop的时间累积会很长,所以XX:CMSFullGCsBeforeCompaction参数设置隔几次GC进行一次碎片整理(默认为0)。

 

 

  • 4) G1(Garbage First)收集器
    • 同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。
    • 因为每个区都有E、S、O代,所以在G1中,不需要对整个Eden等代进行回收,而是寻找可回收对象比较多的区,然后进行回收(虽然也需要STW操作,但是花费的时间是很少的),保证高效率。
    • 新生代收集
      • G1的新生代收集跟ParNew类似,如果存活时间超过某个阈值,就会被转移到S/O区。

        年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域的大小

    • 老年代收集
      • 初始标记 (Initial Mark: Stop the World Event)
        在G1中, 该操作附着一次年轻代GC, 以标记Survivor中有可能引用到老年代对象的Regions.

      • 扫描根区域 (Root Region Scanning: 与应用程序并发执行)
        扫描Survivor中能够引用到老年代的references. 但必须在Minor GC触发前执行完

      • 并发标记 (Concurrent Marking : 与应用程序并发执行)
        在整个堆中查找存活对象, 但该阶段可能会被Minor GC中断

      • 重新标记 (Remark : Stop the World Event)
        完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除并回收, 并计算所有区域的活跃度).

      • 清理 (Cleanup : Stop the World Event and Concurrent)
        在含有存活对象和完全空闲的区域上进行统计(STW)、擦除Remembered Sets(使用Remembered Set来避免扫描全堆,每个区都有对应一个Set用来记录引用信息、读写操作记录)(STW)、重置空regions并将他们返还给空闲列表(free list)(Concurrent)

  • 参考文章

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值