JVM 常见的问题

1. 什么是JVM?

JVM(Java Virtual Machine,Java虚拟机)是运行所有Java应用程序的软件平台。它与硬件无关,并且在任何具有JVM实现的平台上运行Java字节码,从而提供了Java程序的跨平台能力。JVM是Java运行时环境(JRE)的一部分,负责代码的加载、验证、编译以及运行。

2. JVM的工作流程

当运行Java程序时,JVM首先加载编译后的字节码文件(.class文件),然后将这些字节码转换成机器码。在这个过程中,JVM通过类加载器(Class Loader)将类加载到内存中,然后在运行时数据区(Runtime Data Areas)存储程序数据,最后由执行引擎(Execution Engine)执行这些指令。

  1. 编译源代码: :使用Java编译器(如javac)将.java文件编译成字节码文件.class
  2. 类加载 :当Java程序运行时,JVM通过类加载器(Class Loader)加载这些.class文件。

类加载主要按照以下步骤执行:

  • 加载(Loading)
  • 链接(Linking)

这个步骤又可细分成三个步骤进行:

(1)验证

(2)准备

(3)解析

  • 初始化(Initialization)
  1. 运行时数据区:(JVM的内存区域划分)
  • 程序计数器(Program Counter Register):每个线程都有一个程序计数器,是线程私有的,它的作用是当前线程所执行的字节码的行号指示器。
  • 方法区(Method Area)也可称为元数据区:存储每个类的结构信息,如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。
  • 堆(Heap):JVM管理的最大一块内存区域,用于存储所有的对象实例和数组
  • 栈(Stack):存储局部变量和部分结果,并在方法调用和返回时起作用。每个线程拥有自己的栈
  • 本地方法栈(Native Method Stack):为JVM使用到的Native方法服务。
  1. 垃圾回收:JVM在堆内存中管理数据,其中包括垃圾收集(Garbage Collection,GC)。GC的目的是识别和删除不再被使用的对象,以释放和重用资源。

3. JVM的内存区域划分

  • 程序计数器

程序计数器是一个较小的内存区域,用于记录当前线程所执行的字节码指令的地址。每个线程都有一个独立的程序计数器,以便在线程切换时能够恢复到正确的执行位置。(记录当前执行到哪个指令地址)

  • 元数据区(常量池、方法元信息、类元信息)

元数据区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码。(保存当前类被加载好的数据,即类对象等)

  • (每个栈帧中都会保存局部变量,操作栈,方法返回地址等信息)

用于保存方法的调用关系。分为虚拟机栈和本地方法栈:

  • 虚拟机栈

虚拟机栈是线程私有的内存区域,每个线程在执行方法时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。栈帧随着方法的调用和返回而入栈和出栈。

  • 本地方法栈

本地方法栈与虚拟机栈类似,但它为Native方法服务。在一些虚拟机实现中,本地方法栈和虚拟机栈是合并在一起的

  • 堆(保存new 的对象的)

Java堆是所有线程共享的内存区域,用于存放对象实例和数组。堆是垃圾回收的主要区域,通常分为新生代和老年代。新生代进一步划分为Eden区和两个Survivor区(S0和S1)

需要注意的是:如Object b = new Object();这样的操作不一定属于堆;

(1)当b是局部变量,那么b就在栈上

(2)当b是成员变量,那么b就在堆上

(3)当b是静态变量,那么b就在元数据区上

一个进程中有多个线程,每个线程都有一份程序计算器和栈,整个进程共用同一份元数据区和堆。

4. 类加载机制

JVM类加载机制 是Java虚拟机(JVM)的一个核心组成部分,它负责从文件系统或网络中加载Class文件,Class文件在文件开头有特定的文件标识。JVM在运行时期间完成类的加载、连接和初始化过程。

  • 第一步:加载(Loading)

根据类的全限定名 (包名 + 类名),形如:java.lang.String。将找到的该类的.class文件读取到内存里

  • 第二步:链接(Linking)

链接部分可以分为验证、准备、解析三个部分。

① 验证(Verify)

解析并校验当前读到内存中的.class文件的内容是否合法
② 准备(Prepare)
给类对象申请内存空间 (申请的内存空间是没有初始化的,相当于是全0的空间)
③ 解析(Resolve)

将常量池内的符号引用转换为直接引用的过程(将常量池内的符号引用解析成为实际引用),即针对字符串常量池进行初始化的过程。(字符串常量本身就包含在.class文件中,我们会将从.class文件中获取的字符串常量放到元数据区的常量池中)

  • 第三步:初始化(initialization)

针对刚才的类对象进行初始化(对类对象中的各种属性进行填充) ,实际上就是执行类的构造器方法init()的过程。如果这个类有父类,且父类还没加载,此环节也会触发父类的类加载过程。

双亲委派模型

JVM中通过类加载器来负责类加载的过程。JVM默认提供了三种类加载器:

  • Application ClassLoader 应用程序类加载器:负责加载应用程序的类(java的第三方库 / 当前项目所包含的类)
  • Extension ClassLoader 拓展类加载器:负责加载Java的拓展库的类
  • Bootstrap ClassLoader 启动类加载器:负责加载Java的基础核心类库
    所谓的双亲委派模型,就是在进行类加载的时候会优先将请求交给父亲进行处理的一种任务委派机制。

如果所示:

当进行类加载,通过全限定名找.class文件的过程中,我们会将Application ClassLoader 应用程序类加载器作为入口开始,然后将加载类的任务向上委托给其父亲Extension ClassLoader 拓展类加载器,其父亲Extension ClassLoader 拓展类加载器再向上委托给它的父亲Bootstrap ClassLoader 启动类加载器,而Bootstrap ClassLoader 启动类加载器由于没有父亲,就会开始在它管辖的范围内进行类加载,当加载失败时就会将任务返回回它的儿子Extension ClassLoader 拓展类加载器,再在Extension ClassLoader 拓展类加载器的范围内进行类加载,当任务再次失败时,就会将任务交还回它的孩子Application ClassLoader 应用程序类加载器,如果在这一级仍然加载失败就抛出ClassNotFoundException异常。

在其中某一级加载成功,就直接结束这个过程。

5. 垃圾回收机制(Garbage Collection, GC)

JVM (Java虚拟机)的垃圾回收机制是Java内存管理中的核心功能,它负责自动管理对象的生命周期,回收不再使用的对象所占的内存空间。GC回收的是JVM中堆的内存区域,GC回收垃圾的基本单位是对象。

GC工作的过程主要分为找到垃圾 和 释放垃圾两个过程。

找垃圾(死亡对象)

引用计数法:给对象增加一个引用计数器,每当有一个地方的引用指向它时,计数器就+1;当引用是失效时,计数器就-1;当计数器为0时,就说明当前对象不再被使用,是一个垃圾。

引用计数的问题:

  • 内存消耗更多
  • 可能出现对象的循环引用的问题

可达性分析 [ java中采取这个方案 ]

通过⼀系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为引用链,每个可到达的对象都标记为可达,当⼀个对象到GC Roots没有任何的引用链相连时(就说从GC Roots到这个对象不可达),当一个对象不可达就说明这个对象不可用是要回收的垃圾。

对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为 可回收对象。

Java中常见的可作为GC Roots的对象如下:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象 public class Test { public static void main(String[] args) { Test a = new Test(); // GC Roots对象 a } }
  • 方法区中静态属性引用的对象 :类的静态变量引用的对象。例如: public class Test { public static Test s; public static void main(String[] args) { s = new Test(); // GC Roots对象 s } }
  • 方法区中常量引用的对象 :常量池中的常量引用的对象,例如字符串常量池中的字符串对象。例如:public class Test { public static final Test s = new Test(); // GC Roots对象 }
  • 本地方法栈中JNI引用的对象 :JNI(Java Native Interface)引用的对象。例如: public native void nativeRoot();

JVM中的可达性分析是一个周期性的过程,每个一段时间会触发一次可达性分析,这就说说明GC垃圾的处理并不是立刻。

回收垃圾

当我们知道哪些对象是垃圾后,就应该进行垃圾的释放。

标记 - 清除(Mark-Sweep)算法 :将垃圾对象的内存直接进行释放,但是这样的做法会产生内存碎片问题。

内存碎片问题可能会导致后续程序运行中需要分配较大对象时,明明总内存量足够,却无法找到足够大的连续内存来分配。

复制(Copying)算法 :它将可用内存按容量划分为大小相等的两块,每次只使用其中的⼀块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上, 然后再把已经使用过的内存区域⼀次清理掉。

缺点:

  • 内存的空间利用率低
  • 一旦不是垃圾的对象较多,复制的成本会很高(尤其是需要复制的对象是一个较大的对象时)

标记-整理(Mark-Finishing)算法 :将垃圾对象的内存直接进行释放,然后让所有存活对象都向一段移动,然后清理掉端边界以外的内存。

分代收集(Generational Collection)算法分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收。 根据对象存活周期的不同将内存划分为几块。⼀般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用标记-清理或者标记-整理算法

堆主要分为 2 个区域,年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 两个区。

新创建的对象会被放到伊甸区,大部分处于伊甸区的对象活不过第一轮,而那些存活下来的对象会通过复制算法放到幸存区From中,如果这个对象在幸村区FromTo进行了多次GC的扫描都活下来,那么这个对象将从新生代晋升到老年代中。

哪些对象会进⼊新生代?哪些对象会进⼊老年代?

  • 新生代:⼀般创建的对象都会进⼊新生代
  • 老年代:大对象和经历了N次垃圾回收(一般默认N = 15)依然存活下来的对象会从新生代进入老年代

题外话:请问了解Minor GCFull GC么,这两种GC有什么不⼀样吗?

  1. Minor GC⼜称为新⽣代GC:指的是发生在新生代的垃圾收集 。因为Java对象大多都具备朝生夕灭的特性,因此**Minor GC(采⽤复制算法)非常频繁,一般回收速度也比较快**。
  2. Full GC又称为老年代GC或者Major GC:指发生在老年代1的垃圾回收 。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度⼀般会⽐Minor GC慢10倍以上
相关推荐
m0_708830962 小时前
C++中的原型模式变体
开发语言·c++·算法
薯片锅巴2 小时前
锅巴的JavaScript进阶修炼日记2:面向对象编程/原型及原型链
开发语言·javascript·ecmascript
热爱编程的小刘2 小时前
Lesson02---类与对象(上篇)
开发语言·c++
!停2 小时前
数据结构时间复杂度
c语言·开发语言·算法
Cult Of2 小时前
一个最小可扩展聊天室系统的设计与实现(Java + Swing + TCP)(2)
java·jvm·tcp/ip
一叶星殇2 小时前
.NET 6 NLog 实现多日志文件按业务模块拆分的实践
开发语言·.net
2301_811232982 小时前
使用Python进行PDF文件的处理与操作
jvm·数据库·python
lead520lyq2 小时前
Golang GPRC流式传输案例
服务器·开发语言·golang
xyq20242 小时前
《C 经典100例》
开发语言
不染尘.3 小时前
二分算法(优化)
开发语言·c++·算法