知识总结 jvm

 

类加载过程

加载->验证->准备->解析->初始化

  • 加载:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象。
  • 验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
  • 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
  • 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用。
  • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
  • 使用
  • 卸载

类加载器

启动(Bootstrap)

  • c++ 实现,所以在java代码中get体现为null。负责将 Java_Home/lib下面的类库加载到内存中(比如rt.jar)。

    扩展(Extension)

  • 是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。

    系统(App)

  • 是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般称为系统(System)加载器。

    自定义(Custom)

  • 开发者自己定义 +自定义只需继承ClassLoader类,重写findClass方法。其中可使用defineClass方法将class文件的二进制byte数组转化为class类 +自定义类加载器加载自加密的class,可防止反编译,防止篡改
  • parent是如何指定的,打破双亲委派
    1. 用super(parent)指定
    2. 双亲委派的打破
      1. 如何打破:重写loadClass()
      2. 何时打破过?
        1. JDK1.2之前,自定义ClassLoader都必须重写loadClass()
        2. ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定
        3. 热启动,热部署
          1. osgi tomcat 都有自己的模块指定classloader(可以加载同一类库的不同版本)

双亲委派模型

  • 出于安全考虑。首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

Object o = new Obejct() 的半初始化问题

    0 new #2 <java/lang/Object>
    3 dup
    4 invokespecial #1 <java/lang/Object.<init>>
    7 astore_1
    8 return
  • 发生指令重排序,astore_1 如果在 invokespecial 前执行,就会产生引用到空内存的未初始化对象。

内存屏障

inter x86 架构 cpu 硬件级实现

  • Lfence 前后都是 load 的屏障
  • Sfence 前后都是 save 的屏障
  • Mfence 前后任意 的屏障

jvm 内存屏障

  • LoadStore 前 load 后 store
  • LoadLoad
  • StoreLoad
  • StoreStore

volatile 各层次实现

字节码层面(class文件)

  • 仅仅增加一行 ACC_VOLATILE 具体实现由jvm决定

jvm 实现

  • StoreStore volatile写 StoreLoad
  • volatile读 | LoadLoad | LoadStore

    硬件

  • X86 : lock cmpxchg 实现

对象

对象的创建过程

  • 类的加载:加载,验证,准备,解析,初始化 + 对象创建:分配内存,成员变量赋默认值,调用构造方法(成员变量顺序赋初始值,调用构造方法)

    对象的内存布局

    普通对象

    1. 对象头 mark word 8字节
    2. 对象头 klass pointer指针 -XX:-UseCompressedOops 为4字节,否则为8字节
    3. 实例数据 引用类型:-XX:+UseCompressedOops 为4字节 不开启为8字节(Oops Ordinary Object Pointers 普通对象的指针)
    4. pending 对齐,8的倍数

      数组对象

  • 对象头中多了一个 数组长度 4字节

对象头的具体

  • gc分代最大只能15 因为只有4位

使用JavaAgent测试Object的大小

观察虚拟机配置

java -XX:+PrintCommandLineFlags -version

实验

  1. 新建项目ObjectSize (1.8)

  2. 创建文件ObjectSizeAgent

    package com.mashibing.jvm.agent;
       
    import java.lang.instrument.Instrumentation;
       
    public class ObjectSizeAgent {
        private static Instrumentation inst;
       
        public static void premain(String agentArgs, Instrumentation _inst) {
            inst = _inst;
        }
       
        public static long sizeOf(Object o) {
            return inst.getObjectSize(o);
        }
    }
    
  3. src目录下创建META-INF/MANIFEST.MF

    Manifest-Version: 1.0
    Created-By: mashibing.com
    Premain-Class: com.mashibing.jvm.agent.ObjectSizeAgent
    

    注意Premain-Class这行必须是新的一行(回车 + 换行),确认idea不能有任何错误提示

  4. 打包jar文件

  5. 在需要使用该Agent Jar的项目中引入该Jar包

  6. 运行时需要该Agent Jar的类,加入参数:
    -javaagent:C:\work\ijprojects\ObjectSize\out\artifacts\ObjectSize_jar\ObjectSize.jar
    
  7. 如何使用该类:

       import com.jvm.agent.ObjectSizeAgent;
          
       public class T03_SizeOfAnObject {
           public static void main(String[] args) {
               System.out.println(ObjectSizeAgent.sizeOf(new Object()));
               System.out.println(ObjectSizeAgent.sizeOf(new int[] {}));
               System.out.println(ObjectSizeAgent.sizeOf(new P()));
           }
          
           private static class P {
                               //8 _markword
                               //4 _oop指针
               int id;         //4
               String name;    //4
               int age;        //4
           
               byte b1;        //1
               byte b2;        //1
          
               Object o;       //4
               byte b3;        //1
          
           }
       }
    

