【JVM】深入了解Java虚拟机-------内存划分、类加载机制、垃圾回收机制

目录

什么是JVM?

内存划分:

[1.堆 (共享)](#1.堆 (共享))

[2.栈 (私有)](#2.栈 (私有))

3.元数据区(共享)

4.程序计数器(私有)

示例:

[JVM 类加载](#JVM 类加载)

一.类加载过程

1.加载

2.验证

3.准备

4.解析

5.初始化

二.双亲委派模型

如何寻找文件?

双亲委派模型主要是为了应付以下场景:

是否可以打破双亲委派模型呢?

JVM垃圾回收机制

什么是垃圾回收(GC)?

谁是垃圾?

1.引用计数

2.可达性分析

释放垃圾策略:

1.标记-清除算法

2.复制算法

3.标记-整理算法

4.分代回收


什么是JVM?

JVM(Java Virtual Machine,Java虚拟机) 是一种虚拟化的计算机,允许在其上执行 Java 程序(以及其他由 Java 编译器编译成字节码的程序)。JVM 提供了一种跨平台的执行环境,使得开发者可以编写一次代码,并在任何安装了 JVM 的设备或操作系统上运行。这种跨平台能力通常被称为"一次编写,到处运行"的特性。


内存划分:

JVM就是Java进程,当进程一旦跑起来之后,就会从操作系统里面申请一大块内存空间

JVM就是要将这块空间进行划分成不同的区域,并且每个区域都有不同的功能作用

如下图所示:分为五块不同区间

1.堆 (共享)

堆是整个内存区域中最大的一块,放的内容就是代码new出来的对象

堆里面分为两个区域:新生代和老生代,在讲到后面GC垃圾回收机制会提到....

2.栈 (私有)

分为Java虚拟机栈和本地方法栈

Java虚拟机栈存储的是保存了方法调用关系

本地方法栈存储的是本地方法的调用关系

3.元数据区(共享)

元数据区放的是"类数据"

java 复制代码
class Test {
.....
}

Test.class就是类对象

还存放了一些方法相关的信息

例如:类有一些方法中,每个方法都代表了一系列的"指令集合"(JVM字节码指令)

还有常量池也存放在元数据区中,

4.程序计数器(私有)

程序计数器是内存区域里面最小的一块区域,它只需要保存当前要执行的下一条指令(JVM字节码)的地址


在上述四块区域中,堆和元数据区的数据是在整个进程只有一份,每个线程都可以访问到,是大家共同可以使用的;而栈和程序计数器是每个线程中有一份,是只能每个线程跟每个线程独立的,不共同使用。

示例:

java 复制代码
class Test {
    int a;
    Test2 t2 = new Test2();
    String s = "hi";
    
    static int b;

}
public static void main() {
    Test t = new Test();
}

a,t2,s,b t 都存放在哪个区域中呢?

a存放在堆

t2存储在堆 new Test2()存在在堆 并且t2指向它

s 存放在堆 但"hi"存放在元数据上 s执行它

b 是静态变量 存放在元数据上

t 是局部变量存放在栈上 但它保存了对象的首地址(在堆上)

基本判断方法:
1)局部变量在栈上

2)成员变量在堆上

3)静态变量或方法都在元数据上


JVM 类加载

一.类加载过程

一个java进程启动,要将.java文件转化成.class文件,加载到内存中,才能得到'类对象'

类加载过程有以下几个环节:

1.加载

在硬盘中,找对对应的.class文件,读取文件里面的内容

2.验证

检查.class文件里的内容,看看是否符合要求JVM的规范要求,保证这些信息运行后不会危害到JVM的自身的安全;

3.准备

给类对象分配内存(元数据区),并设置类变量的初始值,如果类里有静态变量,那么值为0 /false /null 。。。

4.解析

针对字符串常量进行初始化,把刚才.class文件里面的常量的内容放到元数据区里

5.初始化

针对类对象进行初始化(不是对对象初始化,和构造方法无关),给静态成员进行初始化,执行静态代码块

这样类对象就加载完成,后续代码可以使用这个类对象,创建实例,或者使用里面的静态成员了...


二.双亲委派模型

这个模型描述了JVM加载.class文件过程中,找文件的过程;

这个类加载中的 "双亲委派模型 "出现在"加载"那个环节,根据代码中写的"全限定类名"找到对应的.class文件;
全限定类名:包名+类名 比如String -> Java.lang.String List -> java.util.List;

如何寻找文件?

JVM内置了三个类加载器 负责加载不同的类

1.启动类加载器 BookstrapClassLoader 爷爷

负责加载标准库的类

2.扩展类加载器 ExtensionClassLoader 爸爸

负责加载JVM扩展库的类

3. 应用类加载器 ApplicationClassLoader 儿子

负责加载第三方库的类 和自己代码写的类

这里标注了它们之间的关系并不是类的继承之间的关系,而是通过类加载器中存在一个parent字段 可以指向他们 类似树 所有它也可以称做"父亲委派模型"/"单亲委派模型"

当子类加载器需要加载类时,它会首先将请求委托给父类加载器,直到请求最终被顶级加载器(即 BookstrapClassLoader)处理。只有在父类加载器无法处理该请求时,子类加载器才会尝试加载该类。此时,如果儿子类加载器也没有找到,最后就会抛出ClassNotFoundException

双亲委派模型主要是为了应付以下场景:

当你自己的代码中写的类,类的名称和标准库/扩展库的类发生了冲突,JVM会确保加载的类是标准库上的类,就不加载自己写的类了,如果标准库的类不能加载,那么可能整个Java进程都没法加载了;

