JVM类加载的过程和JVM垃圾回收机制

文章目录

  • 一、JVM类加载的过程
    • 1.1类加载的基本流程
      • 1.1.1加载
      • 1.1.2验证
      • 1.1.3准备
      • 1.1.4解析
      • 1.1.5初始化
    • 1.2双亲委派模型
  • 二、JVM垃圾回收机制
    • 2.1找到垃圾
      • 2.1.1引用计数(比如Python,PHP中用到)
      • 2.1.2可达性分析(比如Java中用到)
    • 2.2释放垃圾
      • 2.2.1标记清除
      • 2.2.2复制算法
      • 2.2.3标记整理
      • 2.2.4分代回收

一、JVM类加载的过程

1.1类加载的基本流程

Java代码会被编译成.class文件(里面包含了一些字节码),JVM会把.class文件读取到内存中并对其进行解析、构造类对象(这个过程叫类加载),类加载完成之后就会在内存中得到类对象,后续要构造这个类的实例都是基于类对象来进行展开的。

1.1.1加载

找到.class文件,打开文件,读取文件内容。从Java代码中往往会得到某个类的“全限定类名”(比如java.lang.String),JVM会根据这个“全限定类名”在一些指定的目录范围内去查找对应的.class文件,找到对应的.class文件就能够把这个.class文件打开并且读取里面的内容。

1.1.2验证

验证.class文件里的内容是否符合要求。
.class文件是二进制格式的文件,里面的某个字节都是有某些特定含义的。java标准文档:https://docs.oracle.com/javase/specs/index.html里说明了一个.class文件的格式是怎样的,.class文件里应该要包含哪些内容。

1.1.3准备

给类对象分配内存空间。这个内存空间的大小是根据上一步的验证的结果来确定的。这里只是分配内存空间,还没有初始化内存空间,此时这个内存空间上的数值全是0,此时如果打印类的static成员就会打印出0。

1.1.4解析

针对类对象中包含的字符串常量进行一些初始化操作。

java代码中用到的字符串常量在编译之后会进入到.class文件中。

比如java代码中有:final String a = “hello”;
编译之后,.class文件的二进制指令中也会有一个a这样的引用被创建出来,由于引用本质上保存的是一个变量的地址,在.class文件中,因为文件不涉及到内存地址,所以.class文件中的a就会先被设置成一个“文件偏移量”,通过这个“文件偏移量”可以找到hello这个字符串所在的位置,当我们把这个类真正加载到内存的时候,再把这个“文件偏移量”替换回真正的hello的内存地址。

在这里插入图片描述
如上图所示,假设在.class文件中,文件开头到hello开头的距离是100个字节,就称hello这个字符串在.class文件中的“文件偏移量”为100。文件开头到test开头的这100个字节里也会有一条指令,这条指令描述了String a = @100,这里的@100表示“文件偏移量”。当.class文件加载到内存中的时候,test这时的内存地址为0x12,String s = @100也会把@100这个“文件偏移量”替换成hello这个字符串真实的内存地址,这个替换的过程就是“解析”阶段要完成的主要工作。这个替换过程也叫把“符号引用”(“文件偏移量”)替换成“直接引用”(内存地址)。

1.1.5初始化

针对类对象进行初始化,即把类对象中的各个属性都设置好。
初始化好static成员。
执行静态代码块。
加载父类。

1.2双亲委派模型

双亲委派模型属于类加载的第一个步骤“加载”过程中的其中一个环节,即根据“全限定类名”找到.class文件。

JVM中内置了三个类加载器(程序员也可以手动创建出新的类加载器):
①BootStrap ClassLoader
②Extension ClassLoader
③Application ClassLoader
这三个类加载器彼此之间存在一个父子关系,即Application ClassLoader是子、Extension ClassLoader是父、BootStrap ClassLoader是爷,这个父子关系不是继承,而是这几个类加载器里都有一个parent这样的属性,这个parent属性指向一个父“类加载器”。

