简说JVM

目录

前言

正文

JVM内存区域划分

JVM执行方式

JVM的类加载机制

类加载器

类加载器的类型

自定义类加载器

垃圾回收

垃圾回收的问题

垃圾回收的范围

垃圾回收机制

垃圾回收算法

释放垃圾内存空间


前言

在Java的技术体系中,有两个至关重要的组件,分别是JVM(Java虚拟机)和Javac(Java编译器),它们在Java程序的生命周期中扮演着截然不同但又紧密关联的角色。
 
Javac编译器的职责是将开发者编写的.java源文件编译成.class字节码文件。这些字节码文件中所包含的Java字节码,可看作是Java自定义的一套类似于“CPU指令”的代码。在这一编译过程中,高级的Java代码被转化为一种JVM能够理解的中间形式代码。Javac的输入是.java源文件,输出则是.class字节码文件,它依赖于JDK(Java Development Kit),是JDK不可或缺的一部分。
 
JVM作为Java程序的运行时环境,承担着加载、验证、解释或编译(借助JIT编译器)并执行.class字节码文件的重任。在运行阶段,JVM会把字节码转换为底层机器能够识别并执行的机器指令。JVM的输入是.class字节码文件,输出则体现为程序的运行结果,例如控制台输出、文件写入等操作的结果。它依赖于JRE(Java Runtime Environment),并且是JRE的核心组件。
 
简单来说,Javac就像是一位严谨的“翻译前处理官”,负责将Java源代码转换为字节码;而JVM则如同一位精准的“翻译执行官”,把字节码转换为机器指令并运行程序。二者紧密协作,共同铸就了Java“一次编写,到处运行”(Write Once, Run Anywhere)这一强大且极具吸引力的特性。
 
在本博客中,只是对JVM进行了简单介绍,若存在任何不准确或错误之处,欢迎各位读者不吝赐教、批评指正。

正文

在Java技术体系里,JVM无疑处于核心地位。它的职责范畴远不止于字节码的加载、验证与执行,更关键的是,借助即时编译(JIT)、垃圾回收(GC)等一系列复杂而精妙的机制,将高级的Java代码高效地转化为底层机器指令,这是Java实现跨平台运行的根本所在。深入钻研JVM的内存模型、类加载机制、执行引擎等核心组件的运行原理,对于开发者而言意义重大。这能够帮助开发者从底层逻辑层面透彻理解Java程序的运行机制,助力开发者达成编写高性能、高可靠性Java应用程序的目标。
 
JVM主要由四个部分构成:类加载器、运行时数据区、执行引擎和本地库接口。在程序执行前,Java代码需先被转换成字节码(即class文件)。JVM首先通过类加载器(ClassLoader),运用特定方式将字节码文件加载到内存中的运行时数据区(Runtime Data Area)。字节码文件遵循JVM的指令集规范,无法直接被底层操作系统执行,所以需要执行引擎(Execution Engine)这一特定的命令解析器,将字节码翻译为底层系统指令,然后交由CPU执行。在这个过程中,为实现整个程序的完整功能,还需调用本地库接口(Native Interface)来对接其他语言的接口。

类加载器(ClassLoader)—— "图书管理员" 职责:将  .java  文件编译后的  .class  字节码文件(类似一本加密的指令手册)加载到内存。

关键行为:

加载:从磁盘找到字节码文件,读入内存。

验证:检查这本“手册”是否符合规范(比如是否被篡改)。

准备:为类变量分配内存(例如静态变量  static int count )。

解析:将符号引用(如类名、方法名)转换为直接内存地址。

类比:就像图书管理员从书架上找到一本加密的密码书,检查它的完整性,再放到阅览室供后续破译。

运行时数据区(Runtime Data Area)—— "工作车间" 职责:存储程序运行时的所有数据。

核心区域:

