JVM的类加载机制和垃圾回收介绍

一、类加载机制

我们知道java代码是先被编译为.class字节码,再加载到JVM中去运行的,那么.class字节码是如何加载进JVM的呢?
首先我们了解一下类的生命周期:
(1)加载:读取二进制内容;
(2)验证:验证class文件格式是否规范,语义分析,引用验证,字节码验证;
(3)准备:分配内存,设置类static修饰的变量的初始值;
(4)解析:类、接口、字段、类方法多等解析;
(5)初始化:为静态变量赋值,执行静态代码块;
(6)使用:创建实例对象;
(7)卸载:从JVM方法区中卸载。
接着回答上面的问题,答案就是类加载器。类加载器负责装入类,也就是通过类加载器读取指定位置(可能是桌面、项目bin目录、target目录等等)的资源,把它解析为类等对象后再加载到JVM中去执行。一个java程序运行,最少有三个类加载器实例,负责不同类的加载,如下图所示:
在这里插入图片描述
下面是我们用代码演示的类加载器ClassLoader:

/**
 * 查看类的加载器实例
 */
public class ClassLoaderView {
    public static void main(String[] args) throws Exception {
        // 加载核心类库的 BootStrap ClassLoader
        System.out.println("核心类库加载器:"
                + ClassLoaderView.class.getClassLoader().loadClass("java.lang.String").getClassLoader());
        // 加载拓展库的 Extension ClassLoader
        System.out.println("拓展类库加载器:" + ClassLoaderView.class.getClassLoader()
                .loadClass("com.sun.nio.zipfs.ZipCoder").getClassLoader());
        // 加载应用程序的
        System.out.println("应用程序库加载器:" + ClassLoaderView.class.getClassLoader());
        // 双亲委派模型 Parents Delegation Model
        System.out.println("应用程序库加载器的父类:" + ClassLoaderView.class.getClassLoader().getParent());
        System.out.println(
                "应用程序库加载器的父类的父类:" + ClassLoaderView.class.getClassLoader().getParent().getParent());
    }
}

运行结果如下:
在这里插入图片描述
从上图可以看到,核心类库的加载器为null,但这并不代表没有加载器,只不过他是用C/C++实现的,没法获取他的对象;拓展类库的加载器名字中带有ExtClassLoader;我们自己写的应用程序的加载器名字中带有AppClassLoader。
需要说明的是,某个类被一个类加载器加载后不会被重复加载,用同一个类加载器加载的类名一样的类是同一个类;所以通过“ClassLoader实例Id+包名+类名”可以唯一标记。并且类是在使用时才加载,不是事先加载的。
那么类何时会被卸载呢?需要满足以下两个条件:
(1)该Class所有的实例都已经被GC了;
(2)加载该类的ClassLoader实例已经被GC了。
为了避免重复加载,需要由下到上逐级委托,由上到下逐级查找,这就是类的“双亲委派”机制。如下图:
在这里插入图片描述
为了便于理解,我们可以把类加载器想象成一个“败家子”,这个“败家子”不会首先自己去加载类,而是委托给他的上一级“败家子”,逐级委托直到顶层的类加载器;如果顶层的类加载器无法完成加载请求(即自己的搜索范围没有找到对应的类),那么它就向下逐级查找,此时每一级的“败家子”才会自己去尝试加载,直到查找到可以加载的类加载器。

二、垃圾回收机制

自动垃圾收集是查看堆内存,识别哪些对象正在被使用、哪些对象未被使用以及删除这些未被使用对象的过程。那么如何识别哪些对象未被使用呢?
答案是利用可达性分析,将对象及其引用关系看做一个图,选定某些对象作为GC Root,然后跟踪引用链条,如果一个对象与GC Root之间不可达(及此内存区域的对象引用已经不存在了),那么任务这个是可回收对象。以下是可以作为GC Root的对象:
(1)虚拟机中正在引用的对象;
(2)本地方法栈中正在引用的对象;
(3)静态属性引用的对象;
(4)方法区常量引用的对象。
可达性是有级别的,在介绍可达性级别前先介绍一下引用类型,包括以下几种:
(1)强引用:最常见的普通对象引用,只要还有强引用指向一个对象,就不会回收(我们new出的一个对象就是这样);
(2)软引用:JVM认为内存不足时,才会试图去回收软引用指向的对象(常用于缓存的场景);
(3)弱引用:虽然是引用,但是它可能随时被回收掉;
(4)虚引用:不能通过它访问对象,它供对象被finalize之后执行指定逻辑的机制(在Netty中,当申请了一个堆外内存后,由于这块内存区域不被JVM管理,所以会加一个cleaner的虚引用,使其在使用完堆外内存后释放)。
可达性级别如下:
(1)强可达:一个对象可以有一个或多个线程可以不通过各种引用访问到的情况(对应强引用);
(2)软可达:只能通过软引用才能访问到对象的状态(对应软引用);
(3)弱引用:只能通过弱引用才能访问到对象的状态,当弱引用被清除时就符合销毁条件(对应弱引用);
(4)幻象可达:不存在其他引用,且finalize过了,只有幻象引用指向这个对象(对应虚引用);
(5)不可达:意味对象可以被清除了。
现在我们找到了要回收的对象,那么我们使用何种算法回收这些对象的内存区域呢?有以下几种垃圾回收算法:
(1)标记-清除算法:首先标记出所有要回收的对象,然后进行清除。此种算法不适合有大对象的堆,因为这种算法有内存碎片化的问题;
(2)复制算法:划分出同等大小的区域,回收垃圾时将存活的对象复制到另一块区域,并且复制的过程中将对象按顺序放置以避免内存碎片化,再将原区域内存回收。此种算法会浪费一定的内存空间,但是可以避免内存碎片化;
(3)标记-整理算法:类似于标记-清除算法,但是为了避免内存碎片化,会在清除的过程中将对象移动。此种算法可以节省空间并且避免内存碎片化,适合有大对象的堆,但是因为有对象移动的过程,会造成一定的性能损耗。
为了更好的回收垃圾,JVM把堆内存划分成了几个部分,每个部分用不同的垃圾回收算法,以发挥各种算法的特长。JVM把堆内存分为新生代(其中又分为伊甸区和存活区)和老年代;新生代和老年代的划分比例为1:2,新生代中存活0、存活1和伊甸区划分比例为1:1:8。图示如下:
在这里插入图片描述
其中:
(1)Eden区使用标记-清除算法;因为新创建的对象首先被放入Eden区,很多是小对象,并且有很多对象是用完即弃的,清除频率比较快,不需要关注内存碎片化的问题,关注的是效率;在Eden区被塞满后,标记出需要回收的对象,把不需要回收的对象放入存活区(S0或S1),并且记录下经历过的垃圾回收的次数(可理解为存活的“年龄”,这里为1),然后直接回收整个Eden区;
(2)Survive区使用复制算法,因为这个区域不会放太多对象,但是要避免内存碎片化;随着程序的运行,Eden区又满了,并且S0区也满了,那么把S0区域的对象放入S1区,并且把Eden区不需要清除的对象也放入S1区(记录“年龄”),再次清除Eden区和S0区(S0区和S1区是来回放的);这样经历一定次数后(可设置),直接把新生代的对象放入老年代。此外,如果新生代内存不够了,新创建的对象也会直接进入老年代;或者一个大对象被创建后也会直接进入老年代(因为大对象经历反复复制会降低效率)。
现在我们了解了JVM的垃圾回收机制,接着我们需要垃圾收集器回收掉垃圾。关于垃圾收集器可以看这篇博客:JVM(HotSpot)7种垃圾收集器

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值