类加载的第一个步骤“加载”过程中找.class文件的过程:
①给定一个类的全限定类名,比如java.lang.String。

②以Application ClassLoader作为入口根据全限定类名开始执行查找对应的.class文件的逻辑。

③Application ClassLoader不会立即扫描自己负责的目录(Application ClassLoader复责的目录是当前项目对应的目录和第三方库对应的目录),而是把查找的任务交给他的父亲Extension ClassLoader。

④Extension ClassLoader也不会立即扫描自己负责的目录(Extension ClassLoader负责的目录是JDK中的一些扩展库对应的目录(JDK厂商会在标准之外做一些扩展)),而是把查找的任务交给它的父亲BootStrap ClassLoader。

⑤BootStrap ClassLoader也不会立即扫描自己负责的目录(BootStrap ClassLoader负责的是标准库对应的目录),而是把查找的任务交给它的父亲,结果发现没有父亲,因此BootStrap ClassLoader只能扫描自己负责的目录,如果类是标准库中的类,那么在BootStrap ClassLoader这个类加载器中就能找到对应的.class文件,此时查找.class文件的过程就结束了。
如果类不是标准库中的类,则查找.class文件的任务就会交给孩子Extension ClassLoader去执行。

⑥Extension ClassLoader就会扫描自己负责的目录,如果找到对应的.class文件,则查找结束,就执行后续的类加载操作;如果没找到,则把任务交给孩子Application ClassLoader执行。

⑦Application ClassLoader就会扫描自己负责的目录,如果找到对应的.class文件,则查找结束,就执行后续的类加载操作;如果没找到,就会抛出ClassNotFoundException。

双亲委派模型的目的是为了维护类被加载的优先级。

二、JVM垃圾回收机制

Java中new一个对象,就是一次“动态内存申请”。
动态表示运行时(程序运行起来才能确定内存大小),静态表示编译时(编译时就能确定内存大小)。
编译时:int a[5],a数组占据多少内存,在编译过程中就能确定下来,一个int是4字节,5个int就是20字节。

在C语言中使用malloc申请的内存在使用完之后需要通过free来释放,在C++中使用new申请的内存需要通过delete来释放。

Java给出了垃圾回收机制(GC),让JVM自动把不再使用的内存回收掉。而不用手动回收内存,大大降低了程序员的心智负担。

局部变量的生命周期是跟随栈帧的生命周期走的,方法执行结束栈帧销毁,局部变量所对应的内存也就释放了。
静态成员变量的生命周期是整个程序的生命周期,是类对象中的一部分,类加载之后是不会卸载的,所以静态成员变量无需释放。
所以GC回收的是堆上的对象。

GC分为两个步骤:

2.1找到垃圾

有两种主流方案:

2.1.1引用计数(比如Python,PHP中用到)

new出来的对象单独安排一块空间来保存一个计数器,这个计数器用来进行引用计数,这个计数器描述了这个对象有几个引用在指向它。
比如:
{
Test t = new Test();
Test t2 = t;
}
出了{}之后,t和t2就被销毁了,引用计数就归0了。当对象的引用计数为0时,此时这个对象就可以视为垃圾了。

但Java没有使用引用计数,因为引用计数有两个缺陷:
①比较浪费内存。因为每个new出来的对象都要单独安排一个计数器来保存它的引用计数,计数器至少要占据两个字节的内存空间,如果对象很少或者对象很大这时影响不大;如果对象很小并且很多这时计数器占据的空间就不容忽视了,内存就被浪费了很多。
②循环问题。
比如:
class A {
public A t;
}
class Test {
public static void main(String[] args) {
A a = new A();
A b = new A();
a.t = b;
b.t = a;
a = null;
b = null;
}
}
在这里插入图片描述
此时a和b两个引用已经被销毁了,new出来的两个对象已经无法被其它代码访问到,但是它们的引用计数不为0,这时这两个对象是不能回收的,第一个对象引用了第二个对象,第二个对象引用了第一个对象。要想拿到第一个对象就要先拿到第二个对象,要想拿到第二个对象就要先拿到第一个对象,这构成了逻辑上的循环错误。

