深入浅出的JVM

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出的JVM相关的知识,希望对你有一定的参考价值。

一、jvm的组织结构

  (1)jvm 和系统调用之间的关系

技术分享图片
相应的名词解释
   - 类加载器:在jvm启动或者类运行时将需要的class加载到jvm内存中
   - 执行引擎:负责执行class文件中包含的字节指令
   - 内存区:是在jvm运行的时候操作锁分配的内存区。运行时内存区分五个部分:堆、方法区、栈、本地方法栈、程序计数器,
   - 本地方法接口:主要是调用c或者c++实现的本地方法及返回结果。

  (2)jvm 的内存结构

技术分享图片
   jvm内存结构主要由三大块:堆内存、方法区和栈。
   堆内存:是jvm中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三个部分:Eden 空间(对相关刚刚被创建时存放的位置)、From Survivor() 空间、To Survivor(存货下来的对象的存放区域) 空间,默认情况下年轻代按照 8:1:1 的比例来分配。
   方法区(永久代):存储信息、常量、静态变量等数据,是线程共享的区域,为与java堆区分,方法区还有一个别名Non-Heap(非堆) jvm各区域的作用。
   :又分为java虚拟机栈和本地方法栈和程序计数器,主要用于方法的执行。

  (3)各个区域参数调控

技术分享图片
堆内存的设置:

-Xms:设置堆的最小空间大小
-Xmx:设置堆的最大空间大小
-XX:NewSize 设置新生代最小空间大小
-XX:MaxNewSize:设置新生代最大空间大小
-XX:PermSize 设置永久代最小空间大小
-XX:MaxPermSize 设置永久代最大空间大小
-Xss 设置每个线程的堆栈大小
没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控 制:老年代空间大小=堆空间大小-年轻代大空间大小

二、jvm各内存区域的作用

(1)各个区域的概述:

  • 共享区
    • 方法区
  • 线程的私有区域
    • java栈
    • 本地方法栈
    • 程序计数器

(2)java 堆(heap):

  对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
  java堆是垃圾收集器管理的主要区域,因此很多时候也被成为GC堆。如果从内存回收的角度看,由于现在收集器基本都是采用分代收集算法,所以java堆中还可以划分:新生代和老年代;在细一点的划分:新生代又包括:Eden 空间、From Survivor 空间、To Survivor 空间。
  如果在堆中没有内存完成实例分配(堆中没有内存装得下对象),并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。
技术分享图片
补充(新生代到老年的过程):大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,此时对象会进入survivor区,当对象满足一些条件后会进入老年代。
  - 长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
  - 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  - 虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

(3)方法区(method area):

   -方法区与java一样,是各个线程的共享的内存区域,它用于存储被jvm加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。
  jvm规范对这个区域的限制非常宽松,除了和java堆一样不需要连续的内存和可以选择固定大小,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,到哪并非数据进入的方法区就“永远”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收 “成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。
  根据 Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

(4)程序计数器:

  程序计数器时一小块的内存空间,它的作用可以看做是当前程序所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。
  由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在 任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程 中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
  如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 native 方法(调用时使用底层的指令),这个计数器值则为空(Undefined)。
  此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

(5)java虚拟机栈:

  Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候 都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
技术分享图片
  局部变量表存放了编译期可知的各种基本数据类型,局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  关于java虚拟机栈的异常:1) 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常 2) 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。

(6)本地方法栈:

  使用native修饰的方法,则存储在本地方法栈中。与虚拟机栈一样,本地方法栈 区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

三、jvm的垃圾回收机制介绍

(1)jvm 垃圾回收介绍:

  垃圾收集Garbage Collection通常被称为“GC”,它诞生于1960年MT的Lisp语言。Jvm中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的 进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 Java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

(2)垃圾收集器(Garbage Collection)的介绍:

  GC其实是一种自动的内存管理工具器行为主要包括2个步骤:在和Java堆中,为新的创建的对象分配空间、回收没有用的对象的内存空间

(3)实现多种GC的好处:

  Java 平台被部署在各种各样的硬件资源上,其次,在 Java 平台上部署和运行着各种 各样的应用,并且用户对不同的应用的性能指标(吞吐率和延迟)预期也不同,为了满足不同 应用的对内存管理的不同需求,JVM 提供了多种 GC 以供选择。
  GC的性能指标主要包括
   - 最大停顿时长:圾回收导致的应用停顿时间的最大值
   - 吞吐率:垃圾回收停顿时长和应用运行总时长的比例
   例:一次应用程序运行了60s,然后GC的时长为2s(进行了4次GC:0.5,0.8,0.2,0.5),那么最大的停顿时长为:0.8,吞吐率为:(60-2)/60
  GC的种类大概分为
   - 序列化GC:适合占用内存少的应用
   - 并行GC或者吞吐率GC,适合占用内存较多,多 CPU,追求高吞吐率的应用。
   - 并发GC:适合占用内存较多,多 CPU 的应用,对延迟有要求的应用。
   并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
   并发:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交 替执行)。