方法区(Method Area):存放类结构(如代码、常量、静态变量),类似“车间蓝图”。 堆(Heap):存放所有对象实例(例如  new Object() ),是垃圾回收的主战场,类似“原材料仓库”。

虚拟机栈(JVM Stack):每个线程私有的栈,存储方法调用的局部变量和操作数栈,类似“工人的操作台”。

本地方法栈(Native Method Stack):执行本地方法(如C/C++代码)的栈,类似“外聘技工的操作台”。

程序计数器(PC Register):记录当前线程执行的位置,类似“书签”。 关键点:堆是线程共享的,栈是线程私有的,这解释了多线程并发时的数据隔离性。  

执行引擎(Execution Engine)—— "翻译官+指挥官" 职责:将字节码(跨平台的中间指令)翻译成底层机器码,并驱动CPU执行。

核心机制:

解释器(Interpreter):逐行翻译字节码,优点是即时响应,缺点是效率低。 即时编译器(JIT Compiler):将热点代码(频繁执行的代码)编译成机器码缓存起来,大幅提升性能(例如HotSpot VM的C1/C2编译器)。

垃圾回收器(GC):自动回收堆内存中无用的对象(清理“仓库废料”)。

类比:执行引擎就像一个精通多国语言的指挥官,既能实时翻译指令(解释执行),又能对关键战术提前编译优化(JIT),同时指挥清洁工(GC)保持战场整洁。  

本地库接口(Native Interface)—— "外援通道" 职责:提供调用操作系统底层功能(如文件操作、网络通信)的接口,通过JNI(Java Native Interface)实现。

典型场景:当Java代码需要执行一些高性能或系统级操作(例如  Thread.start()  最终调用操作系统的线程创建)时,通过JNI调用C/C++编写的本地库( .dll  或  .so  文件)。 类比:就像军队需要调用特种部队(本地代码)完成特定任务,本地接口是连接Java和外部世界的桥梁。
简单概括,类加载器将字节码加载至内存;运行时数据区负责管理内存结构;执行引擎承担字节码的翻译工作;本地库接口则负责连接本地方法。这四个主要组成部分紧密协作,共同维持着JVM的稳定运行,支撑起Java程序的各项功能实现 。

JVM内存区域划分

JVM其实也是一个进程(任务管理器中看到的java进程).

进程运行过程中,要从操作系统这里申请一些资源(内存就是经典的资源),这些内存空间就支撑了后续java程序的执行.比如在java中定义变量(就会申请内存),内存其实就是jvm从系统这边申请到的内存.

jvm从系统申请了一大块内存,这一大块内存给java使用的时候,又会根据实际的使用用途来划分不同的空间(这就是所谓的"区域划分").

简说JVM_第1张图片

1.堆区:代码中new出来的对象,就都是在堆里,对象中持有的非静态成员变量,也就是在堆里.我们常⻅的JVM参数设置-Xms10m最⼩启动内存是针对堆的,-Xmx10m最⼤运⾏内存也是针对堆 的。

2.栈区:栈区又分为两种本地方法栈和虚拟机栈,包含了方法调用关系和局部变量线程私有,栈区生命周期与线程相同。

本地方法栈:jvm内部,通过C++写的代码.执行本地方法

虚拟机栈:记录了java代码的调用关系和局部变量(一般不会关注本地方法栈,所以说的栈一般就是说虚拟机栈).

本地方法(Native Method) 是指用非Java语言(如C、C++、汇编等)编写的方法,通过Java本地接口(Java Native Interface, JNI)在Java程序中调用。这类方法通常用于直接与操作系统或硬件交互,或重用已有的非Java库。通过native关键字
注意这里的堆和栈和数据结构中的堆栈是不一样的,这里是使用一块内存空间用来存放数据,而数据结构中的栈和堆是一种结构.
JNI(Java Native Interface):是Java与本地代码交互的桥梁,允许Java代码调用本地方法,也允许本地代码回调Java方法。JNI定义了数据类型、函数签名和调用规范,确保跨语言兼容性。

