jvm

作者: 追风还是少年 | 来源:发表于2023-07-17 23:18 被阅读0次

    jvm主要组成部分

    包含两个子系统(类加载器、执行引擎)和两个组件(运行时数据区、本地库接口)


    jvm组成
    • 运行时数据区域:即我们常说的jvm内存
    • 执行引擎:执行class中指令
    • 本地库接口:与本地方法库交互,与其它编程语言交互的接口
    • 类加载器:装载class文件到运行时数据区的方法区
    代码执行过程

    编译器把java代码编译成字节码,类加载器再把字节码加载到内存,放入运行时数据区的方法区,命令解析器执行引擎再将字节码翻译成底层系统指令,再交由CPU去执行,过程中可能需要调用其它语言的本地库接口来实现整个功能。

    jvm内存模型(jvm运行时数据区)

    运行时数据区
    • 方法区:存储类信息、常量、静态变量、即时编译后代码
    • java堆:几乎所有对象实例分配在java堆
    • 虚拟机栈:服务于java方法,存储局部变量表、操作数栈、动态链接、方法出口
    • 本地方法栈:与虚拟机栈一样,只不过是服务于调用native方法
    • 程序计数器:当前线程执行的字节码的行号指示器,通过改变计数器的值来选取下一条需要执行的字节码指令

    对象分配内存

    内存分配的两种方式:

    • 指针碰撞,已使用内存放一边,空闲内存放另外一边,已经使用内存和空闲内存中间有一个指针作为分界点的指示器,分配内存时,只需要把指针往空闲内存那边移动一段与对象大小相等的距离
    • 空闲列表,已使用内存与空闲内存相互交错,虚拟机必须维护一个列表来记录哪些内存空间是空闲的,分配内存时,从列表找一块足够大的空间划分给对象,并更新列表上的记录

    对象的创建是非常频繁的,哪怕只是修改一个指针指向的位置,在并发情况下也是不安全的,可能出现正在给A对象分配内存,指针还没来得及修改,对象B又同时使用该指针来分配内存的情况。

    内存分配的并发安全处理:

    • 同步处理,CAS + 失败重试来保证操作的原子性
    • 本地线程分配缓存(Thread Local Allocation Buffer,TLAB),每个线程在java堆中预分配一小块内存,称为本地线程分配缓存,每个线程分配内存时,只要在自己的TLAB上分配内存;只有线程的TLAB用完并需要分配新的TLAB时,才需要同步锁。-XX:+/-UserTLAB参数设置是否使用TLAB

    对象访问定位

    对象访问一般是通过栈上的引用访问堆中的具体对象,对象访问方式取决于JVM虚拟机的实现,目前主流的访问方式有句柄和直接指针两种。

    直接指针:指向对象,代表一个对象在内存中的起始地址
    句柄:可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址

    • 句柄访问:Java堆中划分一块内存作为句柄池,引用中存储对象的句柄地址,而句柄中包含对象实例数据的具体地址信息和对象类型数据的具体地址信息。
      优点:引用中存储的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,而引用本身不需要修改
    • 直接指针:引用中存储的直接是对象地址,那么java堆对象内部的布局中就必须考虑如何放置访问数据类型的相关数据
      优点:速度更快,节省了一次指针定位的时间开销,由于对象的访问在java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。Hotspot中就是采用这种方式


      句柄
    直接指针

    对象引用类型

    java中有哪些引用类型:

    • 强引用,即使内存空间不足,也不会回收
    • 软引用,内存空间足够,不会被回收,内存空间不足,就会回收
    • 弱引用,不管内存是否足够,只要gc就回收
    • 虚引用,任何时候都会被垃圾回收器回收,主要用于跟踪垃圾回收器回收的活动

    堆内存分代

    • 新生代,存放新生对象,使用复制算法
    • 老年代,存放生命周期长的对象,使用标记清除算法
    • 堆 = 新生代(Young) + 老年代(Old)
    • 新生代 = Eden + S0(from) + S1(to)

    默认配置:
    新生代:老年代 = 1:2,通过-XX:NewRatio配置
    Eden:From:To = 8:1:1,通过-XX:NewSurvivorRatio配置

    判断对象是否可回收

    • 引用计数器算法
    • 可达性分析算法
      通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
      Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
      (1)虚拟机栈中引用的对象
      (2)本地方法栈中引用的对象
      (3)方法区中类静态属性引用的对象
      (4)方法区中的常量引用的对象
    • 方法区回收
      因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。
      主要是对常量池的回收和对类的卸载。
      在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
      类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:(1)该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
      (2)加载该类的 ClassLoader 已经被回收。
      (3)该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
      可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。

    分代收集

    现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
    一般将堆分为新生代和老年代。

    • 新生代使用: 复制算法
    • 老年代使用: 标记 - 清除 或者 标记 - 整理 算法
    垃圾收集器
    image.png

    以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

    • 单线程与多线程: 单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
    • 串行与并行: 串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。

    CMS收集器
    CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
    分为以下四个流程:

    • 初始标记
      仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
    • 并发标记
      进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
    • 重新标记
      为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
    • 并发清除
      不需要停顿。

    在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿

    G1 收集器
    G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收

    image.png

    G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离


    image.png

    通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。
    这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
    每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

    • 初始标记
    • 并发标记
    • 最终标记
      为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
    • 筛选回收
      首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

    Minor GC、Major GC、Full GC

    • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集。
    • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集。
    • 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾

    内存分配策略

    • 对象优先在 Eden 分配
      大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC
    • 大对象直接进入老年代
      大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制
    • 长期存活的对象进入老年代
      为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。-XX:MaxTenuringThreshold 用来定义年龄的阈值。
    • 动态对象年龄判定
      虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
    • 空间分配担保
      在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。

    jvm命令

    • jps
      查看当前java进程
    • jstack
      查看Java 应用程序中线程堆栈信息
    • jinfo
      查看正在运行的 java 应用程序的扩展参数,包括Java System属性和JVM命令行参数
    • jmap
      它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列
    #显示Java堆详细信息,包括使用的GC算法、堆配置信息和各内存区域内存使用信息
    jmap -heap pid
    #显示堆中对象的统计信息,包括每个Java类、对象数量、内存大小(单位:字节)、完全限定的类名
    jmap -histo:live pid
    
    • jstat
      查看堆内存各部分的使用量,以及加载类的数量
      jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]
    jstat -gcutil 2815 1000 
    

    相关文章

      网友评论

          本文标题:jvm

          本文链接:https://www.haomeiwen.com/subject/ejagudtx.html