JVM

JVM

JVM内存模型

JAVA优势:在JVM管理机制之下,不再需要为每一个new操作去写配对的内存分配和回收等代码, 不容易出现内存泄漏和内存溢出等问题;一处编译随处运行。

jvm-model.png

程序计数器

  • 介绍
    • 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
    • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 功能
    • 可以使线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存
  • 特点
    • 内存区域中唯一一 个没有规定任何 OutOfMemoryError 情况的区域

JAVA虚拟机栈

  • 介绍

    • 作用于方法执行的一块Java内存区域
  • 功能

    • 每个方法在执行的同时都会创建一个栈帧(Stack Framel)用于存储局部变量表操作数栈动态链接方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程;先进后出,后进先出。
  • 特点

    • 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)以及对象引用(reference 类型)

    • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常

      public class TestA {
          public static void main(String[] args) {
              methodA();
          }
      
          public static void methodA() {
              methodA();
          }
      }
      

本地方法栈

  • 介绍
    • 本地方法(native修饰)执行的一块Java内存区域
  • 功能
    • 与Java虚拟机栈相同,每个方法在执行的同时都会创建一个栈帧(Stack Framel)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 特点
    • Hotshot将Java虚拟机栈和本地方法栈合二为一

  • 介绍
    • 是Java内存区域中一块用来存放对象实例的区域,【几乎所有的对象实例都在这里分配内存】。
  • 功能
    • 此内存区域的唯一目的就是存放对象实例
  • 特点
    • Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块
    • Java 堆是被所有线程共享的一块内存区域。
    • Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。
    • Java堆可以分成新生代和老年代;新生代可分为To Space、From Space、Eden。

方法区

  • 介绍
    • 与Java堆一样,是各个线程共享的内存区域
  • 功能
    • 用于存储已被虚拟机加载的类信息(类版本号、方法、接口)、常量、静态变量、即时编译器编译后的代码等数据
  • 特点
    • 并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载
    • 当它无法满足内存分配需求时会抛出OutofMemoryError

常量池

  • 介绍

    • 静态常量池:存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量符号引用,字面量包括字符串,基本类型的常量;符号引用其实引用的就是常量池里面的字符串,但符号引用不是直接存储字符串,而是存储字符串在常量池里的索引
    • 运行时常量池:方法区的一部分;动态常量池是当class文件被加载完成后,java虚拟机会将静态常量池里的内容转移到动态常量池里,在静态常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用
  • 功能

    • 常量池是为了避免频繁的创建和销毁对象造成系统性能的浪费,实现了对象的共享。
  • 特点

    • 对于byte、short、long、char、boolean对应的包装器类都有对应的常量池,这五种包装器类默认创建在-128到127的对象会存放在在缓存中;对于两种浮点数没有实现常量池技术。

    • 受到方法区内存的限制,当常量池再申请到内存时会抛出OutOfMemoryError异常。

      public class TestA {
          public static void main(String[] args) {
              String a = "hello";
              String b = new String("hello");
              System.out.println(a == b);
              System.out.println(a == b.intern());
          }
      }
      

JAVA对象

创建过程

  • 虚拟机遇到一条new指令时,首先检查这个对应的类能否在常量池中定位到一个类的符号引用

  • 判断这个类是否被加载

  • 执行类加载、解析和初始化

  • 为新生对象在堆中分配内存空间,堆分配内存空间的两种方式如下

    • 指针碰撞

      开辟一块内存和移动指针两个步骤;非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性

    • 空闲列表

      开辟一块内存和修改空闲列表两个步骤;非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性

  • 对新分配的内存空间执行初始化为零值操作

  • 保存新生对象头信息(GC分代年龄,对象的hashcode,元数据信息)

  • 执行对象的init方法

内存分布

obj-mem.png
  • 对象头用于存储对象的元数据信息:
    • 运行时数据(Mark Word )部分数据的长度在32位和64位虚拟机(未开启压缩指针)中分别为32bit和64bit,存储对象自身的运行时数据如哈希值等。Mark Word一般被设计为非固定的数据结构,以便存储更多的数据信息和复用自己的存储空间。
    • 类型指针 指向它的类元数据的指针,用于判断对象属于哪个类的实例。
  • 实例数据存储的是真正有效数据,如各种字段内容,各字段的分配策略为longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类定义的变量的前面。
  • 对齐填充部分仅仅起到占位符的作用

对象访问

对象的访问指的是访问对象的实例数据及类型数据

