类加载
- 类加载大致可以分为5个步骤
- 1、加载 :把硬盘上的
.class文件,找到并打开,读取到文件里面的内容(这里认为读到的是二进制数据) - 2、验证 :当前需要确保读到的文件内容是合法的
.class文件格式,判断是否符合JVM规范要求 - 3、准备:给类对象、申请内存空间
- 4、解析:主要是针对类中的字符串常量进行处理
- 5、初始化:针对类对象完成后续的初始化(静态代码块、父子类加载)
双亲委派模型
- JVM中进行类加载的操作,是有一个专门的模块,称为
类加载器(ClassLoader) - JVM中的类加载器默认是有三个的,支持自定义
- 类加载器的作用,就是给定一个
全限定类名(带有包名的类名),例如java.lang.String - 默认的分别是以下三个
BootstrapClassLoader:负责查找标准库的目录ExtensionClassLoader:负责查找扩展库的目录ApplicationClassLoader:复杂查找当前项目 /第三方库的目录- 上述的三个类加载器,存在"父子关系"

- 双亲委派模型,就描述了上述的类加载器们之间说如何配合工作的。虽然叫双亲,但是实际上类加载器之间,每个类只有一个父亲
- *双亲委派模型的工作过程:
- 1、从ApplicationClassLoader作为入口,先开始工作
- 2、ApplicationClassLoader不会立刻开始搜索自己的目录,而是会把要自己要搜索的任务给自己的父亲
- 3、代码进入ExtensionClassLoader,也不会立刻搜索自己的目录,也是要把搜索的任务交给自己的父亲
- 4、代码进入到了BootstrapClassLoader,也不想立即搜索自己负责的目录,也要把搜索的任务交给自己的父亲
- 5、此时BootstrapClassLoader发现自己往上没有父亲了,才会真正搜索负责的目录(标准库目录),通过全限定类名,尝试在标准库目录中找到符合要求的.class文件。如果
找到了,接下来就直接进入到打开文件/读文件等流程中。如果没找到,回到孩子这一辈的类加载器中,继续尝试加载 - 6、ExtensionClassLoader收到父亲交回给他的任务之后,自己进行搜索负责目录(扩展库的目录)。一样的如果找到了、进入流程。如果没找到,回到自己的孩子类加载器
- 7、ApplicationClassLoader收到父亲交回给他的任务之后,自己进行搜索负责目录(当前项目/第三方库目录)。如果找到了,进入流程。如果没找到默认情况下已经没有孩子了,此时说明类加载过程失败了,抛出异常
ClassNotFoundException

