JVM

112 阅读11分钟

1.内存模型


  • 程序计数器:是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
    • 字节码解释器通过改变程序计数器来依次读取指令,进行流程控制。
    • 上下文切换时恢复现场。
  • JVM虚拟机栈:实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
  • 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
  • 堆:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。年轻代(Eden,S0,S1)+ 老年代 + 永久代(Hotspot 1.7对方法区的一种实现,1.8用元空间直接内存来实现)
  • 方法区:与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
  • 直接内存:本地内存,并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。jdk1.8元空间存储类的元信息, 而静态变量和常量池等并入堆中。相当于1.7的永久代数据被分到了堆和元空间中。


2.垃圾回收

2.0.对象的创建

  • Step1类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  • Step2:分配内存:指针碰撞+空闲列表;CAS+失败重试/TLAB解决并发问题。

  • Step3:初始化零值:

    内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

    Step4:设置对象头

    初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

    Step5:执行 init 方法

    在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

2.1.堆区域划分

  • 年轻代:Eden,From,To;new对象优先在eden区分配空间,Eden空间满,触发一次MinorGC后,存活对象复制到To区,From,To区角色互换,存活年龄+1;
    • 跃升老年代条件:
      • 存活年龄>-XX:MaxTenuringThreshold(默认值和垃圾回收器有关,CMS默认6,其余默认16)
      • 大对象直接进入老年代
      • 动态年龄判断:按年龄从小到大计算,取Min(累加超过Survivor一半的年龄,XX:MaxTenuringThreshold)作为跃迁年龄。
    • 触发MinorGC条件:Eden空间满
  • 老年代
    • 触发FullGC条件
      • System.gc()方法建议JVM进行FullGC(不推荐)
      • 老年代空间不足
      • 通过Minor GC后进入老年代的对象平均大小大于老年代的可用内存
  • 字符串常量
  • PS.堆的空间配置经验
    • Java整个堆大小设置,Xmx 和 Xms设置为FullGC后老年代存活对象的3-4倍年轻代Xmn的设置为老年代存活对象的1-1.5倍。
      老年代的内存大小设置为老年代存活对象的2-3倍。永久代 PermSize和MaxPermSize设置为老年代存活对象的1.2-1.5倍。
      
      ===
      1、Sun官方建议年轻代的大小为整个堆的3/8左右, 所以按照上述设置的方式,基本符合Sun的建议。 
      
      2、堆大小=年轻代大小+年老代大小, 即xmx=xmn+老年代大小 。 Permsize不影响堆大小。
      

2.2.如何标记一个对象死亡

  • 程序计数器:优势(简单高效);劣势(互相引用问题)
  • 可达性分析:当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
    • GCRoot:虚拟机栈,本地方法栈中引用的对象,方法区中的常量,静态属性引用的对象
    • 强、软、弱、虚引用

2.2.垃圾回收算法

  • 复制算法:适合用于年轻代,存活对象少。劣势:需要额外的冗余空间。
  • 标记清除:适用于老年代,容易产生内存碎片。
  • 标记-整理:适用于老年代。

2.3.垃圾收集器

  • Seria收集器:
    • 特点:它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
    • 算法:新生代采用复制算法,老年代采用标记-整理算法。
    • 优势:没有线程交互,简单而高效(与其他收集器的单线程相比)。
    • 劣势:中断工作线程带来的服务抖动。
    • 场景:Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
  • ParNew 收集器:
    • 特点:ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
    • 算法:新生代采用复制算法,老年代采用标记-整理算法。
  • CMS(Concurrent Mark Sweep)收集器:一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
    • 初始标记(STW): 这一阶段主要是进行可达性分析,标记GC root直接关联的对象。注意是直接关联
    • 并发标记: 这一阶段是进行GC root tracing阶段,和用户线程并发执行,此时在第一阶段被暂停的用户线程在该阶段重新运行。在这阶段中,从上一阶段被标记的对象出发,标记所有可达的对象。可以理解为标记间接关联的对象。
    • 并发预清理: 这一步所做的工作还是标记。CMS是以获取最短暂停时间为目的的GC,而在第四步重标记需要STW,因此重标记的工作尽可能多的在并发阶段完成来减少STW的时间。
    • 重标记(STW): 这一阶段暂停所有用户线程,重新扫描堆中的对象(包括新生代和老年代),进行可达性分析,标记活着的对象。注意这个阶段是多线程的。
    • 并发清理: 用户线程被重新激活,同时清理那些无效的对象。
    • 重置: CMS清除内部状态,为下次回收做准备。 
  • G1收集器:G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
    • 初始标记
    • 并发标记
    • 最终标记
    • 筛选回收

3.JVM常用参数配置及调优

3.1.命令行工具

  • jps (JVM Process Status): 类似 UNIX 的 ps 命令。用户查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;
  • jstat( JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据;
  • jinfo (Configuration Info for Java) : Configuration Info forJava,显示虚拟机配置信息;
  • jmap (Memory Map for Java) :生成堆转储快照;
  • jhat (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;
  • jstack (Stack Trace for Java):生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
  • 参考文档

4.类的加载过程

4.1.Clase文件结构

ClassFile {
    u4             magic; //Class 文件的标志
    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的访问标记
    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//Class 文件的字段属性
    field_info     fields[fields_count];//一个类会可以有个字段
    u2             methods_count;//Class 文件的方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}

4.2.类生命周期


  • 加载:类加载过程的第一步,主要完成下面3件事情:
  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
    • 双亲委托:
      • 启动类加载器:BoostrapClassLoader->标准扩展类加载器:ExtClassLoader->应用程序类加载器: AppClassLoader
      • 过程:
      1.  当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。 每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。 
      2.  当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader. 
      3.  当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。 
  • 验证:
    • 文件格式验证+元数据验证+字节码验证+符号引用验证
  • 准备:
  1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  2. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等)
  • 解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
  • 初始化:初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 <clinit> ()方法的过程。
  • 卸载:
    • 条件:
      • 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
      • 该类没有在其他任何地方被引用
      • 该类的类加载器的实例已被GC