是否 可以打破双亲委派模型呢?

答案当然是可以

自己写个类加载就行。。。


JVM垃圾回收机制

什么是垃圾回收(GC)?

垃圾回收机制,是Java提供对于内存自动回收的机制;

JVM(Java虚拟机)的垃圾回收(Garbage Collection,GC)机制是 Java 运行时管理内存的重要部分。其主要目的是自动化地回收那些不再被使用的对象,从而释放内存,避免内存泄漏,提高系统的稳定性和效率。JVM 的垃圾回收机制有一套复杂的算法和流程,但也让 Java 开发者无需手动管理内存。


在GC中,最主要回收的堆上的内存

1)程序计数器:不需要额外回收,线程销毁,自然就没了

2)栈:同理,线程销毁,也就没了

3)元数据区:一般也不需要,都是加载类,不需要卸载类

4)堆: 存放的都是对象实例 GC的主力部分

GC的主要流程,找到谁是垃圾(不再使用的对象),并且给它回收。

谁是垃圾?

在一个对象中,创建的时间往往可以确定,但什么时候不再使用,却是很模糊的。在编程中,一定要确保每个对象都是有效的,可不敢提前释放;

因此,判断一个对象是否需要回收,采取的策略都是比较保守的

Test t = new Test();

使用对象,是通过引用的方式来使用的,如果没有引用这个对象,那么这个对象将在代码中肯定不能使用。

如果 t = null;

new Test() 这个对象没有引用指向了,那么它就可以是作为垃圾;

具体是怎么判断对象是否有无引用,以下会介绍俩种方法。

1.引用计数

这个不是JVM使用的方案,而是Python/PHP等语言使用的方案

java 复制代码
Test a = new Test();
b = a;
java 复制代码
b = null;
a = null;

给对象加个引用计数器,当有地方指向它,那么计数器+1,如果引用失效时,那么计数器-1;

如果计数器为0,那么这个对象将会被当做垃圾被回收

但是有以下几个问题:

1. 消耗额外的空间,如果对象非常大还好,浪费的空间可以接受,但是你对象很小,还要额外开销内存,就会浪费很多空间;

2.存在"循环引用"的问题;

java 复制代码
class Test {
    Test t;
}

Test a = new Test();
Test b = new Test();
a.t = b;
b.t = a;

第一步:

第二步:

java 复制代码
a = null;
b = null;

此时,这俩对象互相执行对方,导致这俩个计数器都为1(但不为0,所有不是垃圾),但是外部代码也访问不到这对象;

Python/PHP它们如果检测到了循环引用就会提供了报错信息;

2.可达性分析

以一系列被称为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到任何一个 GC Roots 没有引用链相连时,则证明该对象是不可达的,是可以被回收的对象。

GC Roots 对象:包括虚拟机栈中引用的对象、本地方法栈中引用的对象、元数据区中类静态属性引用的对象、元数据区中常量引用的对象等。

举例:类似树形状

如果将 a.right = null;

那么 c 将会断开 ,外面将找不到c了 那个就可视为 c 和 f 都是垃圾了

由于可达性分析,需要消耗一定的时间,因此Java垃圾回收,没法做到"实时性",周期性进行扫描(JVM提供了一组专门的负责GC的线程,不停的进行扫描工作);


释放垃圾策略:

1.标记-清除算法

这种算法存在的问题是:

  • 碎片化:回收后的内存空间可能会分散,导致内存碎片化,降低内存使用效率。很难使用到连续大的内存。
  • 效率问题:标记和清理过程需要扫描所有对象,性能可能较差。

2.复制算法

将内存分为大小相等的两块,每次只使用其中的一块。

释放1 3 5 保留 2 4

当这一块的内存用完了,就将还存活的对象复制到另一块内存中,然后把使用过的这块内存空间一次清除掉。

这种算法的优点是避免了内存碎片化,但缺点是需要两倍于 Eden 区的内存空间来存储对象。

3.标记-整理算法

首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

类似顺序表删除中间元素

  • 优点:不会产生内存碎片,提高了内存的利用率;标记和压缩的过程相对来说效率较高。
  • 缺点:相对于标记 - 清除算法,增加了对象移动的开销,需要更新对象的引用地址等

4.分代回收

上述三个方案,只是铺垫,JVM中的实际方案,是综合上述的方案,更复杂的策略,分代回收(即分情况讨论,根据不同的场景/特点选择合适的方案)

新生代中的对象存活率较低,通常采用复制算法进行垃圾回收;老年代中的对象存活率较高,一般采用标记 - 清除算法或标记 - 整理算法进行垃圾回收。

新生代:又分为 Eden 区和两个 Survivor 区,比例一般为 8:1:1。

新创建的对象都在Eden区,当 Eden 区满了之后,会触发一次 GC,只有少部分对象可以活过第一轮GC

Eden区 -> Survivor区 通过复制算法(由于存活数量少,复制算法的开销也比较低,Survivor区的空间也不是很大)

Survivor区 ->另一个Survivor区 通过复制算法 每一轮GC下来,Survivor区都会淘汰一部分对象,剩下的通过复制算法进入下一个Survivor区,存活下来的对象年龄+1

Survivor区 -> 老年代 某些对象 经过多次GC 还没有变成垃圾 那么将会复制到老年代区

老年代区的对象也是需要进行GC的,但是老年代中的对象都是生命周期比较久的,因此可以减小GC的频率

相关推荐
李慕婉学姐5 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆6 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin7 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20057 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉7 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国7 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882487 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈8 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_998 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹8 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理