JVM相关概念,GC,CMS

928 阅读13分钟

JVM的基本特性:

  • 基于栈(Stack-based)的虚拟机: 不同于Intel x86和ARM等比较流行的计算机处理器都是基于寄存器(register)架构,JVM是基于栈执行的
  • 符号引用(Symbolic reference): 除基本类型外的所有Java类型(类和接口)都是通过符号引用取得关联的,而非显式的基于内存地址的引用。
  • 垃圾回收机制: 类的实例通过用户代码进行显式创建,但却通过垃圾回收机制自动销毁。
  • 通过明确清晰基本类型确保平台无关性: 像C/C++等传统编程语言对于int类型数据在同平台上会有不同的字节长度。JVM却通过明确的定义基本类型的字节长度来维持代码的平台兼容性,从而做到平台无关。
  • 网络字节序(Network byte order): Java class文件的二进制表示使用的是基于网络的字节序(network byte order)。为了在使用小端(little endian)的Intel x86平台和在使用了大端(big endian)的RISC系列平台之间保持平台无关,必须要定义一个固定的字节序。JVM选择了网络传输协议中使用的网络字节序,即基于大端(big endian)的字节序。
Java类加载器的特点:
  • 层次结构:Java的类加载器按是父子关系的层次结构组织的。Boostrap类加载器处于层次结构的顶层,是所有类加载器的父类。
  • 代理模型(双亲委派):基于类加载器的层次组织结构,类加载器之间是可以进行代理的。当一个类需要被加载,会先去请求父加载器判断该类是否已经被加载。如果父类加器已加载了该类,那它就可以直接使用而无需再次加载。如果尚未加载,才需要当前类加载器来加载此类。
    • 作用 :防止重复加载同一个.class 通过委托去向上面问一问,加载过了,就不用再加载一遍,保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
  • 可见性限制:子类加载器可以从父类加载器中获取类,反之则不行。
  • 不能卸载: 类加载器可以载入类却不能卸载它。但是可以通过删除类加载器的方式卸载类。

java加载器:

  • Bootstrap加载器:Bootstrap加载器在运行JVM时创建,用于加载Java APIs,包括Object类。不像其他的类加载器由Java代码实现,Bootstrap加载器是由native代码实现的。
  • 扩展加载器(Extension class loader):扩展加载器用于加载除基本Java APIs以外扩展类。也用于加载各种安全扩展功能。
  • 系统加载器(System class loader):如果说Bootstrap和Extension加载器用于加载JVM运行时组件,那么系统加载器加载的则是应用程序相关的类。它会加载用户指定的CLASSPATH里的类。
  • 用户自定义加载器:这个是由用户的程序代码创建的类加载器。

类加载步骤:

  • 加载(Loading): 从文件中获取类并载入到JVM内存空间。

  • 验证(Verifying): 验证载入的类是否符合Java语言规范和JVM规范。在类加载流程的测试过程中,这一步是最为复杂且耗时最长的部分。大部分JVM TCK的测试用例都用于检测对于给定的错误的类文件是否能得到相应的验证错误信息。

  • 准备(Preparing): 根据内存需求准备相应的数据结构,并分别描述出类中定义的字段、方法以及实现的接口信息。

  • 解析(Resolving): 把类常量池中所有的符号引用转为直接引用。

  • 初始化(Initializing): 为类的变量初始化合适的值。执行静态初始化域,并为静态字段初始化相应的值。

    img