call-obj.png
  • 当我们在堆上创建一个对象实例后,就要通过虚拟机栈中的reference类型数据来操作堆上的对象。现在主流的访问方式有两种(HotSpot虚拟机采用的是直接指针访问)
    1. 直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针
    2. 使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了对象实例数据与类型数据的具体地址信息,相当于二级指针
  • 对比
    • 垃圾回收分析:句柄访问时,当垃圾回收移动对象时,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址;直接指针访问时垃圾回收时需要修改reference中存储的地址。
    • 访问效率分析,直接指针访问优于句柄访问,因为直接指针访问只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。

垃圾对象标记算法

引用计数法

堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1

reference-count.png
  • 优点

    引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

  • 缺点

    无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0

    public class ReferenceCountGC {
        public Object instance = null;
        public static void main(String[] args) {
            ReferenceCountGC rGca = new ReferenceCountGC();
            ReferenceCountGC rGcb = new ReferenceCountGC();
            rGca.instance = rGcb;
            rGcb.instance = rGca;
            rGca = null;
            rGcb = null;
        }
    }
    

可达性分析算法

可达性分析算法(又叫跟搜索法)

根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点

arrived.png
  • java中可作为GC Root的对象有
    • 虚拟机栈中引用的对象(本地变量表)
    • 本地方法栈中引用的对象
    • 方法区中静态属性引用的对象
    • 方法区中常量引用的对象

垃圾回收算法

Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存

标记清除算法

  • 标记清除

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

sign-clear.png
  • 缺点
    • 效率问题:标记和清除两个过程的效率都不高;
    • 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法

  • 原因分析

    为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按量划分为大小相等的两块,每次只使用其中的一块

  • 复制算法

    当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效

copy.png
  • 现在的商业虚拟机都采用这种收集算法来回收新生代,研究表明,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor (Survivor from 、Survivor to)空间,每次使用 Eden 和其中一块 Survivor。Eden 、 Survivor from 和Survivor to 的内存比例 8:1:1
  • 当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1, 也就是每次新生代中可用内存空间为整个新生代容量的 90% (80%+10%),只有 10% 的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

标记整理算法

  • 原因分析

    复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100%存活的极端情况,所以在老年代一般不能直接选用这种算法

  • 标记整理

    根据老年代的特点,有人提出了另外一种“标记-整理(Mark- Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

sign-sort.png

分代收集算法

  • 一般把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法
  • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记一整理”算法来进行回收

垃圾收集器

并发与并行

并行(Parallel):指多条垃圾收集线程并行工作。

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行)。

Serial收集器

Serial是一个单线程的垃圾收集器

serial.png

特点:

  • “Stop The World”,它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。在用户不可见的情况下把用户正常工作的线程全部停掉
  • 使用场景:多用于桌面应用,Client端的垃圾回收器(桌面应用内存小,进行垃圾回收的时间比较短,只要不频繁发生停顿就可以接受)

