首页 > 其他分享 >JVM从零到进阶

JVM从零到进阶

时间:2024-05-06 11:22:45浏览次数:23  
标签:进阶 对象 回收 零到 GC 垃圾 JVM 线程 加载

JVM进阶

字节码

​ 字节码为编译后的class文件,比如java、scala这些语言都是可编译成字节码的,字节码借助jvm就可以在任何平台运行,可以理解成跨平台的实现

一、运行时数据区

​ 在程序运行时,由jvm提供的几块内存区域,分别为以下几个区域:

  • 本地方法栈:执行native关键字的方法栈
  • java方法栈:执行类中方法的栈,每执行一个方法叫压栈、执行完就会出栈
  • 程序计数器:线程上下切换时,记录当前执行线程,以及下次要执行的线程地址
  • 堆:存储jvm中所产生的所有的对象
  • 方法区:包含了类中的所有信息,比如静态变量、成员变量、类信息、常量池等等
    • 执行引擎
      • 解释器:执行内存中每个字节码class文件中的指令
      • 垃圾回收器:处理jvm在内存中产生的对象
      • JIT编译器:将热点的字节码文件翻译成机器指令,且会缓存此指令

  • -Xms: ms(memory start), 指定堆的初始化内存大小,等价于-- -XX:initialHeapSize

  • -Xmx:mx(memory max),指定堆的最大内存大小,等价于-XX:MaxHeapSize

  • 一般会根据-Xms和-Xmx设置为一样,这样JVM就不需要再GC之后去修改内存大小了,提高了效率,默认情况下,初始化内存大小 = 物理内存大小 / 64, 最大内存大小 = 物理内存大小 / 4

  • 新生代为刚刚产生的对象、老年代为经过很多次GC还未被清除的对象

  • 通过-XX:NewRatio参数来配置新生代和老年代的比例分配,默认为2,表示新生代占用1,老年代占用2,也就是新生代占堆区总大小的1/3

  • 新生代

    在默认情况下(Eden区: S0区: S1区)的比例关系为(8:1:1), 也就是Eden区占新生代大小的8 / 10, 可以通过-XX: SurvivorRatio调整具体大小比例

    • Eden区:新对象进来都会先放到此区域(除非对象的大小都超过了Eden区的最大大小,那么就只能进入老年代)
    • Surviror0: 也称为fr- - om区
    • Surviror0: 也称为to区,都是用来存放MinorGC(YGC)收集后的对象

GC介绍

  • Young GC / Minor GC: 负责对新生代进行垃圾回收
  • Old GC / Major GC: 负责对老年代进行垃圾回收,目前只有CMS垃圾收集器会单独对老年代进行垃圾收集, 其他垃圾收集器基本都是整堆回收的时候对老年代进行垃圾回收
  • Full GC: 整堆回收,也会对方法区进行垃圾回收

可达性分析法

​ 所谓的GC ROOTS就是对象被引用的一个过程中的线路,比如A对象引用B对象,在GC ROOTS中的展示就是A -> B 以树形结构展示, 那它具体是包含了哪些引用呢?

  • 线程中虚拟机栈中正在执行的方法中的方法参数,局部变量所对应的对象引用
  • 线程中本地方法栈中正在执行的方法中的方法参数、局部变量所对应的对象引用
  • 方法区中保存的类信息中的静态变量属性所对应的对象引用
  • 方法区中保存的类信息中的常量池属性所对应的对象引用

什> 么时候触发垃圾回收?

​ 在Eden区的大小被对象放满了之后,就会触发MinorGC去收集未被使用的对象,如果还在使用的话,就从Eden区移到S0区、当第二次MinorGC的时候,如果S0区的对象还是没有被回收就会继续转移到S1区,并标记一下被GC了2次,当这样反复的被GC了16次之后,就会被放入到老年代!, 大对象如果Eden区放不下就会直接放在老年代中!

程序计数器

  • 是物理寄存器的抽象实现
  • 用来记录待执行的下一条指令地址
  • 它是程序控制流的指示器,循环、if else、异常处理、线程恢复等都需要依赖它来完成
  • 解释器工作时就是通过它来获取下一条需要执行的字节码指令的
  • 它是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域

