总结类加载的过程,双亲委派模型,JVM回收机制

前言(明确学习目的)

首先我们需要明白这里的知识点的定位是和多线程的进阶是类似的,详情可以参考我这篇博客JavaEE初阶----总结锁策略,CAS 和 synchronized 优化过程。都是属于八股文的部分。基本上主要用途是应对面试,在实际工作中,基本不会用到本章节学习的知识

JVM内存区域的划分

在进入JVM之前,先给大家简单介绍一下为什么现在面试中面试官很偏爱问JVM方面的知识。

其实,除非你的工作,就是开发JVM(一般是C++程序猿干的事情),否则一般的Java程序猿是不需要使用到JVM内部的东西的。
那么为啥我们还要学习?因为面试喜欢问!!
那么为啥工作中基本用不到的东西面试却老喜欢考?
在这里插入图片描述
最主要的原因还是因为这本书的横空出世~~,这本书非常的好,非常有深度,非常有干货,从此很多程序猿读了这本书,如获至宝!但是不得不承认,由于没有应用场景,大部分程序猿读这本书的时候,其实只是一知半解,进一步的,这本书中的很多东西,就被断章取义出来,逐渐成为了面试题(主要还是因为卷!!),以至于说这本书都成了“面试教材”

回归正题,那么JVM内存区域到底是如何划分的呢?
在这里插入图片描述
就比如我们租的小房子,一块区域需要根据功能,来划分出不同的小区域

JVM内存从操作系统这里申请来的区域,相当于租了一个房子,JVM就把这个场地也划分成不同的区域了

JVM主要有以下的区域划分:

程序计数器(线程私有)

内存中最小的区域,保存了CPU下一条要执行的指令的地址在哪里~~> 在这里插入图片描述

栈(线程私有)

主要是存放局部变量以及方法调用的信息,方法调用的时候,每次调用一个新的方法,就都涉及到“入栈”操作,每次执行完了一个方法,都涉及到“出栈”操作~~。这里其实还涉及到很多栈帧里面,数据如何排列的,也有一些规则,入栈,出栈具体实现,也有一些技巧和细节,这里我们就不详细介绍了。需要注意的是每个线程都有一个栈,栈的空间其实是比较小的,在JVM中可以配置栈空间的大小,但是一般也就是几M或者几十M,因此栈很有可能会满的。

堆(线程共享)

堆的话每个进程中只存在一份,多个线程共用一个堆~,也是内存中空间最大的区域,new出来的对象,就是在堆中,对象的成员变量自然也在堆中。

方法区(线程共享)

方法区中,放的是“类对象”。.class会被加载到内存中,也就被JVM构造成了类对象(加载的过程就称为“类加载”)这里的类对象,就是放到方法区中,类对象就描述了这个类长啥样~,类的名字是啥,里面有那些成员,那些方法,方法里包含的指令。。。
类对象里还有一个很重要的成员,静态成员~~
static修饰的成员,成为了类属性,而普通的成员,就叫做“实例属性”。

提一嘴:
上述讲的这个内存区域划分,不一定是符合实际情况的,JVM在实现的时候,具体怎么划分这个区域,不一定完全相同,不同厂商,不同版本的JVM实现可能会存在一些差异~

JVM类加载机制

类加载,其实是设计一个运行时环境的一个重要的核心功能,我们此处还是以应付面试为目的。重点去学习其中常见的面试题即可~

那么类加载是要干啥?

类加载主要是把.class文件,加载到内存中,构建成类对象~~
在这里插入图片描述

整个类加载,分成三个大的步骤!,建议大家面试的时候尽量回答相对应部分的英文

1)Loading环节

先找到对应的.class文件,然后打开并读取.class文件,同时生成一个类对象~~
Loading中的一个关键环节,.class文件到底里面是啥样的?
在这里插入图片描述
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,它和类加载 Class Loading 是不同的,一个是加载 Loading 另一个是类加载 Class Loading,所以不要把二者搞混了。
在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