ParNew收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数(例如:-XX: SurvivorRatio、-XX: PretenureSize' Threshold、-XX: HandlePromotionFailure 等)、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一样,在实现上,这两种收集器也共用了相当多的代码

parnew.png

特点:

  • ParNew 收集器除了多线程收集之外,其他与 Serial 收集器相比并没有太多创新之处,但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
  • 使用-XX: ParallelGCThreads 参数来限制垃圾收集的线程数(多线程操作存在上下文切换的问题,所以建议将-XX: ParallelGCThreads设置成和CPU核数相同,如果设置太多的话就会产生上下文切换消耗)

Parallel Scavenge收集器

Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器;由于与吞吐量关系密切,Parallel Scavenge 收集器也经常称为“吞吐量优先”收集器。

特点:

  • Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到个可控制的吞吐(Throughput)。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99% 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
  • 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为 GC自适应调节策略
  • -XX:MaxGCPauseMillisGC停顿时间参数
  • -XX:GCTimeRatio,吞吐量参数

GMS收集器

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

gms.png
  • 步骤:
    1. 初始标记(CMS initial mark) :标记一下 GC Roots 能直接关联到的对象,速度很快
    2. 并发标记(CMS concurrent mark) :并发标记阶段就是进行 GC RootsTracing 的过程
    3. 重新标记(CMS remark) :为了修正并发标记期间因用户程序导致标记产生变动的标记记录
    4. 并发清除(CMS concurrent sweep) :执行垃圾回收工作
  • 缺点
    • 对CPU资源非常敏感
    • 无法处理浮动垃圾,程序在进行并发清除阶段用户线程所产生的新垃圾
    • 标记-清除暂时空间碎片

G1收集器

G1是一款面向服务端应用的垃圾收集器;G1 中每个 Region 都有一个与之对应的 Remembered Set,当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 。


g1.png
  • 步骤
    1. 初始标记(Initial Marking):标记一下 GC Roots 能直接关联到的对象
    2. 并发标记(Concurrent Marking):从GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
    3. 最终标记(Final Marking) :为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs的数据合并到 Remembered Set 中
    4. 筛选回收(Live Data Counting and Evacuation):执行垃圾回收工作
  • 优势
    • 空间整合:基于“标记一整理”算法实现为主和Region之间采用复制算法实现的垃圾收集
    • 可预测的停顿:这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型
    • 在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 堆的内存布局就与其他收集器有很大差别,它将整个 Java 雄划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔髙的了,它们都是一部分 Region(不需要连续)的集合。
    • G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Regions 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage- Firsti 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高

内存自动分配

堆内存分布

heap.png

堆内存分配方式

  • 分配原则
    • 对象主要分配在新生代的 Eden 区上
    • 如果启动了本地线程分配缓冲,将按线程优先分配在 TLAB 上分配
    • 少数情况下也可能会直接分配在老年代中
  • 新生代GC与老年代GC

    • 新生代 GC (Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,
    • 老年代 GC (Major GC/ Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。Major GC 的速度一般会比 Minor GC 慢 10 倍以上。
  • 大对象分配原则

    所谓的大对象是指,需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组;虚拟机提供了一个-XX: PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。

  • 代码

    参数说明

    type desc
    -verbose:gc -XX:+PrintGCDetails 开启GC日志打印
    -Xx: SurvivorRatio=8 指定新生代中 Eden 区与两个 Survivor 区的空间比例是 8:1:1
    -XX:+UseSerialGC 使用Serial GC
    -XX:PretenureSizeThreshold 设置在老年代分配的大对象阀值(单位字节)
    -Xms20M 设置初始堆内存为20M
    -Xmx20M 设置 最大堆内存为20M
    -Xmn10M 设置新生代内存大小为10M
  • 代码

    public class TestMaxObj {
        private static final int byteSize = 1024 * 1024;
        public static void main(String[] args) {
            byte[] bytes = new byte[5 * byteSize];
        }
    }
    
  • JVM参数

    -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
    

逃逸分析和栈上分配

  • 逃逸分析

    逃逸分析的基本行为就是分析对象动态作用域

    • 方法逃逸:当一个对象在方法中被定义后,被外部方法所引用
    • 线程逃逸:当一个对象在方法中被定义后,被外部线程访问
  • 栈上分析

    栈上分配就是把方法中的变量和对象分配到栈上,方法执行完后自动销毁,而不需要垃圾回收的介入,从而提高系统性能

    -XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启)
    -XX:-DoEscapeAnalysis 关闭逃逸分析
    

虚拟机工具

查看运行日志、异常堆栈、GC 日志、线程快照( threaddump/javacore文件)、堆转储快照( heapdump/hprof文件)等。使用适当的虚拟机监控和分析的工具可以加快我们分析数据、定位解决问题的速度。

jps

java提供的一个显示当前所有java进程pid的命令,适合在linux/unix平台上简单察看当前java进程的一些简单情况

命令 解释
jps 显示java进程pid
jps -l 输出应用程序main class的完整package名或者应用程序的jar文件完整路径名
jps -v 输出虚拟机进程启动时JVM参数
jps -m 输出传递给main方法的参数,在嵌入式jvm上可能是null

jstat

用于监视虚拟机各种运行状态信息的命令行工具

  • 用法

    jstat [-命令选项][vmid][间隔时间/毫秒][查询次数]

  • -命令选项详解

    命令 详解
    -class 类加载统计
    -compiler 编译统计
    -gc 垃圾回收统计
    -gccapacity 堆内存统计
    -gcnew 新生代垃圾回收统计
    -gcnewcapacity 新生代内存统计
    -gcold 老年代垃圾回收统计
    -gcoldcapacity 老年代内存统计
    -gcpermcapacity JDK7 下 永久代空间统计
    -gcmetacapacity JDK8 下 元数据空间统计
    -gcutil 总结垃圾回收统计
    -printcompilation JVM编译方法统计
  • 测试

    # 4568 表示进程id ,1000毫秒打印一次 ,打印50次
     jstat -gc 4568 1000 50   
     
     # 参数解释 
     S0C:第一个幸存区的大小
     S1C:第二个幸存区的大小
     S0U:第一个幸存区的使用大小
     S1U:第二个幸存区的使用大小
     EC:伊甸园区的大小
     EU:伊甸园区的使用大小
     OC:老年代大小
     OU:老年代使用大小
     MC:方法区大小
     MU:方法区使用大小
     CCSC:压缩类空间大小
     CCSU:压缩类空间使用大小
     YGC:年轻代垃圾回收次数
     YGCT:年轻代垃圾回收消耗时间
     FGC:老年代垃圾回收次数
     FGCT:老年代垃圾回收消耗时间
     GCT:垃圾回收消耗总时间
    

jinfo

实时地查看和调整虚拟机各项参数

  • 用法

    # pid进程id
    # option下方解释
    Usage:
        jinfo [option] 
            (to connect to running process)
        jinfo [option] 
            (to connect to a core file)
        jinfo [option] [server_id@]
            (to connect to remote debug server)
    
    where 
  • -flag

    用于打印虚拟机标记参数的值,name表示虚拟机标记参数的名称。

    [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jinfo -flag PrintGC 455
    -XX:-PrintGC
    
  • -flag [+|-]

    用于开启或关闭虚拟机标记参数。+表示开启,-表示关闭。

    [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jinfo -flag +PrintGCDetails 455
    [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jinfo -flag PrintGCDetails 455
    -XX:+PrintGCDetails
    [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ 
    
  • -flag =

    用于设置虚拟机标记参数,但并不是每个参数都可以被动态修改的。

    [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jinfo -flag HeapDumpPath 455
    -XX:HeapDumpPath=
    [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jinfo -flag HeapDumpPath=/opt/heapError.hprof 455
    [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jinfo -flag HeapDumpPath 455
    -XX:HeapDumpPath=/opt/heapError.hprof
    
  • -flags

    打印虚拟机参数

    [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jinfo -flags 455
    Attaching to process ID 455, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.231-b11
    Non-default VM flags: -XX:CICompilerCount=2 -XX:HeapDumpPath=null -XX:InitialHeapSize=31457280 -XX:+ManagementServer -XX:MaxHeapSize=482344960 -XX:MaxNewSize=160759808 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=10485760 -XX:OldSize=20971520 -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops 
    Command line:  -Dzookeeper.log.dir=. -Dzookeeper.root.logger=INFO,CONSOLE -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false
    
  • -sysprops

    打印系统参数

    [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jinfo -sysprops 455
    Attaching to process ID 455, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.231-b11
    java.runtime.name = Java(TM) SE Runtime Environment
    java.vm.version = 25.231-b11
    sun.boot.library.path = /usr/local/software/jdk1.8.0_231/jre/lib/amd64
    java.vendor.url = http://java.oracle.com/
    java.vm.vendor = Oracle Corporation
    com.sun.management.jmxremote.local.only = false
    path.separator = :
    java.rmi.server.randomIDs = true
    file.encoding.pkg = sun.io
    java.vm.name = Java HotSpot(TM) 64-Bit Server VM
    sun.os.patch.level = unknown
    sun.java.launcher = SUN_STANDARD
    ...
    
  • 不带任何选项时,会同时打印虚拟机参数和系统参数

jmap

用于生成堆转储快照。如果不使用 jmap 命令,要想获取 Java 堆转储快照,还有一些比较“暴力”的手段:-XX: +HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:/Users/Administrator/Desktop/11 参数,可以让虚拟机在 OOM 异常出现之后自动生成 dump 文件,用于系统复盘环节

  • 用法详解

    Usage:
        jmap [option]   #连接到正在运行的进程
        jmap [option]  #连接到核心文件
        jmap [option] [server_id@] #连接到远程调试服务
    ###############################
    # option: 选项参数。
    # pid: 需要打印配置信息的进程ID。
    # executable: 产生核心dump的Java可执行文件。
    # core: 需要打印配置信息的核心文件。
    # server-id 可选的唯一id,如果相同的远程主机上运行了多台调试服务器,用此选项参数标识服务器。
    # remote server IP or hostname 远程调试服务器的IP地址或主机名。
    ###############################
    
    ################option###############
    # no option: 查看进程的内存映像信息,类似 Solaris pmap 命令。
    # heap: 显示Java堆详细信息
    # histo[:live]: 显示堆中对象的统计信息
    # clstats:打印类加载器信息
    # finalizerinfo: 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象
    # dump::生成堆转储快照
    # F: 当-dump没有响应时,使用-dump或者-histo参数. 在这个模式下,live子参数无效.
    # help:打印帮助信息
    # J:指定传递给运行jmap的JVM的参数
    ################option###############
    
  • 常用命令

    • -dump

      生成 Java 堆转储快照。格式为:-dump: format=b, file=

      [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jmap -dump:format=b,file=/home/zookeeper/a.bin 455
      Dumping heap to /home/zookeeper/a.bin ...
      Heap dump file created
      
    • -histo

      显示堆中对象统计信息,包括类、实例数量、合计容量(结合 more分页去查看)

      [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jmap -histo 455 | more
      
       num     #instances         #bytes  class name
      ----------------------------------------------
         1:          1033         867936  [B
         2:          6817         486552  [C
         3:          1947         220344  java.lang.Class
         4:          6786         162864  java.lang.String
         5:          3884         121400  [Ljava.lang.Object;
         6:           845         114288  [I
         
      # [B    byte
      # [C    char
      # [I    int
      
    • -heap

      显示Java堆详细信息

      [zookeeper@iZuf62iexj3ztw81eg1cnoZ ~]$ jmap -heap 455
      Attaching to process ID 455, please wait...
      Debugger attached successfully.
      Server compiler detected.
      JVM version is 25.231-b11
      
      using thread-local object allocation.
      Mark Sweep Compact GC
      
      Heap Configuration:
         MinHeapFreeRatio         = 40
         MaxHeapFreeRatio         = 70
         MaxHeapSize              = 482344960 (460.0MB)
         NewSize                  = 10485760 (10.0MB)
         MaxNewSize               = 160759808 (153.3125MB)
         OldSize                  = 20971520 (20.0MB)
         NewRatio                 = 2
         SurvivorRatio            = 8
         MetaspaceSize            = 21807104 (20.796875MB)
         CompressedClassSpaceSize = 1073741824 (1024.0MB)
         MaxMetaspaceSize         = 17592186044415 MB
         G1HeapRegionSize         = 0 (0.0MB)
      
      Heap Usage:
      New Generation (Eden + 1 Survivor Space):
         capacity = 9437184 (9.0MB)
         used     = 337192 (0.32157135009765625MB)
         free     = 9099992 (8.678428649902344MB)
         3.5730150010850696% used
      Eden Space:
         capacity = 8388608 (8.0MB)
         used     = 323424 (0.308441162109375MB)
         free     = 8065184 (7.691558837890625MB)
         3.8555145263671875% used
      From Space:
         capacity = 1048576 (1.0MB)
         used     = 13768 (0.01313018798828125MB)
         free     = 1034808 (0.9868698120117188MB)
         1.313018798828125% used
      To Space:
         capacity = 1048576 (1.0MB)
         used     = 0 (0.0MB)
         free     = 1048576 (1.0MB)
         0.0% used
      tenured generation:
         capacity = 20971520 (20.0MB)
         used     = 2583336 (2.4636611938476562MB)
         free     = 18388184 (17.536338806152344MB)
         12.318305969238281% used
      

jhat

Sun JDK 提供 jhat (JVM Heap Analysis Tool)命令常与 jmap 搭配使用,来分析 jmap 生成的堆 转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看

PS C:\Users\Administrator\Desktop\csv> jhat .\a.bin
Reading from .\a.bin...
Dump file created Thu Mar 19 17:07:49 CST 2020
Snapshot read, resolving...
Resolving 48429 objects...
Chasing references, expect 9 dots.........
Eliminating duplicate references.........
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

jstack

用于生成虚拟机当前时刻的线程快照(-般称为 threaddump 或者 javacore 文件)

  • 用法

    Usage:
        jstack [-l]  # 连接活动线程
        jstack -F [-m] [-l]  # 连接阻塞线程
        jstack [-m] [-l]   # 连接dump的文件
        jstack [-m] [-l] [server_id@] # 连接远程服务器
    
    Options:
      # 当进程挂起了,此时'jstack [-l] pid'是没有相应的,这时候可使用此参数来强制打印堆栈信息,强制jstack),一般情况不需要使用。
        -F  to force a thread dump. Use when jstack  does not respond (process is hung)
        # 打印java和native c/c++框架的所有栈信息。可以打印JVM的堆栈,以及Native的栈帧,一般应用排查不需要使用
        -m  to print both java and native frames (mixed mode)
        # 长列表. 打印关于锁的附加信息。例如属于java.util.concurrent的ownable synchronizers列表,会使得JVM停顿得长久得多
        -l  long listing. Prints additional information about locks
        # 打印帮助信息
        -h or -help to print this help message
    
  • 死锁测试

    private void deadLocked() {
            new Thread("线程1"){
                @Override
                public void run() {
                    synchronized (obj1) {
                        System.out.println(Thread.currentThread().getName() + "得到 obj1");
                        try {
                            Thread.sleep(3000L);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        synchronized (obj2) {
                            System.out.println(Thread.currentThread().getName() + "得到 obj2");
                        }
                    }
                }
            }.start();
    
            new Thread("线程2"){
                @Override
                public void run() {
                    synchronized (obj2) {
                        System.out.println(Thread.currentThread().getName() + "得到 obj2");
                        try {
                            Thread.sleep(3000L);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        synchronized (obj1) {
                            System.out.println(Thread.currentThread().getName() + "得到 obj1");
                        }
                    }
                }
            }.start();
        }
    
  • 使用jstack -l pid监控

    "线程2" #28 prio=5 os_prio=0 tid=0x000000001b96f800 nid=0x970 waiting for monitor entry [0x000000001d2be000]
       java.lang.Thread.State: BLOCKED (on object monitor)
            at cn.net.jvm.JvmApplication$2.run(JvmApplication.java:50)
            - waiting to lock <0x0000000082374a58> (a java.lang.Object)
            - locked <0x0000000082374a48> (a java.lang.Object)
    
       Locked ownable synchronizers:
            - None
    
    "线程1" #27 prio=5 os_prio=0 tid=0x000000001b96f000 nid=0x2990 waiting for monitor entry [0x000000001d1bf000]
       java.lang.Thread.State: BLOCKED (on object monitor)
            at cn.net.jvm.JvmApplication$1.run(JvmApplication.java:32)
            - waiting to lock <0x0000000082374a48> (a java.lang.Object)
            - locked <0x0000000082374a58> (a java.lang.Object)
    
       Locked ownable synchronizers:
            - None
            
    Found one Java-level deadlock:
    =============================
    "线程2":
      waiting to lock monitor 0x00000000035fcf68 (object 0x0000000082374a58, a java.lang.Object),
      which is held by "线程1"
    "线程1":
      waiting to lock monitor 0x00000000035fd388 (object 0x0000000082374a48, a java.lang.Object),
      which is held by "线程2"
    
    Java stack information for the threads listed above:
    ===================================================
    "线程2":
            at cn.net.jvm.JvmApplication$2.run(JvmApplication.java:50)
            - waiting to lock <0x0000000082374a58> (a java.lang.Object)
            - locked <0x0000000082374a48> (a java.lang.Object)
    "线程1":
            at cn.net.jvm.JvmApplication$1.run(JvmApplication.java:32)
            - waiting to lock <0x0000000082374a48> (a java.lang.Object)
            - locked <0x0000000082374a58> (a java.lang.Object)
    
    Found 1 deadlock.
    

jconsole

集成了内存和线程的可视化展示工具

  • 本地连接
local-jconsole.png
  • 远程连接

    jar包启动

    nohup java -Xms800m -Xmx800m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxNewSize=512m -Dcom.sun.management.jmxremote.port=9999 -Djava.rmi.server.hostname=139.224.101.91 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -jar jvm-0.0.1-SNAPSHOT.jar &
    

    连接

    remote-jconsole.png

VisualVM

集成命令行JDK工具和轻量级分析功能的可视化工具

visualvm.png

Minor/Major/FullGC

Minor

回收年轻代空间(包括 Eden 和 Survivor 区域)内存;分配率越高,越频繁执行 Minor GC

  • 触发条件:当Eden区满时

Major

清理老年代,通常至少经历过一次Minor GC

FullGC

清理整个堆空间(年轻代、老年代和永久代)

正常频率是大概一天一到两次

  • 触发条件

    • 调用 System.gc()

      此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()

    • 老年代空间不足

      老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组

    • 空间分配担保失败

      用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC

常见JVM问题

  • 线程问题

    死锁

  • 堆内存泄漏

    现象:出现OOM或者Full GC,heap使用率明显上升,经常达到Xmx

  • 堆外内存泄漏

    现象:heap使用率很低,但是出现了OOM或者Full GC(可以使用btrace定位)

你可能感兴趣的:(JVM)