java 运行时数据结构

  • PC 寄存器(计数器):每个线程都会有一个PC(Program Counter)寄存器,并跟随线程的启动而创建。PC寄存器中存有将执行的JVM指令的地址。

  • JVM 栈(虚拟机栈):每个线程都有一个JVM栈,并跟随线程的启动而创建。其中存储的数据元素称为栈帧(Stack Frame)。JVM会每把栈桢压入JVM栈或从中弹出一个栈帧。如果有任何异常抛出,像printStackTrace()方法输出的栈跟踪信息的每一行表示一个栈帧。

    栈帧:在JVM中一旦有方法执行,JVM就会为之创建一个栈帧,并把其添加到当前线程的JVM栈中。当方法运行结束时,栈帧也会相应的从JVM栈中移除。栈帧中存放着对本地变量数组、操作数栈以及属于当前运行方法的运行时常量池的引用。本地变量数组和操作数栈的大小在编译时就已确定,所以在运行时方法的栈帧大小是固定的。

    • 本地变量数组:本地变量数组的索引从0开始计数,其位置存储着对方法所属类实例的引用。从索引位置1开始的保存的是传递给该方法的参数。其后存储的就是真正的方法的本地变量了。
    • 操作数栈:是方法的实际运行空间。每个方法变换操作数栈和本地变量数组,并把调用其它方法的结果从栈中弹或压入。在编译时,编译器就能计算出操作数栈所需的内存窨,因此操作数栈的大小在编译时也是确定的。
  • 本地方法栈:为非Java编写的本地程序定义的栈空间。也就是说它基本上是用于通过JNI(Java Native Interface)方式调用和执行的C/C++代码。根据具体情况,C栈或C++栈将会被创建。

  • 方法区:方法区是被所有线程共用的内存空间,在JVM启动时创建。它存储了运行时常量池、字段和方法信息、静态变量以及被JVM载入的所有类和接口的方法的字节码。不同的JVM提供者在实现方法区时会通常有不同的形式。在Oracle的Hotspot JVM里方法区被称为Permanent Area(永久区)或Permanent Generation(PermGen, 永久代)。JVM规范并对方法区的垃圾回收未做强制限定,因此对于JVM实现者来说,方法区的垃圾回收是可选操作。

  • 运行时常量池:属于方法区一部分,用于存放编译期生成的各种字面量和符号引用,一个存储了类文件格式中的常量池表的内存空间。这部分空间虽然存在于方法区内,但却在JVM操作中扮演着举足轻重的角色,因此JVM规范单独把这一部分拿出来描述。除了每个类或接口中定义的常量,它还包含了所有对方法和字段的引用。因此当需要一个方法或字段时,JVM通过运行时常量池中的信息从内存空间中来查找其相应的实际地址。

  • 数据堆:堆中存储着所有的类实例或对象,并且也是垃圾回收的目标场所。当涉及到JVM性能优化时,通常也会提及到数据堆空间的大小设置。JVM提供者可以决定划分堆空间或者不执行垃圾回收。

    img

对象

  1. 对象的内存布局

    在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)实例数据(Instance Data)对齐填充(Padding)

    • 对象头(Header) : 包括两个部分,第一个部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标识,线程持有的锁,偏向线程id,偏向时间戳等待。 第二部分是类型指针,即对象指向它的类的元数据指针,根据类型指针可以确定该对象属于哪一个类
    • 实例数据(Instance Data) : 程序中定义的类的属性字段的内容
    • 对齐填充(Padding) : 保证对象大小是某个字节的整数倍
  2. 对象的创建
    • 遇到new 指令时,检查指令的参数是否能在常量池中找到对应的符合引用(即检查需要创建的类是否有被编译), 并检查这个符合引用所代表的类是否有被加载,验证,解析,初始化,如果没用执行相应的类加载
    • 类加载完成后,为新对象分配一块内存即在堆的空闲内存中划分一块区域
    • 分配完内存空间后会初始化为0(不包括对象头),然后填充对象头,即把对象是哪个类的实例,如何才能找到元数据信息,对象的hash码,对象的GC分代年龄信息等存入对象头

GC

如何判断一个对象可以被回收

  1. 引用计数算法

    1.概念: 判断对象的引用数量,当引用数量为0 时可以作为垃圾被收集

    1. 优缺点
      1. 优点:执行效率高,程序执行受影响小
      2. 缺点:无法检测出循环引用的情况,可能导致内存泄漏
  2. 可达性分析算法 GC Root
    1. 概念: 通过判断对象的引用链是否可达来决定是否可用被回收, 从GC root 节点出发所走过的路径称为引用链, 当一个对象到GC root 没有没有任何引用链时则说明该对象不可用 2. 可用作为GC root 的对象(方法区,栈和本地方法区不被GC 所管理的对象可以作为GC Root 对象)

    • 通过System Class Loader或者Boot Class Loader加载的class对象
    • 活着的线程
    • java 方法的local 变量或参数
    • 方法区中的常量引用对象
    • 方法区中的静态属性引用对象

jvm 内存问题

  1. 内存泄漏(memory leak) : 本应该回收的对象不能被回收而停留在堆内存中,最终会导致 OOM

    1. 可能原因 :
      1. 连接未关闭 如数据库连接,缓存连接
      2. 线程池未正确使用,线程未正确关闭
      3. 单例模式持有外部对象的引用
      4. 集合
  2. 内存溢出 (out of memory) :程序在申请内存时,没有足够多的空间可供其使用

    1. 可能原因 :
      1. jvm 内存过小
      2. 程序不严谨,产生了过多过大的对象

