JVM相关知识点

引言

本文主要介绍JVM相关内容,带着问题去逐步掌握JVM相关原理和机制,帮助我们更好的理解java程序是如何在虚拟机中运行的。

一、JVM介绍

1.1 java虚拟机的结构是如何设计的

JVM即java虚拟机,是套标准。能运行字节码指令,jvm的好处是可以屏蔽操作系统细节,达到一次编写,到处运行的目的。如下图所示例:

实现JVM的厂商很多,如HotSpot、JRockit、IBMJ9等。主流是HotSpot,JDK和OpenJDK都是基于HotSpot。

HotSpot JVM架构组成类装载器子系统、运行时数据区、执行引擎

运行机制

  1. 首先由类装载器子系统把编译好的.class字节码文件装载到类加载子系统,主要过程查找并验证,完成内存分配和对象赋值。

  2. 类装载到内存后,由运行时数据区完成数据的存储和交换。

    JVM的内存区域和对应的调优参数总结成下图所示,便于大家记录调优涉及到的参数(我个人认为所谓的JVM调优其实有点水,只是调整一些配置,设置参数,不能称之为调优,咱也没那个能力调优不是吗哈哈)。

3.执行引擎主要包含编辑器 (JIT Compiler)和垃圾回收器 (Garbage Collector)

即时编译器主要用于将字节码翻译成操作系统能够执行的CPU指令,可以通过JVM参数来设置是解释执行还是编译执行。
解释执行 ,就是把字节码作为源程序输入解释执行,不必等待编译器全部编译后才执行,可以节省不必要的编译时间。
编译执行 ,由编译程序先把目标代码一次性编译成目标程序,再由机器运行,执行效率更高,占用内存资源更少。

在HotSpot中,默认是以上两种方式的组合。

垃圾回收器主要是回收运行时方法区产生的垃圾数据,就是各种垃圾回收算法的实现,大概分为三种,复制算法、标记清除算法、标记整理算法,我们可以通过JVM来设置选择哪种算法。

这里放一张整体的交互图,通过这个图我们对JVM有一个更清晰的认知:

1.2 什么是双亲委派机制

以下是Java的类加载机制:

Java编译器把Java源文件编译成.class文件,再由JVM把.class文件加载到内存中,加载成功后得到一个Class字节码对象。拿到字节码对象之后就可以进行实例化了。

重点来了,类的加载过程需要用到加载器,JVM提供了3种类装载器,分别是Bootstrap类加载器、Extension类加载器和Application类加载器。

  1. Bootstrap类加载器,主要负责java核心类库的加载,也就是%{JAVA_HOME}\lib下的rt.jar,resources.jar等。

  2. Extension类加载器,%{JAVA_HOME}\lib\ext目录下的jar包和class文件。

  3. Application类加载器,主要负责当前应用中的classpath下的所有jar包和类文件。

除了以上JVM提供的类加载器,我们还可以通过继承ClassLoader的方式,重写findClass()方法来自定义类加载器,来满足一些特殊场景的需求,比如从远程网络加载.class文件。

下面的图展示了各种类加载器之间的关系:


双亲委派机制或者父级委托模型,就是按照类加载器的层级关系逐层进行委派,如下图所示。

这种上级委托加载机制设计有两个好处:

  1. 保证安全性,这种层级关系代表的是一种优先级,所有类加载优先使用Bootstrap。对于核心类库就无法破坏,比如自己写一个java.lang.String,最后会交给Bootstrap加载器,每个类加载器都有自己的作用范围,自己写的类无法覆盖核心类库中的类。

  2. 避免重复加载。避免类已经被父类加载器加载了,重复加载导致混乱问题,也是一种向上检查的过程。

二、内存管理

2.1 JVM如何判断对象可以回收

JVM中判断一个对象是否可以被回收,首先应该判断这个对象是否还在被使用,只有没被使用的对象才能被回收。