使用本地方法有以下好处: 

访问底层资源 直接操作硬件、操作系统API(如文件系统、设备驱动)。
性能优化 对计算密集型任务(如图形渲染、数学计算),本地代码可能比Java更高效
复用现有代码 重用C/C++等语言编写的成熟库,避免重复开发
绕过JVM限制 突破Java内存管理或安全沙箱的限制(需谨慎,可能引入风险)


3.程序记数器:当前线程所执行的字节码的行号指示器,这是比较小的空间是用来存放下一条要执行的java指令的地址,记录代码执行到哪一条指令,并下一条执行语指令的地址.    
4.元数据区:是java8之后对方法区的一种实现取代了以前的永久代存放。
元数据区是JVM规范中方法区的具体实现,主要用于存储类的元数据信息,包括类结构(如类名、方法、字段)、常量池、方法代码、静态变量、一个程序有哪些类,每个类中有哪些方法,每个方法里面都包含哪些指令等。

元数据区和永久代区别
特性 永久代(PermGen) 元数据区(Metaspace)
引入版本 JDK 1.0 引入 JDK 8 引入
存储内容 类元数据、字符串常量池 类元数据
内存位置 JVM 堆内存的一部分 本地内存(Native Memory)
垃圾回收 由Full GC触发,效率较低 由专门的垃圾回收器处理,效率更高
内存管理 固定大小或可扩展 由JVM管理 动态分配


我感觉最大提升就是永久代内存需要频繁的手动优化,而元数据区是动态扩展,省去了不少的麻烦,而且在JDK 8及之后的版本中,元数据区(Metaspace) 仅用于存储类元数据(如类的结构信息、方法信息、字段信息等),而 字符串常量池(String Pool) 和 运行时常量池(Runtime Constant Pool) 被移到了 Java堆内存(Heap) 中。

写的java代码if,wile,for等各种逻辑运算...这些操作最终都会被转换成java字节码,这些字节码在程序运行的时候就会被jvm加载到内存中并通过元数据区管理和执行,此时,当前程序要如何执行,要做那些事情,就会按照上述元数据区里记录的字节码依次执行了。

注意:堆和元数据区在jvm里面只有一份,而栈和程序计数器可能有n份(这里的n和线程有关,就是一个线程都有自己独立的栈区和程序计数器).
JVM把.class文件加载到内存之后,就会把这里的信息使用对象来表示,此时这样的对象就是类对象.class对象,java文件中涉及到的信息都会在.class中有所体现,但是注释是不会包含的.

元数据:是计算机中的一个常见术语(meta data)往往指的是一些辅助性质的,描述性质的属性.
例如:文件数据,硬盘上不仅仅要存文件的数据本体,还需要存储一些辅助信息比如文件的大小,文件的位置,文件的拥有者,文件的修改时间,文件的权限信息"这些属性统称为元数据.


JVM执行方式

主要有两种执行字节码的方式。
解释执行,字节码加载:类加载器将字节码从 .class 文件加载到元数据区,并生成对应的 Class 对象(存储在堆中),按照字节码指令集的定义,逐行解释字节码并执行相应的操作,这个过程并不需要把字节码转化为二进制指令交给操作系统执行。就像是有一个翻译官,一边看字节码(“外语指令”),一边指挥计算机做事。
即时编译(JIT,Just - In - Time),JVM会在运行时将热点代码(频繁执行的代码部分)的字节码编译成机器码(二进制指令)。这样在下次执行这些代码时,就可以直接执行二进制指令,速度更快,这就相当于把一些常用的“外语句子”提前翻译好,下次就可以直接用了,不用再慢慢翻译。
jvm不需要把字节码编译成二进制指令执行,只需要jvm知道这个字节码中某条指令的含义就能在他的运行环境里面执行,字节码文件里包含了一系列按照字节码指令集规范编写的指令。
 