5: 运行时数据区Runtime Data Area

PC 程序计数器

  • PC 寄存器,也叫程序计数器。JVM支持多个线程同时运行,每个线程都有自己的程序计数器。倘若当前执行的是 JVM 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native 方法,则PC寄存器中为空。

    native method stacks

  • java调用的JNI,c和c++写的 这部分主要与虚拟机用到的 Native 方法相关

    jvm stacks

  • 每个线程对应一个栈,每个方法对应一个栈帧
  • 栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,

    栈帧 Frame

    1. Local variable Table 局部变量表
    2. Operand Stack 操作数栈 对于long的处理(store and load),多数虚拟机的实现都是原子的 jls 17.7,没必要加volatile
    3. Dynamic Linking 指向常量池
    4. return address 存放返回值 a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方

      heap 堆

  • 堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。

    Direct memory

  • 直接内存,由操作系统管理,用于访问内核空间内存
  • JVM可以直接访问的内核空间的内存 (OS 管理的内存)
  • NIO, 提高效率,实现zero copy

    method area 装常量池,存储每个class的结构

  • 主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
    1. Perm Space (<1.8) 永久代 字符串常量位于PermSpace FGC不会清理 大小启动的时候指定,不能变
    2. Meta Space (>=1.8) 字符串常量位于堆 会触发FGC清理 不设定的话,最大就是物理内存

常用指令

  • store
  • load
  • pop
  • mul
  • sub
  • invoke
    • InvokeStatic
    • InvokeVirtual
    • InvokeInterface
    • InovkeSpecial, 可以直接定位,不需要多态的方法: private 方法,构造方法
    • InvokeDynamic,JVM最难的指令,lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令

GC

1. 什么是垃圾

  • 没有任何引用指向的一个对象或者多个对象(循环引用)

    2. 如何定位垃圾

  • 引用计数 reference count

    对象内会记录有多少个引用指向,引用数量为0时成为垃圾,但对象之间循环引用时,比如a引用b,b引用c,c引用a时,无法回收

  • 根可达算法 Root Searching

没有与根有引用连接的对象为垃圾

根有哪些?

JVM Stack,native method stack ,run-time constant pool ,static references in method area,Clazz

比如线程栈变量,静态变量,常量池,JNI变量

3.常见的垃圾回收算法

  • 标记清除(mark sweep) - 先扫描一边,将垃圾标记出来,第二次扫描把标记为垃圾的对象清除,不移动对象位置

    位置不连续 产生碎片 需要两遍扫描,效率偏低

    算法相对简单,存活对象比较多的情况下率高

  • 拷贝算法 (copying) - 扫描对象,将有用对象复制到另一块内存,剩下的垃圾整个清除

    没有碎片,浪费空间,移动复制对象,需要调整对象引用

    适用于存活对象较少的情况,只扫描一遍,效率提高,没有碎片。

  • 标记压缩(mark compact) - 把有用对象移动到一起,垃圾直接清理

    没有碎片,效率偏低(两遍扫描,指针需要调整)

    不会产生碎片,方便对象分配,不会产生内存减半

4.JVM内存分代模型(用于分代垃圾回收算法)

  1. 部分垃圾回收器使用的模型

    除Epsilon ZGC Shenandoah之外的GC都是使用逻辑分代模型

    G1是逻辑分代,物理不分代

    除此之外不仅逻辑分代,而且物理分代

  2. 新生代 + 老年代 + 永久代(1.7)Perm Generation/ 元数据区(1.8) Metaspace

    1. 永久代 元数据区 - Class
    2. 永久代必须指定大小限制 ,元数据可以设置,也可以不设置,无上限(受限于物理内存)
    3. 字符串常量 1.7 - 永久代,1.8 - 堆
    4. MethodArea逻辑概念 - 永久代、元数据

  3. 新生代 = Eden + 2个suvivor区

    对象首先会被放在栈Stack上,栈上放不下会被放在eden,垃圾回收之后进入survivor,再次垃圾回收会进入另一个survivor,多次gc之后会进入老年代

    1. YGC回收之后,大多数的对象会被回收,活着的进入s0

    2. 再次YGC,活着的对象eden + s0 -> s1

    3. 再次YGC,eden + s1 -> s0 ,
    4. 年龄足够 -> 老年代

      采用Parallel Scavenge 垃圾回收算法,默认次数是15,存放次数只有4位,最大只能是15

      采用CMS默认是6

      采用G1默认是15

      采用 -XX:MaxTenuringThreshold 指定次数

    5. s区装不下 -> 老年代
  4. 老年代

    1. 顽固分子
    2. 老年代满了FGC Full GC
  5. GC Tuning (Generation)

    1. 尽量减少FGC
    2. MinorGC = YGC :年轻代空间耗尽时触发 ,-Xmn
    3. MajorGC = FGC : 在老年代无法继续分配空间时触发,新生代老年代同时进行回收
  6. 栈上分配,什么情况对象会分配到栈上?

    1. 线程私有小对象

    2. 无逃逸: 只在一段代码中使用,其他代码没有用到
    3. 支持标量替换: 一个对象中只有普通属性,用对象中的普通属性代替对象
  7. 线程本地分配TLAB (Thread Local Allocation Buffer) 调优时建议不调整

    创建对象,分配eden内存时,会有多个线程争用内存地址,如果采用同步效率会降低,于是用TLAB来解决这个问题。每个线程分配1%的空间,这个空间是线程独有的

  8. 对象分配过程图

    对象首先会尝试放在栈Stack上,栈上放得下就放在栈上,如果栈上放不下,再看对象大不大,大的话直接放在Old,小的话放TLAB,TLAB是在Eden区内,进行gc,放入s1区,看年龄够不够,够的话放Old区,不够放S2区