虚拟机栈(Java栈 || Java方法栈)

  • 每一个Java方法都属于一个栈帧
    • 栈帧有哪些参数呢?
      • 局部变量表(很多个Slot)
      • 操作数栈(执行字节码过程中用于计算使用)
      • 方法返回地址
      • 动态链接
      • 附加信息
  • 方法的栈帧会根据调用链路依次压栈
  • 虚拟机栈是线程私有的
  • 方法调用层数过多,可能出现StackOverFlowError
  • 可以通过-Xss来设置虚拟机栈大小

二、类加载子系统

加载class文件步骤:比如一个编译后的class文件:Helloword.class会经过如下几个步骤:

  • 加载:将字节码文件加载到内存中
  • 链接:链接分为三个步骤
    • 验证:验证待加载的class文件是否正确,比如验证文件格式
    • 准备:为static变量分配内存并赋值
    • 解析:将符号引用解析为直接引用(符号引用:class的名称, 直接引用: class最终的内存地址)
  • 初始化:将数据初始化到对象中

类加载器的分类

  • 引导类加载器(BootStrapClassLoader)

    • BootStrapClassLoader: 加载的是jre/lib下面的jar包中的类

    • ExtClassLoader: 加载的是jre/lib/ext下面的jar包中的类

  • 自定义类加载器(继承实现ClassLoader类)

    • AppClassLoader: 加载的是自定义的类的路径的类(也就是你指定什么类 -> classpath, 就加载什么类)
    • WebAppClassLoader

双亲委派机制

双亲委派作用:避免类的重复加载、防止核心api被篡改

从AppClassLoader -> ExtClassLoader -> BootStrapClassLoader

描述:

​ 从AppClassLoader.loadClass()加载一个类的时候,它首先不是拿自己本身的ClassLoader去加载,而是使用ExtClassLoader去加载,当ExtClassLoader加载不了的时候,就再次向上找BootStrapClassLoader去加载此类,如果BootStrapClassLoader都加载不了这个类,那么最终还是由AppClassLoader 加载此类。当类.class.getClassLoader() == null就表示此类是被BootStrapClassLoader而加载的类

Tomcat为什么要自定义类加载器?

  • 一个tomcat可以跑多个应用,而多个引用可能会出现相同path + class的类出现,那么在加载的时候就可能在加载完A应用后,就放弃了B应用class的加载。
  • Tomcat就针对这种极端情况,就针对不同的应用使用继承自定义的方式(WebAppClassLoader)去加载,也就是每个应用使用自己的类加载器去加载自己应用的类,这样就不会有冲突情况,两个应用存在相同path + class也不会过滤掉不加载的情况。
  • 在JVM中判断一个类是不是被加载的逻辑是: 类名 + 对应的类加载器实例

三、GC算法

3.1、标记清除算法

​ 标记清除算法是常用的垃圾回收算法,针对某块内存空间,比如新生代、老年代,如果可用内存不够了,就会暂停JVM虚拟机(stop the world),暂停用户线程的执行,然后执行算法进行垃圾回收。

具体使用步骤:

​ 1、标记阶段:从GC ROOT开始遍历,找到可回收的对象,并对可回收的对象进行记录。

​ 2、清除阶段:堆内存空间进行线性遍历,如果发现对象头(MARK WORD)中被记录删除了,那么就删除回收它

缺点:

​ 1、出现内存碎片 (也就是整个内存空间,有的被标记了可回收,有的不可回收)

​ 2、效率不高 (因为需要遍历整个内存空间)

3.2、复制算法

复制算法思路:将内存划分为两块,一块属于对象存储区域、另一块属于空闲区域,在进行垃圾回收时,将可达性对象复制到另一个没有被使用的内存块中,然后再清除当前内存块中的所有可回收的对象,每次GC都是按照此方法区执行。(适用于新生代GC)

优点:

​ 1、没有标记和清除阶段,通过GC ROOTS找到可达对象,直接复制,不需要修改对象头,效率高

​ 2、不会出现内存碎片

缺点:

