【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的频率

相关推荐
乔木剑衣2 分钟前
JVM学习:CMS和G1收集器浅析
java·jvm·学习·垃圾收集
找了一圈尾巴1 小时前
Wend看源码-Java-Collections 工具集学习
java·开发语言·学习
广而不精zhu小白3 小时前
CentOS Stream 9 安装 JDK
java·linux·centos
程序员云帆哥4 小时前
【玩转23种Java设计模式】行为型模式篇:命令模式
java·设计模式·命令模式
赵谨言4 小时前
基于 Java 大数据的旅游推荐系统的设计与实现
java·经验分享·毕业设计
NHuan^_^5 小时前
RabbitMQ基础篇之Java客户端 Topic交换机
java·rabbitmq·java-rabbitmq
中國移动丶移不动5 小时前
Java List 源码解析——从基础到深度剖析
java·后端·list
东阳马生架构5 小时前
JVM实战—8.如何分析jstat统计来定位GC
jvm
Java知识技术分享6 小时前
spring boot 异步线程池的使用
java·spring boot·spring
m0_748232926 小时前
Springboot3.x整合swagger3
java