类加载的双亲委派模型:
本质上是描述了JVM中的类加载器,如何根据类的权限定名(java.lang.class)找到.class文件的流程
1)这是在加载阶段,是描述JVM去哪个目录中去找.class文件的,同时也是JVM再进行查找类的时候的一个优先级规则,主要出于loading阶段(找到文件,打开文件,生成一个出事的类对象)
2)进行类加载的时候,其中一个非常重要的环节就是根据这个类的名字"java.lang.String"找到对应的.class文件
3)双亲:parent指的是父亲或母亲,进行类加载的过程,一个非常重要的过程就是根据这个类的名字java.lang.String来找到对应的.class文件;
4)在JVM中我们提供了专门的对象来完成类加载,这个对象叫做类加载器,当然我们寻找.class文件的操作也是通过类加载器来进行负责的
5)我们的.class文件,可能放置的位置有很多,有的要放在JDK目录下,有的要放置在项目目录里面,还有的在其他特定位置,因此我们的JVM提供了多个类加载器,每一个类加载器负责一块区域
在JVM中,有三个类加载器,也就是三个特殊的对象,来负责这里面来找文件的操作;
1)JRE/bit/rt.jar(所有的文件都在这里面)他是负责加载java标准的一些类(String,ArrayList)BootStrap
2)JRE/lib/ext/*.jar这里面放的是JVM扩展出来的库中涉及的一些类--ExtClassLoader来进行查找
3)CLASSPATH指定的所有jar或者目录,他是负责加载程序员自己写的类,主要是在我们的项目目录下面除此之外程序员还可以进行自定义类加载器,每一片类加载器负责一片区域,Tomact就进行了自定义类加载器,专门用来加载webapps中的.class文件
1)这三个类加载器之间存在父子关系(这个关系并不是继承中的父类子类,而是类似于链表一样,每一个类中有一个parent字段,指向了父类的加载器)
2)当我们在代码中使用某个类的时候,就会触发类加载,先是从AppClassLoader开始,但是AppClassLoader并不会真的开始立即去扫描自己所负责的路径,而是先去找他的爸爸,ExtClassLoader,但是此时ExtClassLoader也不会立即开始扫自己所负责的路径,而是先去找他的爸爸,BootStrapClassLoader,他此时也不会立即扫描自己所负责的路径,也想要找自己的爸爸,他没有爸爸,只能自己干活,去寻找自己所在的路径,如果在自己的目录中,找到了复合的类,就进行加载;也就没有别的类的事情了,就进行后续的一些操作;
3)但是如果没有找到匹配的类,就会告诉儿子(ExtClassLoader)我没找到,然后ExtClassLoader就来寻找扫描自己所负责的目录区域;如果找到就进行加载,如果还没找到,就会再告诉自己的儿子AppicationClassLoader,再去扫描自己所在的目录,找到就进行加载,如果没找到,就会抛出异常(ClassNotFoundException)
4)搞出这么一套原则,实际上就是来约定三个被扫描目录的优先级,优先级最高的是JRE/bit/rt.jar,其次是JRE/lib/ext/*.jar,最低是CLASSPATH指定的所有jar或者目录,优先级从上向下依次递减;
5)然后在JVM的源码当中,针对这里面的优先级规则的实现逻辑,就被称为双亲委派模型;
如果是咱们自己写的类加载器,其实不需要严格遵守双亲委派模型,咱们自己写类加载器,就是为了告诉程序,向一些的目录中去找.class
6)比如说某个类的名字,同时出现在了多个目录当中,这个时候这个优先级就决定了最中要加载的类是神马?标准库中有一个类叫做java.lang.String,咱们自己写代码,也是可以创建一个类,叫做java.lang.String,我们要下进行加载java.lang.String
Tomact是没有遵守双亲委派模型的,在进行加载webapp中的类
7)所以我们要优先加载标准库的java.lang.String的类,而不是我们自己写的java目录,字节写的lang包的String类
8)这样做的主要目的就是说一旦我们程序员自己写的的类和咱们标准库中的类,全限定类名重复了,也是能够顺利的加载到咱们的标准库中的类
5.JVM的垃圾回收机制(GC,内存是有限的,况且内存是要给很多个进程来进行使用的,都是以对象为单位进行回收,此时对象在对象上面所占用的内存此时也会回收了)
GC是垃圾回收
1)垃圾回收机制使用过更复杂的策略来进行判定内存是否可以进行回收,并进行回收的动作
2)咱们的垃圾回收机制本质上是依靠运行时环境,额外做了很多的工作,来进行完成自动释放内存的操作的,这样就让程序员的心理压力大大的降低了
垃圾回收的劣势:
1)可能会有额外的开销,消耗的资源变得更多了
2)可能会影响程序的流畅运行,因为垃圾回收经常会引入STW问题(所有程序禁止执行)
1)背景介绍
1)我们在进行创建对象的时候,总是要进行申请内存,创建变量,new对象,加载类,毕竟我们的程序的运行,是离不开硬件的支持的,而内存有时我们计算机当中最最重要的硬件之一
1.1)当我们申请内存的时机一般都是明确的,例如说我们想要保存某些数据就需要申请内存,但是释放内存的时机,一般我们是不清楚的
1.2)还有就是说我们在代码里面,申请一个变量(申请一个内存)这个变量啥时候不会再进行使用了?也不是那么容易就可以确定的,如果内存释放的时机有问题,内存还想要用,结果却对了,就非常难受了
1.3)要是内存晚一点进行释放行不行呢?还是不行,你这个内存不再进行使用了,别的人还无法继续申请内存,就类似于图书馆占座问题,你不在这里进行学习,还占着坐,别人还无法使用这个资源,就非常难受了
1.4)在C语言中,内存泄漏这个是咱们管不了,你们程序员自己看着办,这就会发生内存资源泄露的问题,这就会导致可用内存越来越少,最终没有内存可以用了,内存资源泄露,有的暴露块,有的暴露慢,暴露实际不确定
优点:可以非常好的保证不会出现内存泄漏的情况
它的缺点是:需要消耗额外的系统资源,内存的消耗可能存在延时,性能会变低,不是说这个内存在用完后的第一时间释放,而是可能等一会再释放;可能会导致出现STW(stop the world)问题;
(C++的核心目标有两个:和C兼容,追求极致的性能(机器运行的性能),另外要保证尽可能小的资源开销,所以C++没有垃圾回收)
所以说在人工智能领域,游戏引擎,高性能服务器,操作系统内核对于兼容性和性能要求极高的场景,还是使用C++
2)垃圾回收:
第一阶段:找到垃圾
第二阶段:释放垃圾
1)主要回收的是内存,JVM本质上来说是一个进程,一个进程中会有持有很多的硬件资源,带宽资源。例如CPU,硬盘,带宽资源,系统的内存总量是一定的,程序在使用内存的时候,必须先申请,才可以使用,用完之后,还需要进行释放;内存是有限的,用完之后一定要记得归还;
为了保证后续的进程有充足的内存空间,所以使用过的内存一定要进行释放;
2)从代码的编写来看,内存的申请时机,是十分明确的;但是内存的释放时机,是十分不确定的;这样也带来了一些困难,这个内存我是否还要继续使用吗?
3)C语言中,我们都学过malloc,free,如果malloc开辟出来的内存,不手动调用free,这个内存就会一直持有,此时内存的释放就全靠程序员自己来控制,一旦程序员忘了,或者该释放的时候,没有进行释放,就会造成内存资源泄露的问题;一直申请不释放,系统的可用内存资源越来越少,直到耗尽,此时其他资源想要再申请内存,就申请不到了;
4)对于手动回收内存来说,谁申请的谁就要进行释放
对于垃圾回收机制来说,谁申请都行,最后有一个统一的人来进行释放(对于java来说,代码中的任何地方都可以申请内存,然后再JVM中统一回收);是由JVM中一组专门用来进行垃圾回收的线程来做这样的工作;
5) STW:例如说一个男人的生活习惯,换完衣服之后,自己把衣服整理好,放到柜子里面,这是相当于自己手动回收内存
但是如果说换完衣服之后就随即将衣服往沙发上面一扔,最后还要媳妇进行整理,这就相当于是进行垃圾回收机制
但是如果说这个媳妇出差了几天,回到家里面之后发现全部是衣服,那么此时媳妇就要画出全部的时间和精力来进行整理衣服,别的什么事情也没有时间干了;
6)Java中Stop-The-World机制简称STW(java程序员已经做了很多努力,争取把他的时间缩短),是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止
7)此时用java写了一个服务器,这个服务器在每秒钟同时要处理很多服务器发送过来的请求,可能每一秒钟有几万个请求,此时出发了STW,那么此时处理请求的线程无法进行工作了,暂停一下,此时客户端收到服务器的请求的时间就会变长,给用户带来很不好的体验;
2)垃圾回收要回收什么?------我们日常情况下所说的垃圾回收,本质上是指堆上面内存的回收,因为堆占用的内存空间就是最大的,本来就是占据了一个程序中绝大部分的内存
1)JVM中的内存有很多,栈,堆,程序计数器,方法区;
2)栈和程序计数器他们都是和具体的进程绑在一起的,当我们访问某个方法或者是代码块的时候,进入到方法或者代码块之后会申请内存,代码块结束,方法和代码块执行完之后会自动释放内存,所以栈和程序计数器是自动申请,自动进行释放的;
3)我们要回收的是堆和方法区尤其是堆区,方法区里面是类对象,通过类加载的方式得到的,对方法区进行垃圾回收,就相当于是类的卸载(类不进行使用了,就把他从内存中拿掉);堆占据的内存空间是最大的;
堆的几种情况:
在堆上,是new出了很多对象,这里面主要分成三种
1)完全要使用
2)完全不使用
3)一半要使用,一半不使用(不会进行回收)
所以说java的垃圾回收,是以对象为基本单位进行回收的,一个对象,要么是完全被回收,要么完全不回收4)正在使用的内存我们是不能进行释放的,不再使用的内存,我们要进行释放,那对于中间那种一半使用一半不使用的对象,整体来说是不会进行释放的,我们要等到这个对象彻底不会使用,才会释放内存
5)所以说我们在GC当中就不会出现半个对象的情况,主要还是为了让垃圾回收变得更方便,更简单,所以说我们进行垃圾回收的基本单位是对象,而不是字节
回收垃圾的思路:先去找垃圾再去回收垃圾,对于一些对象来说,宁可放过1000,也不可以错杀一个
GC会提高程序员的开发效率,但是会降低程序的运行效率更低
3)如何找垃圾/如何标记垃圾/如何判定垃圾?
把这两个问题区分清楚:
1.谈谈垃圾回收机制中如何判定是不是垃圾?(引用计数+可达性分析)
2.谈谈Java的垃圾回收机制如何判定是不是垃圾?(可达性分析)
1)引用计数(在java中没有使用引用计数,python):我们针对每一个对象,都会额外引入一小块内存,都会引入一个计数器,保存这个对象有多少个引用指向它?
就是使用一个变量来保存这个对象被几个引用来,往往是在对象里面包含一个单独的计数器,随着引用的增加,我们的计数器就进行自增,随着引用减少,计数器就自减例如:
当我们的引用计数是0的时候,就判定这个对象是垃圾,当我们写了一个代码Test t=new Test()的时候,就会发生下面的过程
void func()
{
Test t1=new Test();
Test t2=t1;
}
我们以上面的例子来进行举例,在我们的方法的调用过程中,我们创建了对象,分配了内存,此时这个new Test()也就是方法执行的过程中引用计数是2,但是由于方法结束,由于t1和t2都是局部变量,就随着栈一起释放掉了,这一释放就导致引用计数变成0了,也就是说当前没有引用在指向new Test()这个对象了,也就没有代码可以访问到这个对象了,我们此时就认为这个对象就是一个垃圾
Test a=new Test();//此时有一个引用指向它
Test b=a;//此时有两个引用来指向它
func(a);//这里面的函数里面的形参和下一个函数里面的形参都是由引用来执行这个对象的
void ss(Test a)
{
}
在new Test(),在这个对象中有一个计数器,随着引用增加,计数器就增加,引用减少,计数器就减一
Test a=new Test();
b=a;
a=null;
b=null;
此时没有引用指向new Test();此时这个引用计数为0,就认为这个对象是垃圾
在java中,只可以通过引用来访问这个对象,如果没有引用了,
那么就认为这个对象在代码中再也无法进行使用了;
因此我们就可以判断引用是否存在,来进行判断对象的生死
优点:规则简单,实现方便,比较高效,就是需要注意一下线程安全问题,程序运行效率比较高
缺点:1)空间利用率比较低,尤其是针对大量的小对象,如果一个对象很大(里面加个int没问题,因为这个对象所占用的内存很大,里面就算放一个int类型,才占用4个字节,因此影响也并不会很大,负担也不会太大)程序中对象数目也不多,这时候程序计数没问题;
但是如果是一个小对象,在程序中对象数目也很多,此时引用计数就会带来不可忽视的空间开销,一个int就已经占4个字节,一个对象就占用了四个字节,再来个引用计数空间,空间利用率就会非常低
2)存在循环引用的问题,循环引用会导致引用计数出现问题,有些特殊的代码使用情况下,循环引用会使代码的引用计数出现问题,从而导致无法进行回收;
例:
class Test{ 代码模块1
Test t=null
};
Test t1=new Test(); 1
Test t2=new Test(); 1
------------------------------------------------------------------------------
t1.t=t2;(下面变成2)
t2.t=t1;(上面变成2) 代码模块2
当执行这样的操作的时候:
-----------------------------------------------------------------------------
t1=null,这样的一个操作其实是销毁了两个引用,但是此时的引用计数只减了1,这个操作即销毁了t1,也销毁了t1.t
t2=null, 代码模块3
此时就相当于少了t1(t1被销毁),也少了t1.t,此时这个计数器就会出现问题,没了t1,也就无法使用了t1.t
1)当我们执行代码模块1的时候,我们创建了两个对象,new Test();虽然字母一样,但是我们实实在在的在堆上面分配了两份内存,这两个堆空间都有一块内存来进行引用计数;一开始在进行执行代码模块1的时候,分别由两个引用执行对象1和对象2,随意他们的引用计数分别是1和1
2)当我们进行代码模块2的时候,将t1.t=t2,这个意思就是把t2的地址赋值给t1.t了,也就是这个时候t1中的t的引用就指向了0x222,此时指向0x222的引用有t2和t1.t,此时对象2的引用计数++变成2
3)同理我们进行t2.t=t1的时候,t2中的t指向了0x100,所以对象1的引用计数++;
3)当我们进行第三个模块的时候,t1=null,对象1的引用计数减减,t2=null,对象2的引用计数减减,就变成了下面这个鬼样子
![]()
4)此时此刻两个对象的引用计数不为0,所以无法释放,但是由于引用又彼此长在自己的身上,外界的代码是无法访问这两个对象的,此时这两个对象就被孤立了,即不能使用,又不能释放
5)我们想要访问对象1里的t,就要依靠对象1的引用(没有),连对象1的引用外部引用都没有,不仅对象1访问不了,对象1里面的t也是访问不了的,所以对象2也是同理
2)Java中的垃圾回收机制)---可达性分析
1)我们从一组初始的位置进行出发,向下进行深度遍历,把所有可以访问到的对象都标记成可达(是可以被访问到的),这个时候不可达的对象(没有进行标记的对象就是垃圾)
2)也就是说我们可以通过额外的线程,定期的针对整个内存空间的对象进行扫描,有一些起始位置就类似于GCRoot,会类似于深度优先遍历一样,我们可以把访问的对象都进行标记一遍(带有标记的对象就是可达的对象),没有被标记的对象就是不可达对象,也就是垃圾
来举一个二叉树的例子:
A
B C
D E F G
class TreeNode{
char val;
TreeNode left;
TreeNode right;
}
TreeNode root=...;
TreeNode A=new TreeNode();
TreeNode B=new TreeNode();
TreeNode C=new TreeNode();
TreeNode D=new TreeNode();
1)假设root是一个方法中的局部变量,当前栈中的局部变量,也是进行可达性分析的一个初始位置
默认情况下,递归性进行访问,默认情况下,整个树都是可达的,都不是垃圾
2)也就是说我们在代码中只要拿到树的根节点,我们就可以掌握所有的节点
树上面的任意节点我们就可以都访问到
3)当我们的GC在进行可达性分析的时候,当我们的GC扫描到a的时候,就会把a能够访问到的元素都去访问一遍,并且进行标记
4)但是如果在代码中写,root.right.right=null;那么此时G点就不可达了,就成了垃圾,这个时候我们从a触发就访问不到f了,那么此时我们就认为f是垃圾
但是如果写了这个代码,root.right=null;那么C和G都是不可达的,也都变成了垃圾,我们从a触发c,f我们都访问不到,就变成了垃圾
5)所以说在JVM中,就存在一组线程,来周期性的,来执行上述遍历过程,不断找出这些不可达的对象
由JVM进行回收
6)如果我们内存中的对象特别多,那么这个遍历就会非常慢,GC还是很消耗时间和系统资源的
我们把可达性的初始位置,称为GCRoot,下面三种类型可作为GCRoot
1)栈上的局部变量表的引用
2)常量池中引用所指向的对象
3)方法区中,引用类型所指向的静态成员变量
4)基于上述过程,就完成了垃圾对象的标记,他和引用计数相比,可达性分析确实要麻烦一些,同时实现可达性分析的遍历成本开销也是比较大的
5)但是他解决了引用计数的两个缺点,内存上不需要消耗额外的空间(需要额外的空间来进行保存引用计数),也没有循环引用的问题,但是系统开销是非常大的
6)不管是引用计数,还是可达性分析,实际上都是通过是否有引用来指向对象,通过引用来决定对象的生死