​ 1、占用内存空间较多, 因为要空出一块空余内存来复制删除使用

​ 2、对象复制后,对象存放地址发生变化,需要额外的时间修改栈帧中记录的引用地址

​ 3、如果可达性对象较多,垃圾对象比较少,那么复制算法效率就会比较低,所以垃圾对象多的情况下,复制算法比较适合,因为它是整个区域区域的复制

3.3、标记整理算法

​ 标记整理算法分为三阶段提交:

  • 第一阶段:从GC ROOTS找到并标记可达对象
  • 第二阶段:将所有存活对象移动到内存的另一端(新的一个内存区域)
  • 第三阶段:清理边界外所有的空间

总结GC算法

GC算法对比:

从速度/开销对比 标记-清除 标记-整理 复制
速度 中等 最慢 最快
空间开销 少(有碎片,也就是内存区域不连续) 少(无碎片) 最多
移动对象

3.4、分代收集算法

​ 不同对象的存活时长不一致,就针对新生代、老年代采用不同的GC算法进行回收

​ 默认几乎所有的垃圾回收器都是采用分代收集算法进行垃圾回收的

  • 新生代中对象存活的时间比较短,那么就可以利用复制算法,它适用于垃圾对象比较多的情况下去进行回收,但是会产生一定的碎片
    老年代中存活时间比较长,所以不适用复制算法,可以使用标记清除或标记整理算法, 个人建议采用CMS垃圾回收算法,因为相对以下两个算法,标记清除算法比标记整理更为高效点,但是标记整理更为完美点(很慢)

  • CMS垃圾回收器采用的就是标记-清除算法

  • Serial Old垃圾回收器采用的就是标记-整理算法

3.5、GC参数设置

-XX:+PrintCommandLineFlags 查看使用的垃圾收集器

-XX:+UseSerialGC 指定使用Serial GC和Serial Old GC

-XX:+UseParNewGC 指定新时代使用ParNew GC

-XX:+UseConcMarkSweepGC 指定老年代使用CMS GC

-XX:+UseParallelGC 指定新生代使用Parallel GC

-XX:+UseParallelOldGC 指定老年代使用Parallel Old GC

GC详情图

详解CMS垃圾回收器

CMS整个垃圾收集过程更长了,但是STW的时间变短了,而且在垃圾回收过程中大部分时间用户线程都还在执行,所以用户不会出现短暂的延迟等,但是吞吐量变低了(单位时间内执行的用户线程更少了)

CMS分为几个阶段:

  • 初始标记阶段:

    • STW 暂停所有工作线程
    • 标记GC ROOTS能直接可达的对象
    • 一旦标记完,就恢复工作线程继续执行
  • 并发标记阶段:

    • 从初始阶段标记出的对象,开始遍历整个老年代,标记出所有的可达对象
    • 耗时比较长
    • 不需要STW,用户线程与垃圾收集线程一起执行
    • 三色标记
  • 重新标记阶段:

    • 上个阶段标记的对象,可能有误差,需要进行修正

    • 需要STW,但是时间也不是很长

    • 增量更新

    • 根据三色标记,重新标记对象

    • 比如在finally中将已定义为垃圾对象变成了可达性对象,重复利用,就需要重新标记

  • 并发清除阶段:

    • 删除垃圾对象
    • 由于不需要移动对象,这个阶段可以和用户线程一起执行,不需要STW

CMS总结

如果在并发标记、并发清楚的过程汇总,由于用户线程一直在执行,而工作线程也在同时工作,就可能产生,用户线程正在执行的时候,而工作线程也在回收垃圾对象,那么有新对象进入了老年代,但是空间也不够,那么就会导致“concurrent mode failure”,此时就会利用Serial Old来做一次垃圾收集,就会做一次全局的STW, 在并发清除的过程中,可能产生新的垃圾,这些垃圾统称为”浮动垃圾“,只能等到下一次GC时来处理

由于采用的是标记-清除算法,所以会产生内存碎片,可以通过参数-XX:+UseCMSCompactAtFullCollection可以让JVM在执行完标记-清除后再做一次整理,也可以通过-XX:CMSFullGCsBeforeCompaction来指定多少次GC后来做处理,默认是0,表示每次GC后都会整理。

