JVM-2020-4-20

类编辑解释执行过程

  • 编译:将.java文件编译为.class文件(字节码文件)其中详细过程如下:

源代码文件.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> JVM字节码文件.class

  • 加载:加载即将.class类文件加载到内存中

  • 连接

  • 解释执行

Class 文件

cafe babe 0000 0034 0027 0a00 0600 1809
0019 001a 0800 1b0a 001c 001d 0700 1e07
001f 0100 046e 616d 6501 0012 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 0100
0361 6765 0100 0149 0100 0761 6464 7265
......
  • cafe babe 表示魔数

  • 0000 minor_version

  • 0034 major_version

  • 0000 0034 对应10进制的52,代表jdk8中的一个版本

  • 0027 对应10进制27,代表常量池中有27个常量

ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

ClassFile 字节码文件

魔数与class文件版本
常量池
访问标志
类索引、父类索引、接口索引
字段表集合
方法表集合
属性表集合

类加载机制

装载(Load)

  • 查找和导入class文件

    • 通过一个类的全限定名获取定义此类的二进制字节流

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

    • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

连接(Link)

验证(Verify)

  • 保证类加载的正确性

    • 文件格式验证

    • 元数据验证

    • 字节码验证

    • 符号引用验证

准备(Prepare)

  • 为类的静态变量分配内存,并将其初始化设置为默认值

解析(Resolve)

  • 把类中的符号引用转换为直接引用

初始化 (Initialize)

  • 对类的静态变量,静态代码块执行初始化操作

类加载机制图解

JVM-2020-4-20_第1张图片
image-20200420104614700.png

类加载器ClassLoader

在装载(Load)阶段,其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载Class文件的。

分类

  • BootstrapClassLoader 负责加载$JAVA_HOME中 jre/lib/rt.jar(rt=run time) 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。

  • ExtensionClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。

  • AppClassLoader 负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。

  • CustomClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。

图解

JVM-2020-4-20_第2张图片
image-20200420105817120.png

加载原则

检查某个类是否已经加载:顺序是自底向上,从Custom ClassLoader到BootStrapClassLoader逐层检查,只要某个Classloader已加载,就视为已加载此类,保证此类只所有ClassLoader加载一次。加载的顺序:加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

双亲委派机制

定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。 优势:Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。
破坏:可以继承ClassLoader类,然后重写其中的loadClass方法,其他方式大家可以自己了解拓展一下。

运行时数据区(Run-Time Data Areas)