JVM的类加载机制

java进程运行的时候,需要把.class文件从硬盘,读取到(加载)内存jvm中,并进行一系列的校验解析的,将其转换为 JVM 可用的数据结构的过程,或者简要说就是把把.class文件从硬盘读取到jvm内存.并转换成jvm可以使用的类对象

类加载过程是Java程序运行的重要起始环节,它主要包含以下五个紧密相连的阶段:

1. 加载(Loading):加载是类加载机制的第一步,其核心任务是通过类的全限定名(Fully Qualified Name)获取定义此类的二进制字节流。例如,对于  com.example.MyClass  这样的类,类加载器会根据这个全限定名去查找对应的字节流。字节流的来源多种多样,既可以从本地文件系统读取  .class  文件,也可以从网络、数据库,甚至内存中的字节数组获取。在Java Web应用中,Servlet类的字节流通常由Web容器从部署的WAR文件中读取。

获取字节流后,类加载器会将其代表的静态存储结构转换为方法区中的运行时数据结构。类文件的二进制字节流遵循特定的存储格式,包括以下关键部分:

魔数(Magic Number) 用于识别是否为有效的Class文件
版本号 标识Class文件的版本信息
常量池 存储字面量(如字符串常量、基本类型常量)和符号引用(如类和接口的全限定名、字段和方法的名称及描述符等)
访问标志 描述类或接口的访问权限和属性(如  public 、 final  等)
类索引、父类索引、接口索引集合 定义类的继承关系和实现的接口
字段表集合 描述类中声明的字段
方法表集合 描述类中声明的方法
属性表集合 包含类的额外信息(如源码文件名、注解等)

类加载器会将这些信息按照JVM运行时数据结构的要求进行转换,并存储到方法区中。例如,类文件中的常量池信息会被转换并存储到运行时常量池中,其中的字面量和符号引用都会得到妥善处理。完成上述步骤后,JVM会在堆内存中创建一个  java.lang.Class  对象。这个对象作为方法区的访问入口,就像一把“钥匙”,通过它可以访问方法区中关于这个类的各种数据结构。

2. 验证(Verification):该阶段旨在确保被加载类的字节码文件符合Java虚拟机规范,具体包含文件格式验证(检查字节码是否以正确魔数开头、版本号是否正确等)、元数据验证(检查类是否有正确父类、是否实现了抽象方法等)、字节码验证(检查指令是否合法、操作数类型是否正确等)以及符号引用验证(检查类之间的引用是否正确),通过层层验证保障类的安全性与合法性。
3. 准备(Preparation):此阶段主要为类变量(静态变量)分配内存并设置初始值,这里的初始值是数据类型对应的零值,例如 int 类型的类变量初始值为0,引用类型的类变量初始值为 null  。需注意,此时不会执行Java代码中的初始化语句,仅仅完成内存分配和设置零值。
4. 解析(Resolution):这是将常量池中的符号引用转换为直接引用的过程。符号引用是编译时使用的、不涉及内存地址的引用形式,而直接引用则是能直接指向目标的内存地址或者相对偏移量的引用。比如在一个类中引用另一个类的方法,编译时只是符号引用,解析阶段会将其转换为实际的内存地址或者偏移量,以便运行时能直接调用该方法。
5. 初始化(Initialization):作为类加载的最后一步,主要执行类构造器 () 方法。这个方法由编译器自动收集类中的所有静态变量赋值语句和静态语句块( static{} )合并生成。在此阶段,类变量会被赋予正确的值,静态语句块中的代码也会按顺序执行,完成类的全面初始化,至此类已准备就绪,可以被使用。