2)LinKing连接

连接一般就是建立好多个实体之间的联系

2.1>Venification(验证)

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节 流中包含的信息符合《Java虚拟机 规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证选项:
文件格式验证
字节码验证
符号引用验证…

2.2>Preparation(准备)

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
比如此时有这样一行代码:
public static int value = 123;
它是初始化 value 的 int 值为 0,而非 123。

2.3>Resolution(解析)

解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。

这块不太好理解,主要是涉及到实现的细节了~~

.class文件中,常量是集中放置的,每个常量有一个编号
.class文件的结构体里初始的情况下只是记录了编号,这时候就需要根据编号找到对应的内容,填充到类对象中

3)Initializing初始化

初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程。

典型的面试题

在这里插入图片描述
在这里插入图片描述
大家可以花几分钟的时间思考一下这个问题

标准答案:
A的静态代码块
B的静态代码块
A的构造代码块
A的构造方法
B的构造代码块
B的构造方法
A的构造代码块
A的构造方法
B的构造代码块
B的构造方法
解析:
我们的程序先从main函数开始执行,main这里是Test方法,因此要执行main,就必须先加载Test,Test继承自B,要加载Test,就需要先加载B,B继承自A,要加载B,就必须先加载A,只要这个类被用到了,就要先加载这个类(实例化,调用方法,调用静态方法,被继承…,都算被用到),要想构造Test,就得先构造B,要想构造B,就得先构造A,对于A来说,构造的过程=构造代码块的执行+构造方法的执行~

在这里插入图片描述

双亲委派模型

双亲委派模型,描述的就是JVM中的类加载器,如何根据类的全限定(java.lang.String)找到.class文件的过程~~

JVM里提供了专门的对象,叫做类加载器。负责进行类加载,当然找文件的过程也是类加载器来负责的~~
.class文件,可能放置的位置有很多,有的是放的JDK目录里,有的放在项目的目录里,还有的在其他特定位置
在这里插入图片描述
双亲委派模型,就描述了这个找目录过程,也就是上述类加载器如何配合的
在这里插入图片描述
在这里插入图片描述

双亲委派模型的优点

  1. 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了。
  2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自己提供的因此安全性就不能得到保证了。

了解了双亲委派模型之后,那么为啥JVM要这么设计呢?

理由就是,一旦程序猿自己写的类和标准库中的类,全限定类名重复了,也就能够顺利的加载到标准库中的类!!!

总结一下:
双亲委派模型,只是JVM实现中一个小小的规则和细节,只不过说这个东西有个好名字,于是才火了~~类似的规则和细节,在JVM中可以说是非常非常多。

JVM的垃圾回收机制GC(重点)

上面讲了Java运行时内存的各个区域。对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。因此我们本篇博客所讲的有关内存分配和回收关注的为Java堆与方法区这两个区域。
在这里插入图片描述

Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有如下几种算法

内存 VS 对象

在 Java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此我们将内存回收,也可以叫做死亡对象的回收。

死亡对象的判断算法(找垃圾/判断垃圾)

找垃圾,核心就是确认这个对象未来是否还会使用,什么算不使用了?没有引用指向,就不使用了。

1>引用计数算法

引用计数描述的算法为:给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。
在这里插入图片描述
在这里插入图片描述

引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采用引用计数法进行内存管理。但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题
那么我们先来看看循环引用的问题:在这里插入图片描述
在这里插入图片描述
所以像Python,PHP里进行GC也不只考引用计数,还依赖其他机制的配合~

2>可达性分析算法【这个是Java采取的方案】

通过额外的线程,定期的针对整个内存空间的对象进行扫描,有一些起始位置(称为GCRoots)会类似于深度优先遍历一样,把可以访问到的对象都标记一遍(带有标记的对象就是可达的对象),没被标记的对象,就是不可达对象,也就是垃圾。
在Java语言中,可作为GC Roots的对象包含下面几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象;
2.方法区中类静态属性引用的对象;
3.方法区中常量引用的对象;
4.本地方法栈中 JNI(Native方法)引用的对象。