在装载阶段的第(2),(3)步可以发现有运行时数据,堆,方法区等名词 (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 (3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口 就是类文件被类装载器装载进来之后,类中的内容(比如变量,常量,方法,对象等这些数据得要有个去处,也就是要存储起来,存储的位置肯定是在JVM中有对应的空间)

图解

JVM-2020-4-20_第3张图片
image-20200420111403658.png

Method Area (方法区)

  • 方法区是各个线程共享的内存区域,在虚拟机启动时创建。

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

  • 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。

  • 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

  • 方法区在JDK 8 中就是MetaSpace,JDK6、7中就是PermSpace
  • Run-Time Constant Pool

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

此时回看装载阶段的第2步:(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

Heap(堆)

  • Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。

  • Java对象实例以及数组都在堆上分配。

此时回看装载阶段的第3步:(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

内存模型

一块是非堆区,一块是堆区。堆区分为两大块,一个是Old区,一个是Young区。Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区。Eden:S0:S1=8:1:1S0和S1一样大,也可以叫From和To。

  • 图解

    JVM-2020-4-20_第4张图片
    image-20200420124214234.png

对象所在区域

一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。

比如有对象A,B,C等创建在Eden区,但是Eden区的内存空间肯定有限,比如有100M,假如已经使用了100M或者达到一个设定的临界值,这时候就需要对Eden内存空间进行清理,即垃圾收集(Garbage Collect),这样的GC我们称之为Minor GC,Minor GC指得是Young区的GC。

经过GC之后,有些对象就会被清理掉,有些对象可能还存活着,对于存活着的对象需要将其复制到Survivor区,然后再清空Eden区中的这些对象。

Survivor 区详解

由图解可以看出,Survivor区分为两块S0和S1,也可以叫做From和To 。 在同一个时间点上,S0和S1只能有一个区有数据,另外一个是空的。

接着上面的GC来说,比如一开始只有Eden区和From中有对象,To 中是空的。 此时进行一次GC操作,From区中对象的年龄就会+1,我们知道Eden区中所有存活的对象会被复制到To 区, From区中还能存活的对象会有两个去处。 若对象年龄达到之前设置好的年龄阈值,此时对象会被移动到Old区,如果Eden区和From区没有达到阈值的 对象会被复制到To 区。此时Eden区和From区已经被清空(被GC的对象肯定没了,没有被GC的对象都有了各自的去处)。 这时候From和To 交换角色,之前的From变成了To ,之前的To 变成了From。 也就是说无论如何都要保证名为To 的Survivor区域是空的。 Minor GC会一直重复这样的过程,直到To 区被填满,然后会将所有对象复制到老年代中。

为什么需要Survivor ?只有Eden区不行吗?

如果没有Survivor,Eden区每进行一次Minor GC,并且没有年龄限制的话,存活的对象就会被送到老年代。 这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。 老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。 执行时间长有什么坏处?频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。 可能你会说,那就对老年代的空间进行增加或者较少咯。 假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生FullGC,执行所需要的时间更长。 假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。 所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么需要两个Survivor区?

最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程: 刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。 永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。

新生代中Eden:S1:S2为什么是8:1:1?

新生代中的可用内存:复制算法用来担保的内存为9:1 可用内存中Eden:S1区为8:1 即新生代中Eden:S1:S2 = 8:1:1

Old 区详解

从上面的分析可以看出,一般Old区都是年龄比较大的对象,或者相对超过了某个阈值的对象。 在Old区也会有GC的操作,Old区的GC我们称作为Major GC。一般发生MajorGC就相当于Full GC。

MinorGC 新生代

MajorGC 老年代

FullGC 新生代+老年代+方法区

”对象的一辈子“

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有 一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。 于是我就去了年老代那边,老年代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

JVM-2020-4-20_第5张图片
image-20200420125627347.png

Java Virtual Machine Stacks (Java 虚拟机栈)

  • 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。

  • 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。 调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

JVM-2020-4-20_第6张图片
image-20200420113021834.png
  • 栈帧: 每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。每个栈帧包括如下内容:

    • 局部变量表(Local Variables)

      方法中定义的局部变量以及方法的参数存放在这张表中,局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。

    • 操作数栈(Operand Stack)

      以压栈和出栈的方式存储操作数的

    • 运行时常量池的引用(A reference to the run-time constant pool)

      每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
      动态链接(Dynamic Linking):是一个将符号引用解析为直接引用的过程。java虚拟机执行字节码时,遇到一个操作码,操作码第一次使用一个指向另一类的符号引用,则虚拟机就必须解析这个符号引用

    • 方法返回地址(Return Address)

      当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。

  • 栈帧图解

    JVM-2020-4-20_第7张图片
    image-20200420114647881.png
  • 示例
class Person{
    private String name="jon";
    private int age;
    private final double salary=100;
    private static String address;
    private final static String hobby="Programming";
    public void say(){
        System.out.println("person say...");
    }
    public static int calc(int op1,int op2){
        op1=3;
        int result=op1+op2;
        return result;
    }
    public static void order(){
    }
    public static void main(String[] args){
        calc(1,2);
        order();
    }
}
----------------------------------------------
Compiled from "Person.java"
class Person {
...
public static int calc(int, int);
    Code:
        0: iconst_3 //将int类型常量3压入[操作数栈]
        1: istore_0 //将int类型值存入[局部变量0]
        2: iload_0 //从[局部变量0]中装载int类型值入栈
        3: iload_1 //从[局部变量1]中装载int类型值入栈
        4: iadd //将栈顶元素弹出栈,执行int类型的加法,结果入栈
        5: istore_2 //将栈顶int类型值保存到[局部变量2]中
        6: iload_2 //从[局部变量2]中装载int类型值入栈
        7: ireturn //从方法中返回int类型的数据
...
}
JVM-2020-4-20_第8张图片
image-20200420120225009.png
  • 如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的对象。


    JVM-2020-4-20_第9张图片
    image-20200420120900746.png
  • 方法区指向堆

    • 方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。
private static Object obj=new Object();
JVM-2020-4-20_第10张图片
image-20200420121403438.png
  • 堆指向方法区

    • 方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?
    JVM-2020-4-20_第11张图片
    image-20200420121719321.png
  • Java 对象内存布局
JVM-2020-4-20_第12张图片
image-20200420123158116.png

The PC Register (程序计数器)

我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。 假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。

程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。 如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,则这个计数器为空。

Native Method Stacks(本地方法栈)

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。

垃圾回收(Garbage Collect)

如何确定一个对象是垃圾?

引用计数法:

  • 对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。

弊端: 无法解决相互依赖问题;如果AB相互持有引用,导致永远不能被回收。

可达性分析

  • 通过GC Root的对象,开始向下寻找,看某个对象是否可达。GCRoots对象作为起始点,从这个节点开始向下搜索;搜索过的路径称为GC引用链。当一个对象到GCRoot没有引用链则证明此对象不可用。不可达对象会经过两次标记过程,如果对象没覆盖finalize()或已执行finalize() 则将对象设置F-Queue队列等待垃圾收集器回收。

能作为GC Root

  • 类加载器

  • Thread

  • 虚拟机栈的本地变量表

  • static成员

  • 常量引用

  • 本地方法栈的变量

  • 虚拟机栈中的引用对象

  • 方法区中类静态属性引用对象

  • 方法区中常量引用对象

  • 本地方法栈中JNI引用对象

枚举根节点(GCRoots可达性分析)

可达性分析对执行时间敏感体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行。整个分析过程中系统看起来像被冻结在某个执行点上,不可以出现分析过程中对象关系还在不断变化,该点不满足的话分析结果无法保证。这点导致GC进行时必须停顿所有Java线程。Sun称为(stop the world)。在oopMap的协助下,hotsopt可快速且准确的完成GCRoot枚举。hotSpot在安全点做了文章。

  • 安全点:
*   safepoint让GC发生时让所有线程都跑到安全点停下来:
  • 抢先式中断 :
*   不需要线程执行代码主动配合,在gc发生时,首先把所有线程全部中断。发现有线程中断地方不在安全点则让线程跑到安全点。目前没有虚拟机采用此方式暂停线程响应gc条件。
  • 主动中断式 :
*   不对线程直接操作,设置一个标识,给个线程执行时,主动轮询标志。发现中断标识为真时,自己中断。

对象引用

  • 强引用:

    • Object O = new Object();
  • 软引用:

    • 描述一些还有用但是非必须对象,对于软引用关联的对象在系统将要发生内存溢出时这些对象将被列入回收范围之中,进行二次回收;二次回收之后内存不足抛出异常。
  • 弱引用:

    • 用来描述非必须对象,弱引用关联的对象只能活到下一次垃圾回收之前。
  • 虚引用:

    • 最弱的引用关系,也称幽灵引用。为一个对象设置虚引用的目的为了对象回收时收到一个系统通知。

废弃对象判断

  • 该对象所有的实例都已经被回收,java堆中不存在该类的任何实例

  • 加载该类的classLoader已被回收

  • 该类的java.class.Class对象没有在任何地方引用,无法在任何地方通过反射访问该类

垃圾收集算法

标记-清除法(mark-sweep)

  • 首先表示所有需要回收对象,在标记完成后统一回收所有标记对象。

图解:

JVM-2020-4-20_第13张图片
image-20200420154013404.png
  • 缺点:

    • 标记和清除两个过程都比较耗时,效率不高

    • 会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法(Copying)

  • 将内存分为一块较大的Eden空间和2块较小的survior空间,每次使用Eden空间和一块survior空间。当回收时,将Eden和Survior存活对象Copy到另一块survior空间,清理Eden和Survior空间。Eden和Survior比例为:8:1.只有10%浪费,若另一块survior空间不够用时,需要老年代进行内存担保(新生带的对象一般都是朝生夕死)。

图解:

JVM-2020-4-20_第14张图片
image-20200420160047197.png
  • 缺点

    • 需要老年代进行担保。

标记-整理(Mark-Compact)

  • 先标记存活对象,将存活对象移向一端,然后清理掉端界以外的内存。

图解:

JVM-2020-4-20_第15张图片
image-20200420160713024.png

分代收集算法(Generational Collection)

  • 将Java堆分为新生带和老年代,在新生代中每次垃圾回收都发现有大批对象死去,选用复制法。老年代对象存活率较高,没有额外内存担保就使用标记-清除或标记整理

    • Young区

      • 复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
    • Old区

      • 标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)

垃圾收集器

JVM-2020-4-20_第16张图片
image-20200420161027197.png

Serial 收集器

  • 最基础、最老。基于单线程的收集器,采用复制算法。所谓单线程是指在进行垃圾回收时必须停掉其他所有工作线程。

    • 优点:简单高效,对于限定单个cpu,Serial收集器没有线程交互的开销,专心做垃圾回收。适用于运行在client模式下,默认新生代收集器。

    • 缺点:收集过程需要暂停所有线程

    • 算法:复制算法

    • 适用范围:新生代

    • 应用:Client模式下的默认新生代收集器

JVM-2020-4-20_第17张图片
image-20200420161315857.png

ParNew收集器

  • ParNew收集器就是Serial收集器多线程版本,采用复制算法 ;除使用多线程进行垃圾收集之外,其余行为包括serial可用参数、收集算法、stop the world对象分配规则,回收策略-都与 Serial一样。ParNew在单cpu环境中绝对不会有比Serial更好的效果。甚至优于存在线程交互的开销该收集器在通过超线程技术实现的两个cpu环境中都不能百分之百保证可以超越Serial。当然随着使用cpu的数据增加。他对gc时系统资源的有效利用还是有好处的。它默认开启与cpu数量相等的的线程数。

    • 优点:运行在server模式下,虚拟机中首选的新生代收集器,除了Serial外,目前只有ParNew能和CMS收集器工作。CMS只作为老年代收集器。只选择和新生代的Serial或ParNew一起工作。

    • 缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。

    • 算法:复制算法

    • 适用范围:新生代

    • 应用:运行在Server模式下的虚拟机中首选的新生代收集器

JVM-2020-4-20_第18张图片
image-20200420161457300.png

Parallel Scavenge收集器(paraller:并行 scavenge:清理)

  • 新生代收集器,使用复制算法收集,又是并行的多线程收集器。Parallel Scavenge 收集器目的是为了达到可控吞吐量。

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。

若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。

-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间, -XX:GCTimeRatio直接设置吞吐量的大小。

Serial Old收集器

  • Serial old 是 Serial 收集器的老年代版本,同样单线程。使用标记-整理算法。主要给client 模式下虚拟机用。如果在server模式下,在jdk1.5及之前与Parallel Scavenge 搭配使用。作为CMS收集器后背预案。
JVM-2020-4-20_第19张图片
image-20200420161754848.png

Parallel Old收集器

  • 是Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理算法。“吞吐优先” 和Parallel Scavenge 组合。

CMS收集器(concurrent mark sweep)

  • 是一种以获取最短回收停顿时间为目标的收集器。CMS收集器基于标记清除算法实现,它的运作过程如下:

    • 初始标记:

      • 标识一下GCRoots能直接关联到的对象,速度很快
    • 并发标记:

      • 进行GCRoots tracing过程
    • 重新标记:

      • 修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段停顿时间会比初始标记稍长但远比并发标记短。

      • 修改并发标记因用户程序变动的内容 Stop The World

    • 并发清除:

      • 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。从总体来讲,CMS收集器的内存回收过程与用户线程并行执行。拥有低停顿特点。
  • 缺点:

    • CMS收集器对cpu资源非常敏感,默认启动线程:(cpu数量+3)/4,CMS在垃圾回收时占用cpu资源较大,在cpu缺乏或者单cpu时不推荐使用。

    • CMS收集器无法处理浮动垃圾,可能出现 “concurrent mode failure” 失败而导致另一次 full gc发生。由于CMS在并发清理阶段用户线程还在运行着,伴随程序运行自然产生垃圾这部分垃圾出现在标记过程之后,CMS无法在当次收集器中处理掉他们。只好留下待下一次full gc时清理。这部分垃圾称为“浮动垃圾”。 也是由于垃圾收集阶段用户线程还在运行,那也就需要预留足够的内存空间给用户线程使用。因此,CMS收集器不能像其他收集器那样等到老年代几乎被填满了在进行收集。需要预留一部分空间提供并发收集时程序还运作使用。jdk1.6中cms启动阈值已提升至92%。要是CMS运行期间预留的内存无法满足程序需要就会出现concurrent 莫得filure失败,这时虚拟机将启动后备预案:临时启动serial old 收集器来进行老年代的垃圾收集。这样停顿时间很长。

    • CMS是基于:标记-清除算法实现。会产生大量的垃圾碎片。将会导致在大对象分配内存时无法找到合适的内存空间,不得提前触发一次full gc。为了解决这个问题 CMS一个-xx:usecmscompactatfullcollection参数用于在cms收集器顶不住要进行full gc时,开启内存碎片合并整理。这个过程导致停顿时间变长。虚拟机还提供了一个参数,--xx:cmsfullgcbeforecompaction用于设置执行多次不压缩的full gc后,跟着来一次带压缩的。

  • 优点:并发收集、低停顿

JVM-2020-4-20_第20张图片
image-20200420162352523.png

G1 收集器

并行与并发
分代收集(仍然保留了分代的概念)
空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
  • G1将内存分为多个大小相等的独立区域,虽然还保留新生代和老年代概念,新生代和老年代不在进行物理隔离,他们都是一部分Region的集合。G1之所以能建立可以预测的停顿模型是因为他可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面垃圾堆积的价值大小在后台维护一个优先列表。每次根据允许的收集时间,优先回收最大Region。这种方式保证了G1在有限时间内可以获取尽可以高的收集率。面向服务端垃圾收集器与其他收集器相比G1具有如下特点:

    • 并行与并发:

      • G1能充分利用cpu多核环境下优势,使用多个cpu来缩短“stop the world” 停顿时间。部分其他收集器原本需要停顿java线程执行gc动作。G1收集器仍可以通过并发方式让java线程继续执行。
    • 分代收集:

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

      • 与CMS不同,G1基于 标记-整理算法实现的收集器,意味着G1运行期间不会产生内存空间碎片。收集后提供规整的可用内存。利于程序运行分配大对象时不会因为无法找到内存空间提起触发gc。
    • 可预测的停顿:

      • 能让使用者明确指定在 M毫秒内消耗在垃圾收集上的时间不得超过N毫秒。
  • G1运作步骤:

    • 初始标记(Initial Marking):

      • 标记GCRoots能直接关联的对象,并修改 next top at mark start的值;让下一阶段用户程序并发运行时能正确可用Region中创建对象。需要停顿线程但是耗时很短。
    • 并发标记(Concurrent Marking):

      • 从GCRoots开始对对堆中对象进行可达性分析,找出存活对象,耗时长可以和用户程序并发执行。
    • 最终标记(Final Marking):

      • 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程。
    • 筛选回收(Live Data Counting and Evacuation): 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划。

JVM-2020-4-20_第21张图片
image-20200420164305802.png

JDK 7开始使用,JDK 8非常成熟,JDK 9默认的垃圾收集器,适用于新老生代。

判断是否需要使用G1收集器?

  • 50%以上的堆被存活对象占用

  • 对象分配和晋升的速度变化非常大

  • 垃圾回收时间比较长

理解吞吐量和停顿时间

  • 停顿时间->垃圾收集器 进行 垃圾回收终端应用执行响应的时间

  • 吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任 务。

如何选择合适的垃圾收集器

  • 优先调整堆的大小让服务器自己来选择

  • 如果内存小于100M,使用串行收集器

  • 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选

  • 如果允许停顿时间超过1秒,选择并行或JVM自己选

  • 如果响应时间最重要,并且不能超过1秒,使用并发收集器

如何开启需要的垃圾收集器

  • 串行 -XX:+UseSerialGC -XX:+UseSerialOldGC

  • 并行(吞吐量优先): -XX:+UseParallelGC -XX:+UseParallelOldGC

  • 并发收集器(响应时间优先) -XX:+UseConcMarkSweepGC -XX:+UseG1GC

实战

参数篇

JVM标准参数

  • -version

  • -help

  • -server

  • -cp

-X参数

非标准参数,在JDK各个版本中可能会变动

  • -Xint 解释执行

  • -Xcomp 第一次使用就编译成本地代码

  • -Xmixed 混合模式,JVM自己来决定

-XX参数

非标准化参数,相对不稳定,主要用于JVM调优和Debug

a.Boolean类型
    格式:-XX:[+-] +或-表示启用或者禁用name属性
    比如:-XX:+UseConcMarkSweepGC 表示启用CMS类型的垃圾回收器
         -XX:+UseG1GC 表示启用G1类型的垃圾回收器
b.非Boolean类型
    格式:-XX=表示name属性的值是value
    比如:-XX:MaxGCPauseMillis=500

其他参数

-Xms1000等价于-XX:InitialHeapSize=1000
-Xmx1000等价于-XX:MaxHeapSize=1000
-Xss100等价于-XX:ThreadStackSize=100

查看JVM所有参数

java -XX:+PrintFlagsFinal -version > flags.txt

"="表示默认值

":="表示被用户或JVM修改后的值要想查看某个进程具体参数的值,可以使用jinfo

常用参数

见:深入理解Java虚拟机

命令篇

  • jps 查看Java进程

  • jinfo 实时查看和调整JVM配置参数

    • 查看:jinfo -flag name PID 查看某个java进程的name属性的值

    • 修改:参数只有被标记为manageable的flags可以被实时修改

  • jstat

    • 查看虚拟机性能统计信息

    • jstat -class PID 1000 10 查看某个java进程的类装载信息,每 1000毫秒输出一次,共输出10次

    • jstat -gc PID 1000 10 查看垃圾收集信息

  • jstack

    • jstack -PID 查看线程堆栈信息

    • 排查死锁

  • jmap

    • 生成堆转储快照

    • jmap -heap PID 打印出堆内存相关信息

    • jmap -dump:format=b,file=heap.hprof PID dump出堆内存相关信息

    • -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof 内存溢出自动dump文件

调优

JVM-2020-4-20_第22张图片
image-20200420173936555.png

场景分析

JVM-2020-4-20_第23张图片
image-20200420175219376.png

常见问题思考

  • 内存泄漏与内存溢出的区别

    • 内存泄漏:对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。

    • 内存溢出:内存泄漏到一定的程度就会导致内存溢出,但是内存溢出也有可能是大对象导致的。

  • young gc会有stw吗? 不管什么 GC,都会有 stop-the-world,只是发生时间的长短

  • major gc和full gc的区别

    • major gc指的是老年代的gc

    • full gc等于young+old+metaspace的gc

  • G1与CMS的区别是什么?

    • CMS 用于老年代的回收,而 G1 用于新生代和老年代的回收。

    • G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生。

  • 什么是直接内存 直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。

  • 不可达的对象一定要被回收吗? 即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

  • 方法区中的无用类回收

    • 方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

      • 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :

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

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

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

  • 不同的引用

    • JDK1.2以后,Java对引用进行了扩充:强引用、软引用、弱引用和虚引用

工具

jConsole

  • JConsole工具是JDK自带的可视化监控工具。查看java应用程序的运行概况、监控堆信息、永久区使用情况、类加载情况等。

jvisualvm

  • 可以监控本地的java进程的CPU,类,线程

  • 监控远端Java进程(配置略)

Arthas

  • Arthas 是Alibaba开源的Java诊断工具,采用命令行交互模式,是排查jvm相关问题的利器

  • github :https://github.com/alibaba/arthas

MAT

  • Java堆分析器,用于查找内存泄漏

  • Heap Dump,称为堆转储文件,是Java进程在某个时间内的快照

  • 下载地址:https://www.eclipse.org/mat/downloads.php

Dump

  • 手动:jmap -dump:format=b,file=heap.hprof 44808

  • 自动:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof

GC日志分析工具

  • http://gceasy.io

  • GCViewer

visualgc

  • 下载链接:https://visualvm.github.io/pluginscenters.html

你可能感兴趣的:(JVM-2020-4-20)