jvm专题 之 垃圾回收&故障处理工具

文章目录

前言

java内存动态分配后,为了避免OOM(Out Of Memory)的错误,我们需要将垃圾回收

判断对象是否死亡

引用计数法

  • 当对象被引用时,引用计数器+1,引用失效时,-1。当计数器的值为0时,说明该对象不会被使用
  • 优点:判定效率高
  • 缺点:很难解决对象之间相互循环引用的问题

可达性分析

  • 有一个GC Roots的对象作为起始点,开始向下搜索。当一个对象到GC Roots没有任何引用链相连时,说明对象不可用
  • 可作为GC Roots的对象:
    • 虚拟机栈中引用的对象,如参数、局部变量、临时变量
    • 方法区中常量引入的对象,如字符串常量池的引用
    • 本地方法栈中引用的对象
    • java虚拟机内部的引用,如基本数据类型对象的class对象,常驻的异常对象
    • 所有被同步锁持有的对象

方法区数据的回收

  • 废弃的常量、常量池中的类、接口、方法、字段的符号引用
    • 判定方法:没有对象引用常量池中的常量,即可回收
    • 同类型的还有:常量池中的类、接口、方法、字段的符号引用
  • 不再使用的类型(需要满足3个条件)
    • 1、该类所有的实例都已经被回收
    • 2、加载该类的类加载器已经被回收
    • 3、该类对应的java.lang.Class对象没有在任何地方被引用(即无法在任何地方通过反射访问该类的方法)

引用

强引用

  • 代码中普遍存在的引用复制,是默认的引用类型。 如 Object obj = new Object();

  • 只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象 只有当引用变量被赋值为null时,垃圾回收期才会回收该对象

  • 使用场景:

    • 实现单例模式、缓存等需要长时间存活的对象
    • 在并发中,强引用也经常用于保持线程安全共享的对象。
  • 总结:

    • 1、强引用可以直接访问目标对象
    • 2、强引用所指向的对象在任何时候都不会被系统回收
    • 3、基于2,强引用是造成Java内存泄漏的主要原因之一
    • 4、单例模式、缓存等需要长时间存活的对象使用。
  • 代码示例

    java 复制代码
    Object obj  = new Object();  // 声明一个强引用
    obj = null;    // 销毁强引用

软引用

  • 描述一些还有用,但非必须的对象。

  • 在系统将要发生内存溢出异常前,才会把对象列进回收范围

  • 场景:被重建的对象,如缓存数据或图片,一旦内存不足,就会回收,从而释放内存

  • 代码示例

    java 复制代码
    // 创建软引用,方法一:
    Object obj  = new Object();    // 声明一个强引用
    SoftReference<Object> softReference = new SoftReference<>(obj);    // 声明软引用
    obj = null;    // 销毁强引用,只要有强引用在,软引用永远不会销毁
    
    // 创建软引用,方法二:
    SoftReference<Object> softReference = new SoftReference<>(new Object()); 
    System.gc();    // 系统垃圾回收

弱引用

  • 用来描述那些非必须对象,比软引用更弱一些

  • 只能生存到下一次垃圾收集发生为止(无论当前内存是否足够,都会回收掉)

  • 场景: 并发编程中,弱引用经常被用来实现对象监视器

    如监视一个对象的状态,当对象被回收是,该对象的状态也将被相应的清除

  • 代码示例:

    java 复制代码
    // 创建弱引用,方法一:
    Object obj  = new Object();    // 声明一个强引用
    WeakReference<User> weakReference = new WeakReference<>(obj);    // 声明弱引用
    obj = null;    // 销毁强引用,只要有强引用在,弱引用永远不会销毁
    
    // 创建弱引用,方法二:
    WeakReference<User> weakReference = new WeakReference<>(new Object()); 
    System.gc();    // 系统垃圾回收

虚引用

  • 又称幽灵引用/幻影引用 无法通过虚引用来取得一个实例对象。
  • 作用:在对象收集器回收时收到一个系统通知
  • 在对象被回收时执行必要的清理工作:如关闭数据库连接或释放资源
  • 虚引用实现高级对象的生命周期管理,
  • 虚引用+引用队列:在对象池或者线程池中追踪对象的生命周期,并在对象不再使用时及时清理,保证系统的可靠性和性能

垃圾收集算法

分代收集理论

  • 弱分代假说
    • 绝大多数都是朝生夕灭的
  • 强分代假说
    • 活的越久的就越不容易消亡
  • 跨代引用假说
    • 对象不是孤立的,会存在跨代引用;
    • 存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的