值得注意的是,初始化阶段的触发有着明确的条件:
- 当程序尝试创建类的实例时,会触发类的初始化。例如使用 new 关键字实例化对象。
- 若对类的静态变量进行访问,或者调用类的静态方法,同样会促使类进入初始化阶段。
- 通过反射机制调用类时,也会触发类的初始化操作。
- 当子类进行初始化,如果此时其父类尚未被初始化,那么父类会先被初始化,紧接着再进行子类的初始化。
此外,方法区是JVM中的一个抽象概念,而自Java 8以后,元数据区成为了方法区的具体实现,其与类、接口有着紧密联系 ,在类加载以及Java程序运行过程中发挥着关键作用。

类加载器

在JVM架构中,类加载器扮演着双重角色:它既是JVM的核心组件之一,也是类加载阶段的具体执行工具。类加载器的核心任务是根据“全限定类名”(带有包名的类名)查找对应的 .class 文件,并将其加载到JVM内存中。

类加载器的类型

JVM中有三种内置的类加载器,它们之间通过“父子关系”形成层次结构(每个类加载器都有一个引用指向其父类加载器):

1. 启动类加载器(Bootstrap ClassLoader):位于类加载器层次的最顶层,负责加载Java核心类库(如 java.lang.Object 等),由C++实现,是JVM的一部分,没有对应的Java类。
2. 扩展类加载器(Extension ClassLoader):负责加载JDK扩展目录( jre/lib/ext )中的类库。
3. 应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载当前项目的代码目录以及第三方库中的类。

这三个类加载器之间存在着“父子关系”,每个类加载器都持有一个指向自己父类加载器的引用。这种父子关系构成了一种有序的类加载协作模式,即双亲委派模型(Parent - Delegation Model)。

双亲委派模型(Parent-Delegation Model)描述了类加载器之间的协作机制,其工作流程如下:

1. 委派机制:

当一个类加载器收到类加载请求时,它首先会将请求委派给其父类加载器。如果父类加载器无法加载该类(通常是因为找不到对应的字节码文件),子类加载器才会尝试加载。
2. 工作流程:

从 ApplicationClassLoader 开始,它不会立即搜索自己负责的目录,而是将任务委派给其父类 ExtensionClassLoader 。
ExtensionClassLoader 同样将任务委派给其父类 BootstrapClassLoader 。
 BootstrapClassLoader 发现自己没有父类,于是尝试在标准库目录中查找 .class 文件。如果找到,则直接加载;否则,将任务返回给 ExtensionClassLoader 。ExtensionClassLoader 在扩展库目录中查找,如果找到则加载,否则返回给 ApplicationClassLoader 。ApplicationClassLoader 在项目目录或第三方库目录中查找。如果找到则加载,否则抛出 ClassNotFoundException 异常。
3. 优势:确保Java核心类库的优先加载,避免自定义类覆盖核心类库中的类。
增强了安全性,防止恶意代码篡改核心类库。

自定义类加载器

JVM内置的类加载器遵循双亲委派模型,但开发者可以自定义类加载器来打破这一规则。例如,可以创建一个独立的类加载器,指定其在特定目录中加载类,而不与现有的类加载器形成父子关系。这种方式适用于某些特殊场景,如热部署、模块化加载等。

假设在代码中自定义了一个 java.lang.String 类,由于双亲委派模型的存在,JVM会优先加载核心类库中的 String 类,而不是自定义的类。这有效避免了类名冲突导致的标准库功能失效问题。

垃圾回收

垃圾回收(Garbage Collection, GC):是JVM自动管理内存的核心机制,它不需要开发者手动释放内存。JVM会自动判定内存是否继续使用,如果某块内存不再被使用,就会自动释放。垃圾回收的主要目标是回收堆内存中的对象,但也会涉及方法区(元数据区)的类卸载。

垃圾回收的问题

STW(Stop The World)问题:在触发垃圾回收时,JVM可能会暂停所有应用程序线程,这种现象称为 STW(Stop The World)。虽然STW问题目前无法完全避免,但现代JVM已经能够将STW的时间控制在1毫秒以内。对于服务器请求/响应处理时间通常在几毫秒到几十毫秒的场景下,这额外的1毫秒对性能影响较小。

