java双亲委派模型,垃圾回收机制,JVM详解

JVM板块不太涉及代码内容,主要是理论的一些知识.

JAVA双亲委派模型

首先在讲到java的双亲委派模型之前,我们得先了解一个前提概念就是java的类加载方式(本文主要以叙述双亲委派模型为准(因为名字高大上 所以面试常考)) 下面为类加载大概过程。

 java在类加载中会先找到.class文件,然后读取文件里面的内容并进行一个初步的解析。

同时第二部 他会验证该文件是够符合标准,也就是代码在编写过程中是不是存在一定的问题。验证完毕后,会给该类分配内存空间,然后是字符串常量的初始化,最后才是类对象的初始化(给静态成员赋值,执行静态代码块等过程).

本文要介绍的双亲委派模型就是java在类加载过程中找.class文件的方法(它描述了java找.class文件的过程)

在javaJVM中有三个类加载器

  1. 启动类加载器(Bootstrap ClassLoader)(加载Java标准库)
  2. 扩展类加载器(Extension ClassLoader)(加载Java扩展库)
  3. 应用程序类加载器(Application ClassLoader)(加载第三方库) 

他们的顺序是依次进行继承的,也就是说,1是2的父类,2是3的父类这种关系

在我们编写代码的过程中,当我们写了一个类时,在JVM进行类加载的过程中当一个类加载器收到了类加载请求后,第一时间并不会自己尝试对该类进行加载,而是把加载这个类的任务委派给自己的父类,然后父类收到该请求后,也不会自己尝试加载而且也把该请求传给了自己的父类。直到最后Bootstrap ClassLoader(标准版类加载器)没有父类后才会自己进行类的寻找,要是当前加载的类不在自己的库中,他的下一级加载器就开始负责加载。直到最后找到加载的类时停止.

就是当我写了一个类A后,因为他属于第三方库,应该在Application ClassLoader中,但是此时Application ClassLoader并不会直接对他进行加载,而是把加载任务交给Extension ClassLoader此时Extension ClassLoader收到加载请求后也不会对类进行加载而是交给类Bootstrap ClassLoader,当Bootstrap ClassLoader加载类时,因为他没有父类了,所以就检索自己的库是够有该类,有的话就进行加载 ,没有的话就交给自己下一级进行加载。Extension ClassLoader也进行了检索自己库中也没有该类,于是最后才是Application ClassLoader对我们所写的A类进行了加载.

双亲委派模型的优点

  1. 双亲委派模型可以避免类的重复加载.也就是父类加载器对类进行加载后,子类就不需要进行重复加载了,优化了加载成本.
  2. 可以有效避免我们自己编写的代码篡改了java的核心api,比如我自己写了一个Java.lang.String的类后,根据双亲委派模型的规则,会优先寻找到Java标准库中的类,然后根据优点1我们自己编写的类就不会被加载,这样我们编写的代码就无法对Java核心API产生影响了

双亲委派模型的破坏

Java的双亲委派规则被写了了java.lang.ClassLoader类的loadClass方法中。只要重写loadClass方法就可以对规则进行破坏。

双亲委派场景

目前非常主流的一个服务器Tomcat就改写了loadclass方法,对双亲委派模型做出了破坏,因为tomcat在实现的过程要,要做到一个tomcat容器可以部署多个服务器,每个服务器又是互相独立的,同时每个服务器可能还使用了很多不同版本的第三方库. 为了使每一个服务器独立,他改写了规则,先子类开始加载,不能加载在寻找父类加载器,这样有效避免了不同版本但使用同一个第三方库之间存在影响的问题

JAVA垃圾回收机制

Java回收机制回收的内容主要是对象。

同时垃圾回收机制分为2步

  1. 判断对象是否是垃圾
  2. 对对象进行回收

垃圾判断方式

这里我们简单的介绍几种常见的判断一个对象是否是垃圾的方法

引用计数