2.1.2可达性分析(比如Java中用到)

可达性分析本质上是时间换空间。有一个/一组线程周期性地扫描代码中的所有对象,从一些特定的对象出发,尽可能地进行遍历访问(比如类似于N叉树遍历),把所有能够被访问到的对象都标记成“可达”,不能被访问到的未被标记的对象就是垃圾了。
可达性分析开始遍历访问的起点对象有很多,比如:局部变量中引用的对象、常量池中引用的对象、方法区中类静态属性引用的对象……,这些起点对象统称为GCRoots。
可达性分析是周期性进行的,因为某个对象是否是垃圾是会随着代码的执行而发生改变的(比如这个对象现在不是垃圾,代码执行了一段时间之后就变成垃圾了)。所以可达性分析比较消耗系统资源,导致系统时间开销较大,相比之下引用计数通过计数器来衡量当前对象是否是垃圾,比较精准,时间开销比较小。

2.2释放垃圾

有三种基本思路:

2.2.1标记清除

把垃圾对象直接释放掉,但这个方案非常不好,因为这会产生很多的内存碎片。我们释放内存是为了让其它代码能够申请内存,而申请内存时我们申请到的都是连续的内存空间。如果使用标记清除使用了一段时间,那么内存中出现内存碎片的情况将会非常严重,导致内存申请变得十分困难。

2.2.2复制算法

把内存分成两份,一次只用其中的一半。通过复制的方式把有效的对象归类到另一半,再统一释放原来那一半的所有空间。
复制算法可以有效解决内存碎片问题,但这个方案也有缺点:
(a)内存要浪费一般,内存利用率低。
(b)如果有效的对象非常多,那么拷贝的开销就会很大。

2.2.3标记整理

这个方法既能够解决内存碎片的问题,又能够解决复制算法中内存利用率低的问题,但拷贝的开销和复制算法差不多。
标记整理类似于顺序表删除元素时的搬运操作。在内存空间中把有效的对象一个一个地往内存空间的前面搬运,然后把内存空间后面的空间回收掉。
在这里插入图片描述

2.2.4分代回收

JVM释放内存的方法,是上述三种基本思路的结合体,即分代回收。
把堆分成两部分,这两部分不是等分的。左边称为新生代,右边称为老年代。新生代中有一个幸存区和一个伊甸区,幸存区里等分为两部分。
在这里插入图片描述
①刚new出来的新的对象放在伊甸区,从对象诞生到可达性分析扫描开始,这个过程虽然时间不长(往往是毫秒~秒级别),但在这个时间里大部分对象都会成为垃圾,即大部分对象都活不过一轮GC。

②伊甸区中经过一轮GC后仍然可达的对象,就会通过复制算法被拷贝到幸存区。然后释放整个伊甸区的内存。由于伊甸区中幸存下来的可达对象并不多,复制开销不大,所以这里非常适合用复制算法。

③GC扫描线程也会扫描幸存区,然后把GC扫描到的可达对象通过复制算法拷贝到幸存区的另一半,然后释放掉幸存区原来那一半的内存。对于幸存区之间的拷贝,每一轮GC会拷贝多个对象、也会淘汰多个对象。

④当某个对象在幸存区中存活过很多轮GC扫描之后,JVM就认为这个对象在短时间内应该是不会成为垃圾的,就会把这个对象拷贝到老年代。

⑤进入老年代的对象也会被GC扫描,但老年代GC扫描的频率会比新生代GC扫描的频率低很多(这减少了GC扫描的开销)。老年代使用标记整理的方式对内存进行回收。

新生代使用复制算法进行垃圾回收,老年代使用标记整理进行垃圾回收。