四、杂七杂八信息

对象创建过程

  • 类加载器检查
  • 分配内存(指针碰撞、空闲列表等方式)
  • 初始化“零值”, 基本类型是默认值0, 引用类型是null
  • 设置对象头(类实例、元数据信息、对象的哈希码、GC分代年龄)
  • 执行init,此方法是字节码自动生成的,是一个实例构造器
  • 对象栈上分配(对象创建后分配到堆的哪个位置,就需要经历逃逸分析最终决定)

对象栈上分配流程

  • 对象创建后会被分配到新生代 young gc中进行管理
  • 如果young gc也就是eden区放不下,就尝试放在survivor区
  • 如果survivor区也放不下,则放入到old区
  • 大对象直接放入old区(大对象的定义即超过新生代区域的50%以上)

为什么JVM放弃永久代而选择元空间

  • 内存限制:JVM加载的class总数,方法的大小很难确定,因此不好制定其大小
  • 降低了OOM:使用的是元空间存放在本地内存(上限较大)中的方式来替换永久代,这样可以降低OOM发生的可能性
  • 提升GC的性能:永久代是通过Full GC进行垃圾回收的,也就是和老年代同时实现垃圾回收,替换元空间简化了Full GC的过程,可以在不进行暂停的情况下并发的垃圾进行回收,提升了GC的性能
  • JRockit没有永久代:Oracle合并Hotspot和JRockit,而JRockit没有永久代

对象由什么组成?

  • 对象头(包含锁状态markword、GC分带年龄、线程持有锁、哈希码等等)
  • 实例数据
  • 对其填充

一个对象(一般16byte)组成,在排除很多字段的情况下

类加载的理解?