- 上述设定的最主要目的就是为了确保,这几个类加载器之间的优先级
比如:按照上述的顺序,假定在代码中自己定义了一个java.lang.String这样的类,最终程序的执行效果,自己定义的类把会被JVM加载,而是加载标准库中的String- 这样可以有效避免自己写的类、不小心和标准库的类名字重复,导致标准库的类功能失效
- ⚠️
但是上述的一系列规则,只是JVM自带的类加载器,遵守的默认规则,如果自己写类加载器,也可以打破上述规则
垃圾回收机制 GC
- 在C语言中,会涉及动态内存管理,
malloc申请内存,free释放内存。此处申请到的内存,生命周期是跟随整个进程的,这一点对于服务器程序非常不友好。当服务器每个请求都去malloc一块内存,如果不free释放掉,申请的内存越来越多,后续要想申请内存就没得申请了。这种情况我们称为内存泄漏问题 - 实际上,free很容易出现忘记调用,或者因为某些原因没有执行到
- 鉴于上述内存不能自己释放,于是垃圾回收机制 就
诞生了,Java就属于支持垃圾回收的语言。 - 引入了垃圾回收机制之后,就不需要靠手动来进行释放了,程序会自动判定,某个内存是否会继续使用,如果内存后续不用了,就会自动释放掉了。(就像聘请了一个保姆来负责你家里的卫生一样)
- 垃圾回收中有一个很重要的问题:STW (stop the world)问题,就是触发垃圾回收的时候,很可能会使当前程序的其他的业务逻辑被
强制暂停。(就像是正在玩游戏,但是妈妈突然过来要扫桌子底下的垃圾一样,这时不得不站起来停下手中的事情) - 随着技术体系的发展,目前是有办法把STW的时间控制在1ms之内的
- 垃圾回收,回收的是内存,JVM中的
内存有好几块 - 1、程序计数器 不需要GC
- 2、栈 不需要GC,局部变量都是在代码块执行结束之后自动销毁的(这是栈自己的特点)
- 3、元数据区 一般不需要GC,JVM一般都是涉及到
类加载很少涉及到类卸载 - 4、堆 需要GC
- 这里提到的垃圾回收,说是回收内存,但是更准确的说是
回收对象 - 每次垃圾回收的时候,都是释放若干个对象(
实际的单位都是对象)
垃圾回收的具体流程
- 1、
识别出垃圾 - 判定这个
对象后续是否还要继续使用,在Java中,使用对象,一定要通过引用的方式来使用(匿名对象除外),反之如果一个对象没有任何引用指向它,就视为是无法被代码中使用,可以作为垃圾了。这样判断比较简单的情况,如果要判断复杂的情况就要引入两个机制 - A 引用计数 :这种思想方法,并没有在JVM中使用,但是广泛应用于其他的主流语言的垃圾回收机制中。所谓引用计数,就是给
每个对象安排一个额外的空间,空间里要保存当前这个对象有几个引用,然后在要回收的时候,有专门的扫描线程,去获取到当前每个对象的引用计数的情况。当发现对象的引用计数为0,说明这个对象就可以释放了。这种方法缺点也明显,会消耗更多内存空间,毕竟要每个对象都安排一个计数器
关于引用计数的循环引用 问题,和线程死锁 的原理类似
点击跳转👉🏻什么是线程死锁
- B 可达性分析 :JVM用的方案,本质上是用
时间换空间相比于引用计数,需要消耗更多的时间,而且不会产生循环引用这样的问题。在写代码的过程中,会定义很多的变量,就可以这些变量作为起点,出发、尝试进行遍历。所谓的遍历就是沿着这些变量中持有的引用类型的成员、再进一步的往下进行访问。所有能被访问的对象,自然就不是垃圾了。反之遍历一圈下来也访问不到的对象,就视为垃圾(这个过程就像是判断一个二叉树的节点在不在树上一样,如果节点可达,就说明在树上) - 2、
把标记为垃圾的对象内存空间进行释放 - 主要的释放方式,有三种
- A 标记-清除
- 把标记为垃圾的对象,直接释放掉,最简单粗暴的方法

- 上述的释放方式、就可能会产生很多的、小的、分散的空闲内存空间。我们称为
内存碎片,内存碎片可能会导致后续申请内存失败。申请内存,一般都是一次申请一个连续的内存空间,内存碎片就打断这里的连续 - B 复制算法
- 发现可回收的对象,
不直接释放内存,而是把不是垃圾的对象,复制到内存的另一半,然后再整体释放掉


- 一起不用回收的对象移到右边之后,把左侧空间整体释放掉
- 复制确实能避免内存碎片问题,但是缺点也明显,
可用的内存变少了,复制的对象多了开销很大 - C 标记-整理
- 类似于顺序表删除中间元素,通过
搬运存活对象、来达到整理的目的



- 这样也能有效解决内存碎片问题,并且这个过程也不像复制算法一样、浪费过多的内存空间。
- 但是这里移动复制的
开销也很大 - X 分代回收
- JVM使用的就是这个,鉴于前面的思想,搞出的综合性方案
- 分代回收,依据不同种类的对象,采取不同的方式
- 引入概念,对象的
年龄。JVM中有专门的线程负责周期性扫描、释放 - 一个对象,如果被线程
被扫描了一次,可达了就不是垃圾,年龄+1(初始年龄相当于是0) - JVM中就会根据对象
年龄的差异,把整个堆内存分成两个大部分:新生代、老年代

- 1、当代码中new 出来一个新的对象,这个对象就是被创建在
伊甸区的,这样以来伊甸区就会有很多对象。一个经验规律:伊甸区中的对象,大部分时活不过第一轮GC的,这些对象生命周期都很短 - 2、第一轮GC扫描完成之后,少数伊甸区中幸存的对象,就会通过
复制算法(两块内存),复制到幸存区,后续GC的扫描线程还会持续进行扫描(幸存区也会扫描)。幸存区中的大部分对象也会被标记为垃圾,少部分幸存,一样的继续用复制算法、复制到另一半幸存区(只要这个对象在幸存区中继续存活)。每次经历一次GC扫描,年龄就+1 - 3、如果这个对象在生存区中,经历了若干轮GC依然
存活,JVM就会认为,这个对象生命周期大概率很长,就把这个对象从生存区、复制到老年代 - 4、老年代的对象,也会被GC扫描,只不过
频率会小很多 - 5、当对象在老年代也没有
存活了,JVM就会按照标记-整理的方式,释放内存