jvm 调优参数

  1. Xms128m : 初始化堆内存大小为128M,不包括永久代的空间,是 -XX:InitailHeapSize=128m 的缩小
  2. Xmx2g : 最大堆内存大小 为 2G,不包括永久代的空间, 是 -XX:MaxHeapSize=2g 的缩小
  3. -XX:+HeapDumpOnOutOfMemoryError : jvm 发送内存溢出时自动生成堆内存快照
  4. -XX:HeapDumpPath= :设置堆内存快照生成的路径
  5. -XX:OnOutOfMemory="" : 可接收一串指令,当堆内存溢出时,执行指令
  6. -XX:PermSize= : 初始化永久代大小
  7. -XX:MaxPermSize= : 最大永久代大小

新生代垃圾回收

  1. 新生代特征

    1. 新生代对象朝生夕死,生命周期短,基本采用复制算法做垃圾回收
  2. 新生代划分

    1. 伊甸园区(Eden): 对象最开始在此分配到内存
    2. 幸存区(suivivor): 新生代垃圾回收时,存活对象符合条件(年龄,大小)会被移入幸存区
      1. from,to : 大小一致,from 和 to 的作用会来回转换

      上图演示GC过程,黄色表示死对象,绿色表示剩余空间,红色表示幸存对象

3. 调优参数

  1. -XX:NewSize : 初始化新生代大小
  2. -XX:MaxNewSize : 最大新生代大小
  3. -XX:NewRatio=3 : 老年代和新生代的大小比, 老年代/新生代 : 3/1 ,新生代为整个堆区的 1/4
  4. -XX:SuivivorRatio=8 : 伊甸园区和单个幸存区(from/to)的大小比 , 伊甸园区占新生代8/10,from和to区域占新生代 1/10

垃圾回收器评估标准

  1. 吞吐量 :吞吐量越高越好,吞吐量:应用程序线程占程序总用时比例
  2. 暂停时间 : stop the world 的时间越短越好。
  3. 高吞吐量和低暂停时间互相矛盾,高吞吐(减少GC 线程占时)-> 减少GC 线程数/ 减少GC次数; 低暂停-> 垃圾回收量有限 -> 增加GC次数 -> 增加GC线程总占有时间 -> 低吞吐

CMS 收集器

  1. 基本介绍
    1. 低暂停型收集器,大多数时间GC线程可以和应用线程并行
    2. 基于标记-清除算法,无压缩操作,容易产生碎片
    3. 针对老年代
  2. 执行过程
    1. 初始化标记:stop the world ,标记gc root 指向的对象为存活对象,标记年轻的中可达老年代的对象
    2. 并发标记 : gc root tracing ,标记gc root 指向的对象所指向的对象为存活对象
    3. 并发预清理
    4. 重标记:stop the world ,修正并发标记阶段应用线程改动了的对象
    5. 并发清理 : 清理无用引用,回收堆内存
    6. 并发处置:收尾工作
  3. 可能存在的问题
    1. 内存碎片化
    2. 浮动垃圾 : 并发标记阶段,产生的新垃圾,当前的GC 无法清理的垃圾,需要下次GC 再清理
  4. 常用命令
    1. -XX:+UseConcMarkSweepGC :指定使用CMS 收集器
    2. -XX:+UseCompactAtFullCollection : 开启内存碎片的压缩整合
    3. -XX:+CMSFullGcsBeforeCompact : 与*-XX:+UseCompactAtFullCollection* 一起使用,指定执行多少次不压缩gc 后,进行一次压缩整合,可缓解碎片化问题
    4. -XX:CMSInitiatingOccupancyFraction=75 : 老年代使用率达到75%时进行垃圾回收,需要预留空间,给浮动垃圾和新进入老年代的对象,所以该值不能太高
  5. 1,2,3 步或者2,3,4步的作用

GC 日志

  1. -XX:+PrintGC

    输出基本的gc 信息:gc类型,gc 前后堆内存的使用大小,gc耗时

  2. -XX:+PrintGCDetails

    输出详细的gc 信息:gc类型,所使用的gc器,gc前后堆内存的使用大小,堆内存的总大小,gc耗时

  3. -Xloggc

    指定gc 日志输出文件,默认设置了 -XX:+PrintGC-XX:+PrintGCTimeStamps

参考文章