垃圾回收的范围

1. 程序计数器:不需要垃圾回收。程序计数器是线程私有的,线程创建时分配,线程销毁时自动回收。
2. 栈:栈中的局部变量在代码块执行结束后自动销毁,无需垃圾回收。
3. 元数据区/方法区:主要涉及类加载,类卸载较少。但如果一个类的元数据对象没有被其他代码或对象引用,也可能被回收。
4. 堆:垃圾回收的主要目标是堆内存中的对象。

垃圾回收机制

垃圾回收的核心是 回收对象,每次垃圾回收都会释放若干个不再使用的对象。判定对象是否为垃圾的依据是 引用。在Java中,使用对象必须通过引用的方式(匿名对象除外)。如果一个对象没有任何引用指向它,就视为无法被代码使用,可以被回收。

 
public void func() {
    Test t = new Test(); // 创建对象
} // 方法执行完毕,局部变量 t 被释放,new Test() 创建的对象无引用指向,成为垃圾

如果多个引用指向同一个对象,需要确保所有引用都销毁后,对象才能被视为垃圾。如果这些引用的生命周期不同,垃圾回收的判定会变得复杂。

垃圾回收算法

1..引用计数:在Java虚拟机(JVM)中,并未采用这种垃圾回收机制。引用计数的原理是为每个对象额外分配一块空间,用于存储当前指向该对象的引用数量。每当有一个变量存储了该对象的引用地址时,引用计数就会增加1;而当该对象被赋值为null、被销毁或者变量被重新赋值为指向其他对象的引用时,引用计数则会减1。一旦某个对象的引用计数变为0,就意味着该对象不再被任何变量引用,此时便可以对其进行内存释放,并且会立即触发垃圾回收机制。
 引用计数机制,从原理上看是一种简单且直观有效的垃圾回收策略。

然而,它存在两个较为关键的问题:其一,会消耗额外的内存空间。由于需要为每个对象都设置一个计数器,如果按照每个计数器占用两个字节来计算,当对象数量庞大时,所消耗的总内存空间将会非常可观。特别是当每个对象本身占用的内存较小,比如假设每个对象仅占用4个字节时,计数器所消耗的空间就已经达到了对象自身空间的一半,这无疑会对内存的使用效率产生较大影响。

问题2:引用计数可能会产生"循环引用的问题"此时,引用计数就无法正确工作.

2.可达性分析可达性分析是Java虚拟机(JVM)中一种重要的垃圾检测算法,其本质是采用“时间换空间”的策略。

与引用计数算法相比,可达性分析虽然需要消耗更多的额外时间,但总体上其时间开销是可控的,并且能够有效避免诸如“循环引用”这类在引用计数算法中可能出现的问题。

可达性分析以一些被称作“GC Roots”的对象作为起始点。这些“GC Roots”涵盖了多个方面,例如虚拟机栈中引用的对象(即当前线程中各个栈帧所引用的对象)、方法区中类静态属性引用的对象(如类的静态成员变量所指向的对象)、方法区中常量引用的对象(像被声明为 final 的常量所引用的对象)以及本地方法栈中JNI(Java Native Interface)引用的对象等。

从这些“GC Roots”对象出发,算法会尝试进行“遍历”操作。这里的“遍历”指的是沿着变量中持有的引用类型成员,逐层深入地访问对象。也就是说,从一个对象的引用开始,访问该对象内部的引用类型成员所指向的其他对象,并以此类推。在这个过程中,所有能够被访问到的对象,都被认定为是“存活”的,即不是垃圾;而经过一轮遍历后仍然无法被访问到的对象,则被判定为垃圾对象。

JVM中存在专门的扫描线程,这些线程会持续不断地对代码中已有的变量进行遍历,尽可能多地访问到各个对象。由于JVM自身能够知晓系统中存在的所有对象,通过可达性分析的遍历过程,将所有可达的对象标记出来后,剩下未被标记的对象自然就属于不可达的垃圾对象,等待后续的垃圾回收处理。