引用计数器,就是为每一个对象添加计数器,用来统计指向当前对象的引用次数,每引用一次该对象,就把这个计数器加一,反之减少,当变为0时说明可以回收了。缺点也很明显,需要额外的存储空间来存储引用计数器。但是实现简单,效率高。

主流的JVM没有采用这个方法,因为引用计数器在处理一些的循环引用或者相互依赖的情况时,可能会出现一些不再使用但是又无法回收的内存,造成内存泄漏的问题,如下图所示:

主流JVM使用一个叫可达性分析的算法来判断对象是否应该被回收。

先来说下这个可达性分析的概念:

  • 可达性分析 (Reachability Analysis)是垃圾回收(Garbage Collection, GC)机制中用来确定哪些对象是活跃的、哪些对象可以被回收的一种算法。它的基本原理是从一组被称为"GC Roots"的根对象开始,沿着对象之间的引用链进行深度优先或者广度优先的搜索,来判断哪些对象是可达的。
  • GC Root指的是垃圾回收机制可以访问到的一系列对象起点 。这些根对象是垃圾回收器在进行可达性分析算法时的起始点。如果一个对象可以从GC root直接或间接地访问到,那么这个对象就被认为是可达的,不会被垃圾回收器回收。以下是一些常见的GC root类型:
    • 栈中的本地变量:方法参数、局部变量等。
    • 静态字段引用:类中的静态成员变量。
    • 常量池中的引用:如字符串常量池中的引用。
    • 线程工作内存中的引用:每个活动线程的引用。
    • JNI(Java Native Interface)全局引用:本地代码通过JNI持有的一些Java对象引用。

GC Root 对象比如虚拟机栈中的引用,本地方法栈的引用,这些都是不能回收的,沿着这些对象的引用链往下搜索,寻找它的直接和间接引用对象。遍历完后如果发现某些对象不可达,那就认为这些对象已经没有用了,需要被回收。垃圾回收时会先找到所有的GC Root,这个过程会暂停所有用户线程,也就是stop the world,再从根节点向下寻找,可达对象保留,不可达的就回收。

2.2 如何理解JVM中的GC算法

GC就是Garbage Collector,垃圾回收算法主要有三种:

  1. 标记清除算法 :将存活的对象打上标记,没有被标记的对象就是需要被回收的垃圾对象。
    缺点:会产生比较多的内存碎片,随着时间推移,这些内存碎片存在,系统无法再大量分配连续的内存空间。这会触发频繁的GC操作。

  2. 标记复制算法 :把内存分为两等份,每次使用其中一份,等到正在使用的这部分内存满了之后,就会标记出存活对象,然后把存活对象复制到另一部分闲置的内存中,留在第一部分内存中的对象会全部被垃圾回收器回收。
    缺点:导致实际使用的内存只有50%,浪费空间。对象量大的话,复制会很耗时。这种算法适合处理存活对象少,垃圾对象比较多的场景。

  3. 标记整理算法 :先标记出存活的对象,再把所有存活的对象整理到内存空间的另一端,而没有被标记的对象就可以被覆盖或者释放。
    缺点:本质和标记清除算法差不多,缺点也类似,还多了一个移动的操作。

java对象基本都是临时对象,用完就回收,根据对象在内存中的中的存活时间,分为年轻代、老年代、永久代。

年轻代采用标记复制算法,每次复制后存活下来的对象少。

老年代是经历过几次垃圾回收的对象,JVM认为可能继续存活下来,采用的是标记清除算法。

永久代表示会一直存活的对象,只有在触发Full GC的情况下才会回收,永久代如果创建对象过多会造成内存溢出,永久代采用的也是标记清除算法。不过重要的是JDK8移除了永久代的概念,取而代之的是元空间,也就是方法区

2.3 JVM年龄分代为什么是15次

JVM的年龄分代策略是用于决定对象何时从年轻代(Young Generation)晋升到老年代(Old Generation)的一种机制