回收方法区

  • 废弃常量: 没有任何对象引用常量池中的常量
  • 无用的类:
    • 该类所有的实例都已经被回收
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 在大量使用反射、动态代理、CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出】

垃圾收集算法

标记清除

  • 原理:先标记处需要挥手的对象,标记完成后,统一回收掉所有被标记的对象

  • 缺点:

    1、执行效率不稳定;

    2、容易在内存空间产生大量碎片。
    碎片太多可能会导致后续需要分配较大对象时无法找到足够的连续的空间而提前触发一次垃圾收集动作

  • 收集器应用:CMS

标记复制

  • 目的:为了解决标记-清除算法面对大量回收对象时执行效率低的问题
  • 原理:
    • 1、将堆内存划分为大小相等的AB两块,每次使用其中一块A。
    • 2、当A的内存用完了,将还存活的的复制到B上,然后把A一次清理掉
  • 缺点:
    • 1、内存中多数对象是存活的,会产生大量的内存间复制的开销;
    • 2、空间浪费严重
  • 优化:
    • 1、新生代中98%的对象熬不过第一轮
    • 2、将空间分为Eden(80%)和两个survivor(10%)区:每次分配内存只使用Eden和其中一块Survivor。当垃圾收集时,将存活的对象一次性复制到另一块Survivor空间上,然后直接清理掉Eden和使用过的Survivor;
    • 3、当Survivor空间不够时,对象通过分配担保机制直接进入老年代。
  • 收集器应用:Serial、ParNew、Parallel Scavenge

标记整理

  • 原理
    • 1、标记出需要回收的对象
    • 2、将所有存活的对象都向内存空间的一端移动
    • 3、然后清理掉边界以外的内存
  • 缺点: 对于有大量对象存活区域的老年代,移动对象必须全程暂停用户应用程序
  • 优化: 暂时容忍碎片的存在,当忍无可忍影响了对象分配时,然后整理一次,获取规整的内存空间

经典垃圾收集器

Serial收集器:

  • 单线程工作的收集器。
  • 在进行垃圾收集时,必须暂停其他所有工作线程,直到收集完成

ParNew收集器:

  • 1、除了支持多线程并行收集之外,其他与Serial收集器基本相似;
  • 2、能与CMS收集器配合工作

Parallel Scavenge收集器:

  • 1、基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器
  • 2、目标是达到一个可控制的吞吐量

CMS收集器(Concurrency Mark Sweep )

  • 基于标记清除算法实现
  • 首次实现了让垃圾收集线程与用户线程(基本上)同时工作
  • 以获取最短回收停顿时间为目标的收集器
  • 运作过程:
    • 1、初始标记:
      • 很快 但是 需要Stop The World
      • 标记一下GC Roots能直接关联到的对象
    • 2、并发标记:
      • 耗时,无需Stop The World
      • 从GC Roots的直接关联对象开始遍历整个对象图的过程,
      • 可以与垃圾收集线程一起并发运行
    • 3、重新标记:
      • 需要Stop The World,初始标记 < 耗时 < 并发标记
      • 为了修正并发标记期间,因用户继续运作而导致标记产生变动的标记记录
    • 4、并发清除
      • 无需Stop The World
      • 清除掉标记阶段判断的已经死亡的对象
      • 可以与用户线程同时并发
    • 缺点:
      • 1、对处理器资源非常敏感: 并发阶段会降低总吞吐量
      • 2、无法处理浮动垃圾 因为没有stop the world,所以新产生的垃圾只能等下一次垃圾收集时再清理掉

G1(Garbage First)

开创了面向局部收集的设计思路 和 基于Region的内存布局形式