给大家举个例子
在这里插入图片描述

GC在进行可达性分析的时候,当GC扫描到a的时候,就会把a能访问的所有元素都去访问一遍,并且进行标记~~但是如果代码中,c.right=null,则此时从a触发就访问不到f了,f就是不可达,f就是垃圾就应该要被回收掉。如果内存中的对象特别多,这个遍历就会很慢,GC还是比较消耗时间和系统资源的

可达性分析的优点:
克服了引用计数的两个缺点,空间利用率低,循环引用~
自身的缺点:系统开销大,遍历一次可能比较慢

垃圾回收算法(回收垃圾)

明确了谁是垃圾之后,接下来就要回收垃圾了~~
通过上面的学习我们可以将死亡对象标记出来了,标记出来之后我们就可以进行垃圾回收操作了,在学习垃圾收集器之前,我们先看下垃圾回收机器使用的几种算法(这些算法是垃圾收集器的指导思想)。
回收垃圾(释放内存)三种基本策略

1>标记-清除算法

"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 :
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路并对其不足加以改进而已。
"标记-清除"算法的不足主要有两个 :
1.效率问题 : 标记和清除这两个过程的效率都不高
2.空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集
在这里插入图片描述

2>复制算法

"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使 用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配 时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运 行高效。算法的执行流程如下图
在这里插入图片描述

复制算法的问题:
1.内存空间利用率低
2.如果要保留的对象多,要释放的对象少,此时复制的开销就很大

3>标记-整理算法

类似于顺序表删除中间元素,有一个搬运的操作~~
在这里插入图片描述
这个方案空间利用率是高了,但是仍然没有解决复制/搬运元素带来的开销大的问题

4>分代算法

上述的方案,虽然能解决问题,但是都有缺陷,实际上JVM中的实现,会把多种方案结合起来使用,这种思路我们称为“分代回收”
分代算法和上面讲的 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收。这就好比中国的一国两制方针一样,对于不同的情况和地域设置更符合当地的规则,从而实现更好的管理,这就时分代算法的设计思想

当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

分代回收,是针对对象进行分类(根据对象的“年龄”分类)一个对象熬过一轮GC的扫描,就称为“长了一岁”,针对不同年龄的对象,采取不同的方案
在这里插入图片描述
1.刚创建出来的对象,就放在伊甸区~~
2.如果伊甸区的对象熬过一轮GC扫描,就会被拷贝到幸存区(复制算法),大部分对象都是“朝生暮死”真正熬过一轮GC的对象,并不多。
3.在后续的几轮GC中,幸存区的对象就在两个幸存区之间来回拷贝(复制算法),每一轮都会淘汰掉一波幸存者
4.在持续若干轮之后,对象终于“多年媳妇熬成婆”,进入老年代~~
老年代有个特点:里面的对象都是比较年纪大的。
基本规律:一个对象越老,继续存活的可能性就越大
因此老年代的GC扫描频率大大低于新生代~~

其实整个分代回收的过程,就像我们找工作的一个过程
1.先投简历(进入伊甸区)
2.筛选简历(伊甸区->幸存区)大部分人会被淘汰掉。
3.通过简历筛选之后,接下来会有几轮笔试和面试
4.通过重重筛选,拿到offer就可以入职了~~相当于进入老年代
5.进入公司,公司也有末位淘汰,如果干的不好,也会被辞退~只不过公司考核周期就比较长了,频率大大降低了

分代回收中,还有一种特殊情况,有一类对象可以直接进入老年代(大对象,占有内存多的对象),大对象拷贝开销比较大,不适合复制算法。

我们找工作要通过多轮筛选才能进入公司,有的人进入公司只需要打一声招呼就行了~~(公司是他爸开的…)

垃圾收集器(了解)

如果说上面我们讲的收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值