5.常见的垃圾回收器

  1. JDK诞生 Serial追随 提高效率,诞生了PS,为了配合CMS,诞生了PN,CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多,因此目前任何一个JDK版本默认是CMS 并发垃圾回收是因为无法忍受STW

  2. STW gc时会停止工作进程的方式,除了CMS不是STW类型的,其他方式都是STW的

  3. Serial 年轻代 串行回收

    单线程 ,在安全点停止工作进程,gc后再恢复工作进程

  4. PS 年轻代 并行回收

    parallel scavenge 默认的垃圾回收策略,清理线程是多个

  5. ParNew 年轻代 配合CMS的并行回收

    相对于parallel scavenge加了一些增强,以适用于CMS

  6. SerialOld 用于老年代,也是单线程,已淘汰

  7. Parallel Old 默认的

  8. ConcurrentMarkSweep 老年代垃圾回收器 并发的, 垃圾回收和应用程序同时运行,降低STW的时间(200ms) CMS问题比较多,所以现在没有一个版本默认是CMS,只能手工指定 CMS既然是MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,使用SerialOld 进行老年代回收

CMS清理垃圾的4个阶段:

  1. 初始标记: STW,标记根节点
  2. 并发标记: 标记垃圾,与工作线程并发进行
  3. 重新标记: STW,标记并发标记过程中新产生的垃圾
  4. 并发清理: 清理垃圾,与工作进程并发进行,该过程会产生浮动垃圾
    • 算法:三色标记 + Incremental Update

G1(10ms)

  1. G1中的GC收集
    • G1保留了YGC并加上了一种全新的MIXGC用于收集老年代。G1中没有Full GC,G1中的Full GC是采用serial old Full GC。 + 算法:三色标记 + SATB

ZGC (1ms) PK C++

  • 算法:ColoredPointers颜色指针 + LoadBarrier读屏障

垃圾收集器跟内存大小的关系:

  1. Serial 几十兆
  2. PS 上百兆 - 几个G
  3. CMS - 20G
  4. G1 - 上百G
  5. ZGC - 4T - 16T(JDK13)

6.三色标记算法

会用白色,灰色、黑色来标记对象。

  • 白色:未被标记的对象
  • 灰色:自身被标记,成员变量未被标记
  • 黑色:自身和成员变量均已标记完成

但是会存在漏标问题:

  1. 在remark过程中,黑色指向白色,如果不对黑色重新扫描,则会漏标,会把白色对象当做没有新引用指向从而被回收掉
  2. 并发标记过程中,删除了所有从灰色到白色的引用,会产生漏标,此时白色对象应该被回收

漏标的两个充要条件

  • 有至少一个黑色对象在自己被标记之后指向了这个白色对象
  • 所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用

为了避免漏标,有以下两种方法:

  1. incremental update – 增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性,CMS采用该方式。会多导致扫描一次。
  2. SATB snapshot at the begining – 关注引用的删除,当灰色对象对白色对象的引用删除时,要把这个引用推到GC的堆栈中,保证白色对象还能被GC扫描到,相当于把白色标记为灰色。G1采用该方式。会导致浮动垃圾到下一次才回收。

为什么G1用SATB?

灰色->白色 引用消失时,如果没有黑色指向白色,引用会被push到堆栈

下次扫描时拿到这个引用,由于有RSet的存在,不需要扫描整个堆去查找指向白色的引用,效率比较高。