通俗一点来讲就是给每一个对象分配一个对象计数器.每次有引用指向他,引用计数器则+1,当引用计数器为0的时候。则判断该对象为垃圾 

  • Test test = new Test();
  • test  = null;

开始的时候test指向了一个对象 这个对象的计数器就会标记为1,当test为null的时候,就没有变量指向Test这个对象了,这时Test就会被判定为垃圾对象从而被回收。

但是引用计数的方法也是存在一定弊端的 比如

package csdn;
public class JVM {
    static class A{
        B b;
    }
    static class B{
        A a;
    }
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        a.b = b;
        b.a = a;
        a = null;
        b = null;
    }
}

这种两个类循环引用的情况下,引用计数就没办法实现了。需要引入其他的算法来解决,同时这种算法会给每一个对象都增加一个计数器,会有很多额外的内存开销。

为此java采用了另外一种垃圾判断机制

可达性分析

java在判断垃圾时,会根据现有的代码用一个扫描线程,依次对所有可能访问到的对象进行遍历。能访问到的对象就是存在引用的,无法访问的对象也就是垃圾了。一个程序中,对象是否是垃圾,一般是动态变化的,所以需要扫描线程每隔一段时间便进行一次扫描。

垃圾回收方式

标记清除(直接释放)

标记清除就是直接在内存中将该对象的内存给释放掉,但是直接释放掉之后,造成“内存碎片”的问题,因为在内存开辟对象空间的过程中,都是开辟一块连续的空间地址,要是进行直接释放,内存空间会变得断断续续。长时间运行,会导致即使有内容也无法进行内存的申请了。

复制算法

复制算法是指将空间分为左右2份,第一次扫描的时候,将垃圾回收掉,剩下的有用的对象依次复制到右侧。同理右侧也是。

这样可以有效的避免内存碎片问题,但缺陷是每一次的复制都会很消耗内存资源,因为每一次都需要将每个对象进行复制。

因为复制算法会将内存分为2个区域,所以内存利用率极低。

标记整理

类似于线性表删除元素时的操作,将后面的元素对前面的元素进行覆盖。以次来达到删除的作用。

但是标记整理算法,有效解决了上述复制算法的内存利用率问题,但是当元素多的时候,删除元素时搬运的代价非常巨大。

java内存回收方式

在介绍完上述三种垃圾回收的算法后,java采取了百家之长,就是结合了上述三种算法的优点

首先,我们先了解一个前提,根据写JVM的大佬不完全统计,一般的对象的生存时间在程序中一般分为2种,一种是存在时间极为短暂,一种是存在时间非常非常长。存在时间不短不长的就更少了。(上述结论是根据经验得出的,无法进行科学的证明)(、

首先,我们先定义一个概念,叫对象的年龄,也就是每一次扫描线程扫描后,要是该对象不是垃圾,则该对象的年龄加一岁。

java就是根据对象的年龄来制定不同的回收规则的。

它将对象存储的内存划分为了2个区域,所有的对象开始都会进入伊甸区,但是在java程序中大部分的对象其实是活不过第一轮的扫描的.此时对象要是活过了第一轮的扫描之后,大概率就是可以存在很久很久的对象了,此时这个对象会进入幸存区(生存区),这里有2块内存相同的区域,同理当这里的对象变成垃圾之后,用复制算法来对对象进行删除, 要是在幸存区,经过反复几轮的扫描之后,幸存区的对象大概率不会被回收了,此时就进入了老年区。根据经验,老年区的对象大概率是不会被回收的,但还是有可能会出现垃圾,所以老年区出现了垃圾对象后以标记整理算法为主.

要是一个对象是很大的对象,经过伊甸区的第一轮扫描后,为直接进入老年区,因为大对象不适合使用复制算法

值得注意的是,这几个区域中,伊甸区是扫描频率最高的,其次是幸存区,最后才是老年区。
(感觉不难理解 就不做解释了)

你可能感兴趣的:(java,jvm,开发语言)