是一款主要面向服务端应用的垃圾收集器,且不需要其他新生代收集器的配合

  • 思路 优先回收内存中存放的垃圾数量最多,回收收益最大 的内存空间
  • 原理:
    • 1、把连续的java堆划分为多个大小相等的独立区域,叫Region;
    • 2、每个Region都可以根据需要,扮演新生代的Eden、Survivor或者老年代空间
      • 专门用来存储大对象的Humongous区域:大小超过Region一半的称为大对象
      • G1中大多数行为把Humongous Region作为老年代的一部分看待
      • 收集器能够对扮演不同角色的Region采用不同的策略去处理
      • 新生代和老年代是不固定的,是一些列不一定连续的动态集合
    • 3、建立可预测的停顿时间模型 支持指定在一个长为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒
    • 4、G1收集器跟踪各个Region里面的垃圾堆积的"价值"大小 将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍

      价值即回收所获得的空间大小及回收所需时间的经验值

    • 5、在后台维护一个优先级列表,根据用户设定允许的手机停顿时间,优先处理回收价值收益最大的Region 用户设定允许的收集停顿时间参数:-XX:MaxGCPauseMillis,默认是200ms
  • 运作过程: 通过原始快照(SATB)算法实现
    • 1、初始标记:有耗时很短的线程停顿,且是在借用Minor GC的时候同步完成
      • 标记一下GC Roots能直接关联到的对象,修改TAMS指针的值 TAMS(Top At Mark Start)指针:

        ①、并发回收时新分配的对象地址都必须要在这两个指针范围以上

        ②、G1收集器默认在这两个指针范围地址以上的对象是存活的

        ③、修改TAMS的值是保证下一阶段的用户可以正常在Region分配对象

    • 2、并发标记:采用的是原始快照(SATB)算法实现
      • 扫描整个堆里的对象图,找出要回收的对象 - 此阶段耗时较长,可与程序并发执行
      • 扫描完成重新处理SATB下在并发时有变动的对象
    • 3、最终标记:用户线程有短暂的暂停
      • 处理并发结束后仍遗留下来的少量的SATB记录
    • 4、筛选回收:
      • 更新Region统计的数据,对各个Region的回收价值和成本排序
      • 根据用户设定的停顿时间指定回收计划
      • 将决定回收Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间
  • 优点
    • 由用户指定期望的停顿时间
    • 设置不同的期望停顿时间,可以使G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡

内存分配与回收策略

对象优先分在Eden分配

  • 当Eden区没有足够空间进行分配时,虚拟机发起一次Minor GC
  • 大对象直接进入老年代
    • 大于 -XX:PretenureSizeThreshold值的对象可以直接进入老年代
    • 好处:避免在Eden和Survivor区来回复制,产生大量的内存复制操作
  • 长期存活的对象将进入老年代
    • 每熬过一次Minor GC,年龄+1。当年龄到15时,晋升到老年代中
    • 老年代的阙值:-XX:MaxTenuringThreshold

动态对象年龄判定:

  • 1、-XX:MaxTenuringThreshold来设置年龄
  • 2、在Survivor中相同年龄所有对象大小的总和达到一半,大于等于该年龄的直接进入老年代

空间分配担保(有风险)

  • 先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
  • 检查老年代最大可用的连续空间是否大于历晋升到老年代对象的平均大小
  • 冒险开关:-XX:HandlePromotionFailure

基础故障处理工具

jsp

  • 列出正在运行的虚拟机进程,并显示虚拟机执行主类名称:jps -l

jstat

  • jvm统计信息监视工具:
  • 监视堆状态的垃圾收集:jstat -gc 线程id 300 20
    • 查询线程id为777 的垃圾收集,每隔300ms查询一次,一共查询20次
    • 如果300 和 20省略,则说明只查询一次
  • 监视类加载、卸载数量:jstat -class 线程id
  • 监视垃圾收集,输出主要关注已经使用空间的百分比:jstat -gcutil 线程id
  • 输出即时编译编辑过的方法、耗时等信息:jstat -compiler 线程id

jinfo

  • 实时查看和调整虚拟机各项参数:jinfo -flag 需要查询的参数 线程id

jmap:

  • 生成一个正在运行的Eclipse的堆转储快照文件:jamp -dump:format=b,file=eclipse.bin 线程id

jhat

  • jvm堆转储快照分析工具:jhat

jstack

  • java堆栈跟踪工具(用于生成虚拟机当前时刻的线程快照):jstack
  • jstack -F 线程id:正常输入的请求不被相应时,强制输出线程的堆栈
  • jstack -l 线程id:除堆栈外,显示关于锁的附加信息
相关推荐
1.01^100010 小时前
[5-01-01].第04节:初识字节码文件 - 字节码文件作用
jvm
找不到、了13 小时前
JVM核心知识整理《1》
jvm
L.EscaRC15 小时前
面向 Spring Boot 的 JVM 深度解析
jvm·spring boot·后端
学到头秃的suhian1 天前
JVM-类加载机制
java·jvm
NEFU AB-IN2 天前
Prompt Gen Desktop 管理和迭代你的 Prompt!
java·jvm·prompt
唐古乌梁海2 天前
【Java】JVM 内存区域划分
java·开发语言·jvm
众俗2 天前
JVM整理
jvm
echoyu.2 天前
java源代码、字节码、jvm、jit、aot的关系
java·开发语言·jvm·八股
代码栈上的思考2 天前
JVM中内存管理的策略
java·jvm
thginWalker3 天前
深入浅出 Java 虚拟机之进阶部分
jvm