释放垃圾内存空间

在JVM中垃圾回收过程中,释放被标记为垃圾的对象所占用的内存空间,主要有以下四种方式:

1.标记清楚这是一种最为朴素的方法,即直接释放被标记为垃圾的对象所对应的内存空间。然而,这种方案在实际应用中很少被采用,原因在于它存在一个极为致命的问题——内存碎片

内存碎片,指的是内存中存在的一些不连续的小空闲区域。由于这些空闲区域不连续,它们无法满足一些较大的内存分配请求。当采用标记清除算法时,垃圾对象被释放后,会产生许多离散的小空闲内存空间,这极有可能导致后续的内存申请失败。因为内存申请通常要求获得一块连续的空间,例如,若要申请1M的内存空间,就需要这1M字节的内存是连续的。即便总的空闲空间远远超过1M,但如果不存在大于1M的连续空间,内存申请便会失败。

2.复制算法该算法的核心策略是将总体内存划分为两部分,使用其中一半,预留另一半。在垃圾回收时,并非直接释放内存,而是将那些仍在使用(未被标记为垃圾)的对象复制到预留的另一半内存空间中,然后将原来使用的这一半空间整体释放掉。

这种算法存在明显的缺点:其一,由于内存被平均分成两部分,导致总的可用内存减少了一半,这在一定程度上降低了系统的内存使用效率。其二,如果每次需要复制的对象数量较多,那么复制操作所带来的开销将会非常大。不过,如果在一轮垃圾回收(GC)过程中,大部分对象都可被释放,仅有少数对象存活,那么复制算法就比较适用。

3.标记-整理此算法同样能够有效解决内存碎片问题。其原理类似于在顺序表中删除中间元素的过程,即当某个对象被标记为垃圾并删除后,后续的对象会依次向前移动,填补被删除对象所留下的空间。然而,这种“搬运”操作会带来较大的开销,尤其是当内存中的对象数量较多时,搬运所消耗的时间和资源都不容小觑。

4.分代回收该算法依据不同种类的对象,采取不同的回收方法,并引入了“对象年龄”的概念,对象初始年龄为0。每个对象都有一个“对象头”,其中有一个属性用于存储年龄。根据对象年龄,可将其分为新生代(年龄较小的对象)和老年代(年龄较大的对象)。内存空间又进一步分为三个区域:伊甸区、两个大小相等的生存区(也可称为幸存区)以及老年区。

具体过程如下:

 1). 当在代码中使用 new 关键字创建一个新对象时,该对象会被创建在伊甸区,因此伊甸区通常会存在大量对象。根据经验规律,伊甸区中的大部分对象生命周期都非常短,往往“朝生夕死”,难以存活过第一轮GC。
2). 第一轮GC扫描完成后,伊甸区中少数幸存的对象会通过复制算法被拷贝到其中一个生存区。后续,GC扫描线程会持续进行扫描,不仅会扫描伊甸区,还会扫描生存区中的对象。在扫描过程中,生存区中的大部分对象会被标记为垃圾,仅有少数对象存活。这些存活的对象会继续通过复制算法被拷贝到另一个生存区。每经历一轮GC扫描,对象的年龄就会增加1。
3). 如果某个对象在生存区中经过了若干轮GC后依然存活,JVM会认为该对象的生命周期大概率较长,此时会将这个对象从生存区拷贝到老年代。
4). 老年代中的对象同样需要接受GC扫描,但扫描的频次会大大降低。这是因为老年代中的对象生命周期较长,频繁扫描不仅意义不大,还会浪费时间。
5.) 当老年代中的对象被标记为垃圾时,JVM会采用标记整理的方式来释放其占用的内存空间。

你可能感兴趣的:(jvm,java,后端)