​ 类加载器是JAVA中的一种加载形式,在加载字节码到内存中时会使用双亲委派机制,使用递归的形式不断向尝试去加载父类的加载器,如果父类加载器为null,则调用本地方法,交由启动类加载器加载,所以说ExtClassLoader(加载jre/ext/lib/*.jar)的父类加载器为BootStrapClassLoader(jre/ext/lib/rt.jar)

JDK调优命令

  • jps 列出当前机器正在运行的进程

  • jinfo 查看或修改虚拟机参数

    • 栗子:jinfo -flags
      • -flag <name>:显示指定名称的 JVM 标志(flag)的值。
      • -flags:显示正在运行的 Java 进程的所有 JVM 标志的名称和值。
      • -sysprops:显示正在运行的 Java 进程的系统属性(System Properties)的名称和值。
  • jstat 监视虚拟机各种运行状态信息

    • 栗子:jstat [option] pid [interval [count]]
  • -class:显示类装载相关的统计信息,如装载数量、卸载数量、总装载类数等。

  • -gc:显示垃圾回收相关的统计信息,包括新生代和老年代的情况,如垃圾回收时间、吞吐量、堆大小等。

  • -gccapacity:显示垃圾回收堆的容量统计信息,包括新生代和老年代的容量、已使用空间、垃圾回收器标记等。

  • -gcutil:显示垃圾回收相关的统计信息,包括新生代和老年代的使用率、垃圾回收时间等。

  • -compiler:显示即时编译器相关的统计信息,如编译任务数量、编译耗时等。


  • jstack 生成虚拟机当前时刻线程快照
    • 栗子:jstack [option] pid
      • -l:除了堆栈信息外,显示关于锁的附加信息,包括每个线程等待的锁及锁的拥有者等信息。
      • -F:当 jstack 无法连接到 Java 进程时,强制输出线程堆栈信息。通常在进程卡死或无响应时使用。
      • -m:输出混合模式,显示 Java 和本地(C/C++)堆栈信息。
      • -h--help:显示帮助信息,列出 jstack 的所有选项。

  • jmap 生成堆转储快照
    • 栗子:jmap [option] pid
      • -heap:显示 Java 进程的堆内存使用情况,包括堆大小、已使用内存、垃圾回收器信息等。
      • -histo[:live]:显示 Java 进程中各个类的实例数量和占用内存大小,如果指定了 live 参数,则只统计活动对象。
      • -dump:[live,]format=b,file=<filename>:生成 Java 进程的内存映像文件,如果指定了 live 参数,则只包含活动对象。format 参数指定输出文件的格式,可以是 b(二进制)或 hprof(Hprof 格式)。file 参数指定输出文件的路径和名称。

怎么确定一个对象是否可回收(是否是垃圾)?

  • 可达性分析 也就是是否是可达对象,如果可达则不可回收,反之为垃圾对象
  • 引用指向对象 是否有引用也就是指针指向这个对象

什么是运行时数据区?

  • 方法区
    • 方法区是各个线程共享的内存区域,在虚拟机启动时创建
    • 方法区描述为堆的一个逻辑部分
    • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
    • 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
  • 程序计数器
    • 需要CPU去调度线程,在这过程中还可能一直切换线程

栈帧结构与动态链接的作用是什么?

  • 局部变量表:保存方法的局部变量
  • 操作数栈:JVM使用一种基于栈的指令集,在执行运算时,实在操作数栈上执行
  • 动态链接:指向运行时常量池的引用
  • 方法返回地址:方法返回地址和附加信息

类的生命周期

  • 加载:编写.java文件、编译成.class文件
  • 连接
    • 验证:验证字节码文件是否可执行
    • 准备:准备基本数据类型初始化
    • 解析:字符引用解析成直接引用
  • 初始化:调用类的静态方法/new出来对象的时候就会进行初始化工作
  • 使用:使用对象操作
  • 卸载:被垃圾回收器回收

对象一定分配在堆上面吗?

​ 不一定的,也有可能分配在栈的,因为JDK有一个优化的手段是通过逃逸分析来减少内存堆分配的压力

什么是逃逸分析?

​ 逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析与外形分析相关联,当变量(或者对象)在方法中分配后,其指针有可能被返回或被全局引用,这样就会被其他方法或者线程所引用,这种现象就称为指针(或者引用)的逃逸。

通俗讲就是一个对象new出来后,它可能被外部所调用,如果是作为参数传递到外部了,就称之为方法逃逸。当方法逃逸了之后,就存储在堆中,反之存储在栈中!, 使用“ -xx: -DoEscapeAnalysis ” 的JVM参数将所有对象都存储在堆中
**

// 当外部调用此方法,并且返回了对象引用此方法就是逃逸,就称为方法逃逸,此对象就存储在堆中
public static User createUser() {
   User user = new User();
   user.setName("123");
   return user;
}

// 当外部调用此方法,未返回了对象引用此方法就是未逃逸,此对象就存储在栈中
public static String createUser() {
   User user = new User();
   user.setName("123");
   return user.toString();
}

public static void createUser() {
   User user = new User();
   user.setName("123");
}

逃逸分析优点:

  • 栈上分配:如果对象分配在栈上,随着方法出栈后,对象随着销毁。
  • 同步消除:线程同步本身是相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全的消除掉
  • 变量替换:如果是基本类型,不可拆分,那么就称之为标量,把一个JAVA对象拆散,将其用到的成员变量恢复到原始类型来访问,这个过程就称为标量替换,加入逃逸分析能够证明一个对象不会被方法外部访问,那么这个对象可以被拆散,那么可以不创建对象,直接用创建若干个成员变量来代替,可以让对象的成员变量在栈上分配和读写。

频繁出现minor gc怎么做?

​ 优化Minor GC频繁问题:由于新生代空间较小,Eden区很快被填满,就会导致频繁的Minor GC, 因此需要通过调大-Xmn分配的内存来降低Minor GC频率

# 1000 代表1秒钟打印一次
jstat -gc <进程号>  1000

问题排查思路

1、使用top命令获取到进程pid
2、根据printf '0x%x\n' <pid> 获取到对应的十六进制
3、根据jstack命令 <pid> | grep pid的十六进制id -A 20 就可以看到对应的cpu飙高的代码行了

导出还在存活的文件的快照信息。
jmap -dump:live, file=/home/sdc/demo/file.hprof <pid>

标签:进阶,对象,回收,零到,GC,垃圾,JVM,线程,加载
From: https://www.cnblogs.com/curryAhui/p/18174656

相关文章

  • 基于Luckfox Pico的opencv使用UDP协议与ubuntu传输摄像头数据-小白进阶
    使用UDP传输opencv的mat数据并显示本教程适用于进阶的小白尝试先说一下背景吧,正在工作的我,突然间看到淘宝上有个很漂亮的价格还不错的linux小板子,遂买下。没错,工作太无聊以至于开始摸鱼学习~但奈何每天工作完回家就像躺着,所以板子到手都快半年了才开始研究实现了简陋的摄像头......
  • 【动画进阶】巧用 CSS/SVG 实现复杂线条光效动画
    最近,群里在讨论一个很有意思的线条动画效果,效果大致如下:简单而言,就是线条沿着不规则路径的行进动画,其中的线条动画可以理解为是特殊的光效。本文,我们将一起探索,看看在不使用JavaScript/Canvas的基础上,使用纯CSS/SVG的方式,我们可以如何大致的还原上述的线条动画效果。基于......
  • [转帖]深入JVM - Code Cache内存池
    https://juejin.cn/post/6985913007142354958 1.本文内容本文简要介绍JVM的CodeCache(本地代码缓存池)。2.CodeCache简要介绍简单来说,JVM会将字节码编译为本地机器码,并使用CodeCache来保存。每一个可执行的本地代码块,称为一个nmethod。nmethod可能对应一个......
  • 数论进阶
    数论进阶原根与阶阶若\(a,p\)互质,定义\(a\)在模\(p\)意义下的阶为最小的正整数\(t\)满足\(a^t\modp=1\)。\(a\)在模\(p\)意义下的阶记作\(ord_p(a)\),\(a^{ord_p(a)}\modp=1\)。对于整数\(k\),\(a^k\equiv1(\modp)\)当且仅当\(ord_p(a)|k\)。计算......
  • cryostat jvm 容器化环境安全的jfr管理工具
    cryostat属于一个jfr管理工具,由红帽团队开发,可以用来安全的管理容器环境中的jfr处理包含的工具operator 可以方便的集成到k8s,openshift中agent 可以实现cryostat发现以及jfr数据的推送grafanadatasource支持 数据grafanadatasource的一个插件,可以方便使用grafan......
  • Python进阶篇笔记
    一、面向对象1、面向过程与面向对象面向过程:把程序流程化面向对象:把程序抽象成类,类与类之间有联系2、类与对象对象就是容器,是用来存放数据和功能的,对象就是数据和功能的集合类的作用是吧对象做区分和归类,以及解决不同对象存相同数据的问题。类也是容器,也是用来存放数据和......
  • 大模型_2.1:Prompt进阶
    目录:1、PromptframeWork2、Prompt结构化格式3、如何写好结构化Prompt?4、Zero-ShotPrompts5、Few-ShotPrompting6、自洽性Self-Consistency7、Program-AidedLanguageModels 1、PromptframeWork 结构化、模板化编写大模型Prompt范式的思想目前已经广为传......
  • solidity进阶(更新中)
    开启第二阶段,主要学习合约部署、测试和预言机。CryptoZombies的教程是用Truffle,现在主流是Hardhat,但学一学思想也有益无害。----------------------------update5.3学完了Truffle部署合约,后面几节是部署到它们的Loom网络,就不写这几节的笔记了 启动一个新的终端窗口,创建项......
  • 学习笔记-JVM OOM实验
    堆内存溢出packagecom.dameng.lxm;importjava.util.ArrayList;importjava.util.List;publicclassHeapOOM{ staticclassOOMObject{ } publicstaticvoidmain(String[]args){ List<OOMObject>objlist=newArrayList<OOMObject>(); while......
  • JVM内存管理
    在JVM初识中提到之所以在程序和操作系统之间增加JVM,就是JVM有些内存管理的特性直接在操作系统上实现有些费劲。那么JVM的内存管理是怎样的呢?应用程序运行大致如下过程: 其中内存部分就是运......