对象在年轻代中经历多次垃圾回收(Garbage Collection,GC)后,如果仍然存活,其年龄值会逐渐增加。这个年龄值是由对象头部的Mark Word中的部分位来表示的。

在Java 8及之前版本中,对象年龄的表示使用了Mark Word中的4个比特位,这意味着年龄的最大值是2^4 - 1 = 15。这是因为4比特可以表示从0到15共16种状态,但通常0表示新创建的对象,因此15是对象年龄可以达到的最大值

默认情况下,GC年龄达到15岁后,对象就会从年轻代转移到老年代。

选择15次作为年龄上限的原因主要是基于统计和经验。JVM的设计者发现,大多数对象在创建后很快就会被垃圾回收,只有少数对象会存活足够长的时间以至需要移动到老年代。15次是一个足够大的数字,可以确保那些确实需要长期存在的对象能够被识别并移到老年代,同时又不会浪费过多的位数来表示年龄,因为Mark Word的空间是有限的,需要存储其他重要的信息,如锁标志、线程ID 等。

如果需要改变这个默认值,可以通过JVM参数进行调整,但是由于Mark Word的位数限制,年龄的最大值不能超过15。在某些JVM实现中,可以通过参数如-XX:MaxTenuringThreshold来调整这个阈值,但实际可设置的范围可能受限于Mark Word中年龄字段的位数。

2.4 JVM为什么使用元空间代替永久代

JVM使用元空间(Metaspace)代替永久代(Permanent Generation)的主要原因是为了解决永久代的一些固有问题,尤其是与内存管理和性能相关的问题。以下是几个关键因素:

  1. 内存溢出问题: 在JDK 1.8之前,永久代的大小是固定的或者可以通过JVM参数调整,但它仍然属于堆内存的一部分。这意味着如果永久代的大小设置不当,很容易出现OutOfMemoryError: PermGen space错误。元空间将类元数据存储在本地内存中,而不是堆内存中,这避免了由于永久代满而导致的频繁垃圾收集。

  2. 更灵活的内存管理: 元空间的大小不再受到堆大小的限制,而是受限于系统的可用物理内存和最大映射文件大小。这使得元空间的管理更加灵活,可以随着类的加载和卸载自动扩展和收缩,减少了手动调整JVM参数的需求。

  3. 减少GC压力: 将类元数据移出堆意味着减少了堆内存的负载,从而减少了垃圾回收的压力。在永久代中,类元数据的垃圾回收(也称为类卸载)是一个复杂的过程,而在元空间中,类元数据的管理与堆上对象的管理分离,使得垃圾回收更加高效。

  4. 简化JVM配置: 使用元空间之后,开发人员和系统管理员不需要再关注永久代的大小设置,因为元空间的大小会根据需要自动调整。这简化了JVM的配置过程,减少了因配置不当导致的性能问题。

  5. 性能优化: 元空间的引入允许JVM对类元数据的访问进行优化,因为它们现在位于本地内存中,这可能比堆内存提供更快的访问速度。

相关推荐
学到头秃的suhian2 小时前
JVM-类加载机制
java·jvm
NEFU AB-IN9 小时前
Prompt Gen Desktop 管理和迭代你的 Prompt!
java·jvm·prompt
唐古乌梁海14 小时前
【Java】JVM 内存区域划分
java·开发语言·jvm
众俗15 小时前
JVM整理
jvm
echoyu.15 小时前
java源代码、字节码、jvm、jit、aot的关系
java·开发语言·jvm·八股
代码栈上的思考1 天前
JVM中内存管理的策略
java·jvm
thginWalker1 天前
深入浅出 Java 虚拟机之进阶部分
jvm
沐浴露z1 天前
【JVM】详解 线程与协程
java·jvm
thginWalker1 天前
深入浅出 Java 虚拟机之实战部分
jvm
程序员卷卷狗3 天前
JVM 调优实战:从线上问题复盘到精细化内存治理
java·开发语言·jvm