分代回收是JVM中主要的垃圾回收思想方法。但是在垃圾回收器具体实现的时候,可能还会有一些调整和优化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/217706.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

jetson NX部署Yolov8

一,事情起因,由于需要对无人机机载识别算法进行更新,所以需要对yolov8算法进行部署到边缘端。 二,环境安装 安装虚拟环境管理工具,这个根据个人喜好。 我们需要选择能够在ARM架构上运行的conda,这里我们选择conda-forge 下载地址 安装即可 剩下的就是和conda 创建虚拟…

Jensen不等式

如果是正数,并且它们的和等于1,f是凸函数,那么: 也可表述为: 即x期望的凸函数值小于等于x凸函数值的期望

5、LED流水灯

LED流水灯 思路&#xff1a;每次LED灯熄灭后&#xff0c;下一个LED灯亮 #include <REGX52.H> #include <INTRINS.H>void Delay500ms() //12.000MHz {unsigned char i, j, k;_nop_();i 4;j 205;k 187;do{do{while (--k);} while (--j);} while (--i); }void m…

Leetcode—45.跳跃游戏II【中等】

2023每日刷题&#xff08;四十&#xff09; Leetcode—45.跳跃游戏II 贪心法思想 实现代码 #define MAX(a, b) (a > b ? (a) : (b))int jump(int* nums, int numsSize) {int start 0;int end 1;int ans 0;int maxStride 0;while(end < numsSize) {maxStride 0;fo…

【程序员的自我修养01】编译流程概述

绪论 大家好&#xff0c;欢迎来到【程序员的自我修养】专栏。正如其专栏名&#xff0c;本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度&#xff0c;我会尽自己最大的努力&#xff0c;争取…

Kotlin学习——流程控制,when,循环,range工具 kt里的equals if实现类似三元表达式的效果

Kotlin 是一门现代但已成熟的编程语言&#xff0c;旨在让开发人员更幸福快乐。 它简洁、安全、可与 Java 及其他语言互操作&#xff0c;并提供了多种方式在多个平台间复用代码&#xff0c;以实现高效编程。 https://play.kotlinlang.org/byExample/01_introduction/02_Functio…

acm是什么?你准备好去打了吗?

1.引言2.acm究竟是什么&#xff1f;3.acm的时间安排重点 赛制查询榜单网络赛的作用1.名额分配2.校内选拔 icpc省赛省赛选拔赛(校内) 4.acm该如何准备1.前期的算法积累1.Acwing 平台算法基础课 -y总业界良心。算法提高课 基本囊括了蓝桥杯的知识范畴算法进阶课&#xff08;选&am…

只考数据结构,计算机评级C+,成都信息工程大学考情分析

成都信息工程大学(C) 考研难度&#xff08;☆☆&#xff09; 内容&#xff1a;23考情概况&#xff08;拟录取和复试分析&#xff09;、院校概况、24专业目录、23复试详情、各专业考情分析、各科目考情分析。 正文1715字&#xff0c;预计阅读&#xff1a;3分钟 2023考情概况 …

2022年12月 Scratch(三级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 默认小猫角色和气球角色都是显示状态,小猫程序如下图所示,气球没有程序,点击绿旗,舞台上最终显示的效果是?( ) A:可能出现6个不同位置的小猫和6个小球 B:可能出现6个不同位…

2016年12月13日 Go生态洞察:2016年Go用户调查与企业问卷

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Java研学-异常

一 异常 异于正常状态的显示,即控制台显示结果与预期的不一致 // 抛出异常java.lang.ArrayIndexOutOfBoundsException int[] nums new int[2]; System.out.print(nums[2]);二 异常分类 1 检测性异常:有称为非运行时异常,一般是在编写代码的过程中编程软件直接报错 2 非检测…

linux rpm安装软件卸载 以卸载mysql为例

查看rpm包 rpm -qa | grep 内容 卸载rpm rpm -e --nodeps rpm名称