JVM(2)

类加载的双亲委派模型:

本质上是描述了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提供了多个类加载器,每一个类加载器负责一块区域

1)在JVM中,有三个类加载器,也就是三个特殊的对象,来负责这里面来找文件的操作;

2)这三个类加载器主要是:

BootStrapClassLoader:负责加载JAVA标准库的一些类

ExtensionClassLoader:负责加载JDK扩展的一些类,现在很少用到

ApplicationClassLoader:负责加载项目中的一些类

1)JRE/lit/rt.jar(所有的文件都在这里面)他是负责加载java标准的一些类(String,ArrayList)
2)JRE/lib/ext/*.jar这里面放的是JVM扩展出来的库中涉及的一些类
3)CLASSPATH指定的所有jar或者目录,他是负责加载程序员自己写的类,主要是在我们的项目目录下面

除此之外程序员还可以进行自定义类加载器,每一片类加载器负责一片区域,Tomact就进行了自定义类加载器,专门用来加载webapps中的.class文件,Tomact会把打好的war包放到webapps目录下面

双亲委派模型,就描述了找目录的这个过程,也就是说上述类加载器是如何进行配合的

1)这三个类加载器之间存在父子关系(这个关系并不是继承中的父类子类,而是类似于链表一样,每一个类中有一个parent字段,指向了父类的加载器)

2)当我们在代码中使用某个类的时候,就会触发类加载,先是进入ApplicationClassLoader这个类加载器,但是ApplicationClassLoader并不会真的开始立即去扫描自己所负责的路径

3)而是先去找他的爸爸,ExtClassLoader,但是此时ExtClassLoader也不会立即开始扫自己所负责的路径,而是先去找他的爸爸

4)BootStrapClassLoader,他此时也不会立即扫描自己所负责的路径,也想要找自己的爸爸,他没有爸爸,只能自己干活,去寻找自己所在的路径,如果在自己的目录中,找到了复合的类,就进行加载;也就没有别的类的事情了,就进行后续的一些操作;

3)但是如果没有找到匹配的类,就会告诉儿子(ExtClassLoader)我没找到,然后ExtClassLoader就来寻找扫描自己所负责的目录区域,如果找到就进行加载,如果还没找到,就会再告诉自己的儿子AppicationClassLoader,再去扫描自己所在的目录,找到就进行加载,如果没找到,就会抛出异常(ClassNotFoundException)

4)搞出这么一套原则,实际上就是来约定三个被扫描目录的优先级,优先级最高的是JRE/bit/rt.jar,其次是JRE/lib/ext/*.jar,最低是CLASSPATH指定的所有jar或者目录,优先级从上向下依次递减;

当我们的程序开始进行启动的时候,会先进入到ApplicationClassLoader类加载器

1)我们的AppicationClassLoader就会进行检查,它的父类加载器是否已经加载过了,如果说没有,就会调用父类加载器,也就是说ExtensionClassLoader

2)我们的ExtensionClassLoader也会进行检查一下,它的父类加载器是否已经加载过了,如果没有,就会调用父类加载器,BootStrapClassLoader

3)当我们进入到BootStrapClassLoader之后,他也会进行检查一下,它的父类加载器是否已经加载过了,自己发现没有父亲,那么就会扫描自己所进行负责的路径

4)如果我们进行加载的类可以在标准库中找到,那么直接由BootStrapClassLoader来进行后续的加载过程,查找环节于是就结束了

5)如果说我们发现这个类在BootStrapClassLoader中找不到,那么,就会回到子类加载器来进行继续扫描

6)BootStrapClassLoader于是继续向下走,找到子类ExtClassLoader就会进行扫描自己所在的目录,发现没有扫描到,那么就会继续回到子类加载器来进行继续扫描

7)ApplicationClassLoader也是可以扫描自己所在的目录,最终能找到自己所扫描的类,就进行后续的加载,那么查找目录的环节结束

8)如果说最终我们的ApplicationClassLoader没有扫描到,那么最终就会抛出ClassNotFoundException异常

5)然后在JVM的源码当中,针对这里面的优先级规则的实现逻辑,就被称为双亲委派模型;

如果是咱们自己写的类加载器,其实不需要严格遵守双亲委派模型,咱们自己写类加载器,就是为了告诉程序,向一些的目录中去找.class

6)比如说某个类的名字,同时出现在了多个目录当中,这个时候这个优先级就决定了最终加载的类是神马?标准库中有一个类叫做java.lang.String,咱们自己写代码,也是可以创建一个类,叫做java.lang.String,我们要下进行加载java.lang.String

Tomact是没有遵守双亲委派模型的,在进行加载webapp中的类

7)所以我们要优先加载标准库的java.lang.String的类,而不是我们自己写的java目录,字节写的lang包的String类

8)这样做的主要目的就是说一旦我们程序员自己写的的类和咱们标准库中的类,全限定类名重复了,也是能够顺利的加载到咱们的标准库中的类

如果说我们进行自定义类加载器,是否也要遵守双亲委派模型呢?

可以遵守,也可以不遵守,比如说在webapp目录里面,Tomact进行加载webapp中的类,就没有遵守,因为遵守了也没有什么意义,毕竟咱们webapp中自己写的类,都是有咱们程序员自己写的,自己来进行部署的,如果你自己写的类加载器在特定的目录中都找不到,那就不要指望JAVA标准库能够找到了,咱们自己写的类都是自己命名的

5.JVM的垃圾回收机制(也叫作GC,内存是有限的,况且内存是要给很多个进程来进行使用的,都是以对象为单位进行回收,此时对象在对象上面所占用的内存此时也会被回收了)

在我们进行申请某一个内存的时候,一般都是明确的,我们需要进行保存某些数据,那么就需要进行申请内存,但是我们释放内存的时机,却不是那么清楚

GC是垃圾回收

1)垃圾回收机制使用过更复杂的策略来进行判定内存是否可以进行回收,并进行回收的动作

2)咱们的垃圾回收机制本质上是依靠运行时环境,额外做了很多的工作,来进行完成自动释放内存的操作的,这样就让程序员的心理压力大大的降低了;

垃圾回收的劣势:

1)可能会有额外的开销,消耗的资源变得更多了

2)可能会影响程序的流畅运行,因为垃圾回收经常会引入STW问题(所有程序禁止执行)

1)背景介绍

1)我们在进行创建对象的时候,总是要进行申请内存,创建变量,new对象,加载类,毕竟我们的程序的运行,是离不开硬件的支持的,而内存有时我们计算机当中最最重要的硬件之一

1.1)当我们申请内存的时机一般都是明确的,例如说我们想要保存某些数据就需要申请内存,但是释放内存的时机,一般我们是不清楚的

1.2)还有就是说我们在代码里面,申请一个变量(申请一个内存)这个变量啥时候不会再进行使用了?也不是那么容易就可以确定的,如果内存释放的时机有问题,内存还想要用,结果却对了,就非常难受了,所以说内存早点释放不行

1.3)要是内存晚一点进行释放行不行呢?虽然我这个内存暂时不会被使用,晚一点释放行不行呢?还是不行,你这个内存不再进行使用了,别的人还无法继续申请内存,就类似于图书馆占座问题,你不在这里进行学习,还占着坐,别人还无法使用这个资源,就非常难受了

所以说内存的释放,早了也不行,因为别人有可能还在继续进行使用,所以说内存释放晚了也是不行的,因为你占着这块内存空间不使用,别人还用不了,所以说我们还是需要恰到好处才可以

1.4)在C语言中,内存泄漏这个是咱们管不了,直接摆烂,你们程序员自己看着办,完全由程序原来自己释放内存,内存什么时候释放,什么时候不释放,释放的是晚还是早,这完全取决于程序员自己,程序员自己承担,这就会发生内存资源泄露的问题,内存申请了之后不会被释放或者说太晚了才进行释放,这就会导致可用内存越来越少,最终没有内存可以用了,1.5)内存资源泄露,有的暴露块,有的暴露慢,暴露实际不确定,如果说遇到了在半夜服务器崩溃了,暴力时机不确定,需要花很多的精力来进行查找

优点:可以非常好的保证不会出现内存泄漏的情况

缺点是:1)可能会影响程序的流畅运行

2)需要消耗额外的系统资源,内存的消耗可能存在延时,性能会变低,不是说这个内存在用完后的第一时间释放,而是可能等一会再释放可能会导致出现STW(stop the world)问题;

1)C++的核心目标有两个:和C语言兼容,也要和各种操作系统和各种硬件做到最大化的兼容,追求极致的性能(机器运行的性能),另外要保证尽可能小的资源开销,所以C++没有垃圾回收,但是C++有一个叫做内存指针的来进行缓解内存泄漏的可能性,从根本上没有缓解问题;

2)所以说在人工智能领域,游戏引擎,高性能服务器,操作系统内核对于兼容性和性能要求极高的场景,还是使用C++;

所以说我们的垃圾回收,本质上都是依靠运行时环境,额外做了很多的工作,来进行自动释放内存的操作的

Rust是最近几年的新兴起的一个语言,是想要挑战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,那么此时处理请求的线程无法进行工作了,暂停一下,此时客户端收到服务器的请求的时间就会变长,给用户带来很不好的体验;

什么是STW?为什么要进行STW?

1)是当我们在进行执行垃圾回收算法的时候,JAVA应用程序的其它线程都要被挂起,所有JAVA代码停止

2)因为在我们的JAVA应用程序里面,引用关系是在不断发生变化的试想一下当前的Object a是一个垃圾,GC把他标记成为垃圾,但是在清除之前又有其他对象指向了Object a,那么此时他就不是垃圾了,如果说我们没有STW又要无限地进行维护这种关系来进行采集正确的信息;

2)垃圾回收要回收什么?------我们日常情况下所说的垃圾回收,是在回收内存,本质上是指堆上面内存的回收,因为堆占用的内存空间就是最大的,本来就是占据了一个程序中绝大部分的内存

1)JVM中的内存有很多,栈,堆,程序计数器,方法区;

2)栈(只要函数执行完毕咱们的栈帧就会自动被释放了)和程序计数器(是一个固定大小的内存区域,只是保存地址的,不会涉及到申请,也不会涉及到释放)

况且来说他们都是和具体的进程绑在一起的,当我们访问某个方法或者是代码块的时候,进入到方法或者代码块之后会申请内存,代码块结束,方法和代码块执行完之后会自动释放内存;

3)我们要回收的是堆和方法区尤其是堆区,最需要GC的,方法区里面是类对象,通过类加载的方式得到的,对方法区进行垃圾回收,就相当于是类的卸载(类不进行使用了,就把他从内存中拿掉,类卸载是一个非常低频的操作);堆占据的内存空间是最大的,我们本质上来说就是对new的内存来进行垃圾回收

在堆上,是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;
}

我们以上面的例子来进行举例,在我们的方法的调用过程中,我们创建了对象,分配了内存,此时在我们的方法执行的过程中引用计数是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个字节,因此影响也并不会很大,负担也不会太大)程序中对象数目也不多,这时候程序计数没问题,就是说每一个new处的对象都要搭配一个计数器
但是如果是一个小对象,此时引用计数就会带来不可忽视的空间开销,一个int类型的数据就已经占4个字节,一个对象就占用了8个字节,再来个引用计数空间,空间利用率就会非常低,就低了一倍

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被销毁),此时这个对象的引用计数也是减1,也少了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)也就是说我们可以通过额外的线程,定期的针对整个内存空间的对象进行扫描,有一些起始位置就类似于GCRoots,会类似于深度优先遍历一样,我们可以把访问的对象都进行标记一遍(带有标记的对象就是可达的对象),没有被标记的对象就是不可达对象,也就是垃圾

来举一个二叉树的例子:

              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;从A出发,那么此时G点就不可达了,就成了垃圾,这个时候我们从a出发就访问不到f了,那么此时我们就认为f是垃圾,f就应该被回收到
但是如果写了这个代码,root.right=null;那么C和G都是不可达的,也都变成了垃圾,我们从a出发c,f我们都访问不到,就变成了垃圾,他们都是不可达的,就都被标记成垃圾
5)所以说在JVM中,就存在一组线程,来周期性的,来执行上述遍历过程,不断找出这些不可达的对象
由JVM进行回收
6)如果我们内存中的对象特别多,那么这个遍历就会非常慢,GC还是很消耗时间和系统资源的

我们把可达性的初始位置,称为GCRoots,下面三种类型可作为GCRoots

1)栈上的局部变量表的引用

2)常量池中引用所指向的对象

3)方法区中,引用类型所指向的静态成员变量

4)基于上述过程,就完成了垃圾对象的标记,他和引用计数相比,可达性分析确实要麻烦一些,自身的缺点:同时实现可达性分析的遍历成本开销也是比较大的,遍历开销比较大,是最消耗时间的

5)但是他解决了引用计数的两个缺点,内存上不需要消耗额外的空间(需要额外的空间来进行保存引用计数),也没有循环引用的问题,但是系统开销是非常大的,遍历一次比较慢

6)不管是引用计数,还是可达性分析,实际上都是通过是否有引用来指向对象,通过引用来决定对象的生死,找垃圾本质上是确定这个对象是否要被使用,什么时候算不使用了,没有引用指向,就算不进行使用了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值