(4)对象存活的判断:

   - 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加 1,引用释放时计数减 1,计数为 0 时可以回收。此方法简单,缺点是无法解决对象相互循环引用的问题。
   - 可达性分析:从 GC Roots 开始向下搜索,搜索所走过的路径称为 引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的不可达对象。
技术分享图片
可达对象:通过根对象进行引用搜索,最终可以达到的对象。
不可达对象:通过根对象进行引用搜索,最终没有被引用到的对象。
Java 语言中,GC Roots 包括:

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

    (5)MinorGC和Full GC:

      新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生 夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
      老年代GC(Full GC):是清理整个堆空间—包括年轻代和老年代或者永久代。。Full GC 的速度一般会比 Minor GC 慢 10 倍以上。

    四、垃圾回收算法:

    (1)标记-清除算法:

    技术分享图片
      “标记-清除”(Mark-Sweep)算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
      缺点
       - 一个是效率问题,标记和清除过程的效率都不高;
       - 一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能 会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

    (2)复制算法:

    技术分享图片
      复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用 其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
      缺点:
      - 这种算法的代价 是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

    (3)标记整理算法:

    技术分享图片
      标记-整理”(Mark-Compact)算法,标记过程仍 然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

    (4)分代收集算法:

      分代收集”(Generational Collection)算法,把 Java 堆分为新生代和老年代,这样就可以 根据各个年代的特点采用最适当的收集算法。
      年轻代(生存周期短,大量对象都是垃圾对象) 使用复制算法。
      年老代(生存周期长,少量对象时垃圾对象) 使用标记整理,或者标记清除。

    五、垃圾收集器:

      如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。不同厂商、不 同版本的虚拟机实现差别很大,HotSpot 中包含的收集器如下:
    技术分享图片

    (1)Serial 收集器(单核服务器,最快,单线程版本):

      串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线 程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集 的过程中会 Stop The World(服务暂停)
    技术分享图片
    这种算法的缺点就是:在垃圾回收的时候,会停止其他的线程

    (2)ParNew收集器(多线程版本)

      ParNew 收集器其实就是 Serial 收集器的多线程版本。
      新生代并行,老年代串行;新生代复制算法、老年代标记-整理
    技术分享图片

    (3)Parallel收集器(多线程)

      Parallel Scavenge 收集器类似 ParNew 收集器,Parallel 关注吞吐量的多线程 ParNew 进化版。新生代复制算法、老年代标记-整理算法。
    技术分享图片

    (4)CMS收集器(多线程)

      CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目 前很大一部分的 Java 应用都集中在互联网站或 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
    技术分享图片
    优点:并发收集、低停顿
    缺点:产生大量的空间碎片,并发节点吞吐率低。
    参数配置

    -XX:+UseConcMarkSweepGC 使用 CMS 收集器 
    -XX:+ UseCMSCompactAtFullCollection Full GC 后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长 
    -XX:+CMSFullGCsBeforeCompaction 设置进行几次 Full GC 后,进行一次碎片整理 
    -XX:ParallelCMSThreads 设定 CMS 的线程数量(一般情况约等于可用 CPU 数量)

    (5)G1收集器(多线程)

      特点:
       - 空间整合:G1 收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次 GC。
       - 可预测的停顿:,降低停顿时间是 G1 和 CMS 的共同关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 N 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ) 的垃圾收集器的特征了。
       - G1搜集器,Java 堆的内存布局与其他收集器有很大差别,它将整个 Java 堆划分为多个大 小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region 的集合。
    技术分享图片
      G1 的新生代收集跟 ParNew 类似,当新生代占用达到一定比例的时候,开始触发收集。这样一来可以做到,当达到一定的比例时,触发垃圾回收,那些没有使用region仍然可以对外提供使用,就有效的避免了stop the world。
      与CMS的比较:
       - 分代:CMS 中,堆被分为 PermGen,YoungGen,OldGen;而 YoungGen 又分了两个 survivor 区域。在 G1 中,堆被平均分成几个区域(region),在每个区域中,虽然也保留了新老代的概 念,但是收集器是以整个区域为单位收集的。
       - 算法:相对于 CMS 的“标记—清除”算法,G1 会使用“标记--整理”算法,保证不产生 多余的碎片。
       - 停顿时间可控制:了缩短停顿时间,G1 建立可预存停顿模型,这样在用户设置的停顿时 间范围内,G1 会选择适当的区域进行收集,确保停顿时间不超过用户指定时间。

    六、jvm参数配置以及常用的分析工具:

    (1)jvm的参数列表:

    各个内存大小分配

    java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
    -Xmx3550m:最大堆内存为 3550M
    -Xms3550m:初始堆内存为 3550m
    一般情况:-Xmx与-Xms值设置为相同的,以避免每次垃圾回收完成后 JVM 重新分配内存。
    -Xmn2g:设置年轻代大小为 2G
    -Xss1m:设置每个线程的堆栈大小
    -XX:NewRatio=4:设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久 代)。设置为 4,则年轻代与年老代所占比值为 4:1,年轻代占整个堆栈的 4/5
    -XX:SurvivorRatio=4:设置年轻代中 Eden 区与 Survivor 区的大小比值。
    -XX:MaxPermSize=16m:设置持久代大小为 16m。
    -XX:MaxTenuringThreshold=15:设置垃圾最大年龄。

    收集器设置

    -XX:+UseSerialGC 设置串行收集器 
    -XX:+UseParallelGC 设置并行收集器 
    -XX:+UseParalledlOldGC 设置并行年老代收集器 
    -XX:+UseConcMarkSweepGC 设置并发收集器

    垃圾回收统计信息

    -XX:+PrintGC
    –XX:+PrintGCDetails 
    -XX:+PrintGCTimeStamps 
    -Xloggc:filename

    并行收集器设置

    -XX:ParallelGCThreads=n 设置并行收集器收集时使用的 CPU 数。并行收集线程数
    -XX:MaxGCPauseMillis=n 设置并行收集最大暂停时间
    -XX:GCTimeRatio=n 设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n)

    并发收集器设置

    -XX:+CMSIncrementalMode 设置为增量模式。适用于单 CPU 情况
    -XX:ParallelGCThreads=n 设置并发收集器年轻代收集方式为并行收集时,使用的 CPU 数并行收集线程数

    (2)jvm分析工具:

    jps:查看java进程

    -q 只显示pid,不显示class名称,jar文件名和传递给main 方法的参数
    -m 输出传递给main 方法的参数,在嵌入式jvm上可能是null
    -l 输出应用程序main class的完整package名 或者 应用程序的jar文件完整路径名
    -v 输出传递给JVM的参数
    jps host 查看host的jps情况(前提:host提供jstatd服务)

    jstatd:启动jvm监控服务。它是一个基于rmi(远程接口调用)的应用,向远程机器提供本机jvm应用程序的信息。默认端口1099。-p指定端口。
    jmap1:观察运行中的jvm物理内存的占用情况

    -heap:打印jvm heap的情况(垃圾收集器类型)
    -histo:打印jvm heap的直方图。其输出信息包括类名,对象数量,对象占用大小。
    -histo:live :同上,但是只打印存活对象的情况
    -permstat:打印permanent generation heap(方法区)情况
    -finalizerinfo:打印正等候回收的对象信息

    jmap2:用jmap把进程内存使用情况dump到文件中,再用jhat分析查看。

    jmap -dump:format=b,file=dumpFileName pid
    jmap -dump:format=b,file=4574.heap20151215  4574

    jinfo:打印命令行参数和系统属性

    -flags  打印命令行参数
    -sysprops  打印系统属性

    jstack1:能得到运行java程序的java stack和native stack的信息。可以轻松得知当前线程的运行情况

    -l长列表. 打印关于锁的附加信息,例如属于java.util.concurrent的ownable synchronizers列表
    -m打印java和native c/c++框架的所有栈信息

    jstat

    Options — 选项,我们一般使用 -gcutil /-gc 查看gc情况
    pid      — VM的进程号,即当前运行的java进程号
    interval[s|ms]  ——  间隔时间,单位为秒或者毫秒,默认为ms。必须是正整型
    count     — 打印次数,如果缺省则打印无数次
    例:jstat -gc 4645 500 10  表示查看进程为4645的gc每个 500ms打印一次,一共打印10次

以上是关于深入浅出的JVM的主要内容,如果未能解决你的问题,请参考以下文章

深入理解JVM_java代码的执行机制01

深入浅出-JVM:栈帧

深入理解jvm原理之逃逸分析

深入浅出JVM

深入JVM - Code Cache内存池

深入JVM - Code Cache内存池