深入理解Java虚拟机-3垃圾收集器与内存分配策略

本章将是jvm相关面试的核心内容之一,而之所以将jvm作为java面试的重点,正是在于相对于C++而言的。因为jvm的存在,导致java存在了很多的优点,如跨平台、开发者不需要过多的关注内存变化等,因为这些正是jvm帮助我们做的,而对于C++而言,比如回收内存都是需要开发者亲自来操作的,并且其中很多的错误都是由此导致的。
还记得本科大二的时候(2014年),当时正在为找工作准备方向,当时是的方向很多,C++,前端,安卓等,宿舍的大神也正在学C++,其中在许多方面我都是受着他的引导。但是重点是c语言我掌握并不是很好,记得当时期末考试我只考了68分(大学之前我是没怎么接触过电脑的,直到高考模拟填报志愿时我都不怎么会复制粘贴。但是这不是理由,主要原因在于我压根没怎么上课,而这个高三形成的坏习惯将在之后的很长时间内影响着我,影响着我的际遇,但是我却不想也不能改变)。最初,我将C++作为工作方向,当时也并不知道C++是做什么的,但知道java是做安卓的。但是C++和C一样的繁杂,特别是指针,这个老师和书本都喜欢重点讲述的知识,却是最容易被误导的。大学时代中,其实很简单的知识,总是被老师以各种题目的方式考察,这样反倒是让我们不知所以。最终,在一个星期后,当我看到了一本java书籍后,我就抛弃了我当初一心选择的C++。因为java在许多方面要比C++等开发清晰得多,相对我而言,那种格式让一个初学者一看就会,只需要一个100页的“垃圾书”就可以入门了。

3.1 概述

本章将主要围绕下面几个问题来讲述:

  • 什么是垃圾?
    无用的数据就是垃圾,比如在Java中为了将网页数据保存在数据库中,网页端是一个学生信息(name,age,school),需要将此信息保存在数据库中。但是对于一个javaweb而言,这种将将需要很多的中间变量来作为桥梁,才能传输到数据库中。而这些中间变量就是垃圾,在运行完成后都需要删除。如(Student对象,Service对象等等)
  • 那些内存是垃圾,需要回收?

  • 什么时候回收?

  • 如何回收?

一个问题:
既然垃圾回收器自动回收,为什么还需要了解分配机制?
因为自动回收器就像一个机器人,每一个机器人都有一个界限,比如一分钟处理10个对象,突然有有一天来了100个对象,此时就打破了这个界限,就需要人为改变。

3.2 什么时候回收
对于一个对象,我们什么时候需要回收它?对于这个问题,主要有两种方式:

  • 引用计数法
    即对于每一个对象,我们都申明一个指向这个对象引用的计数,当引用计数为0时,就可以回收了;这种方式python语言在使用;主流的java虚拟机没有选用,主要是因为这样无法解决循环引用的问题,比如A和B互相引用,但是A、B都不再使用,但却无法回收
  • 可达性分析法
    java和C#以及lisp都是使用的这种方式。
    可达性的方式相当于一个树形结构,通过定义一个根节点GC ROOTS对象,然后所有的其它对象都是被他引用,一直往下走,就相当于一个树形结构。这里面最重要的就是根节点GC ROOTS的设定,一般而言,GC Roots主要分为以下四类(也包括其他的):

    • 1虚拟机栈中引用的对象,即方法中的引用对象
    • 2类的静态变量引用的对象
    • 3类中常量引用的对象
    • 4本地方法引用的对象

3.3 引用的分类-参考1参考2

最开始一个对象就只有有引用和没有引用之分,这样其实很简单。但是这样处理有很多问题,比如有些对象只是在一定时期类没有引用,但是之后会用到,如果直接就回收的话,会导致一些错误出现。因而,为了改善这些“食之无味,弃之可惜”的对象,我们将对象更具引用的强弱又分为以下四种引用:强引用、软引用、弱引用、虚引用

对这些引用分类的依据为判断当前内存是否足够及应对实际场景解决问题,如果足够就丢弃,即什么都要。这就相当与《荒野行动》这个游戏一样,玩家的背包容量有限,但是最初的时候我们什么也没有,包是空的,所有我们简单什么都会装在包里吗;但是随着东西的增多,包满了,这时候我们就会有选择行的扔掉一些东西,比如多装一些步枪子弹,而丢掉一些没用的散弹枪子弹。

  • 1强引用
    只要引用存在就永远不会被回收。强引用就是一般的引用,我们经常遇到的引用,就像Student s = new Student()这种。
public class Main {
    public static void main(String[] args) {
        new Main().fun1();
    }

    public void fun1() {
        Object object = new Object();
        Object[] objArr = new Object[1000];
    }
}
  • 2软引用(缓存)
    软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

      软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。下面是一个使用示例:

import java.lang.ref.SoftReference;

public class Main {
    public static void main(String[] args) {

        SoftReference<String> sr = new SoftReference<String>(new String("hello"));
        System.out.println(sr.get());
    }
}
  • 3弱引用(清除表中的无用对象,避免标的引用存在)
    无论内存是否足够,下次垃圾回收时都会被回收。应用场景:比如淘宝中有一个商品A,Product productA = new Product(...);我们将这个商品添加到hashmap中。当有一天这个商品被买走了,这个商品就不存在了,productA = null也没有存在的意义,但是productA 并不能被回收,因为hashmap还引用着它,所以这时候就可以加入一个中间弱引用对象对Product进行封装WeakReference weakProductA = new WeakReference<>(productA);然后添加weakProductA 到hashmap中,此时弱引用对象就会被立即回收,同时productA也会被回收。
import java.lang.ref.WeakReference;

public class Main {
    public static void main(String[] args) {

        WeakReference<String> sr = new WeakReference<String>(new String("hello"));

        System.out.println(sr.get());
        System.gc();                //通知JVM的gc进行垃圾回收
        System.out.println(sr.get());
    // 输出结果为:
    // hello
    // null
    }
}
  • 虚引用(回收前的日志记录)-虚引用参考
    最弱的一种引用,任何时候都可以被回收。垃圾收集过程中,对象的可触及状态改变的时候,可以把引用对象和引用队列关联起来【这里说的关联,是说垃圾收集器会把要回收的对象添加到引用队列ReferenceQueue】,这样在可触及性发生变化的时候得到“通知”。当垃圾收集器对加入队列的对象改变可触及性的时候,就可以收到异步通知了。
    我们可以声明虚引用来引用我们感兴趣的对象,在gc要回收的时候,gc收集器会把这个对象添加到referenceQueue,这样我们如果检测到referenceQueue中有我们感兴趣的对象的时候,说明gc将要回收这个对象了。此时我们可以在gc回收之前做一些其他事情,比如记录些日志什么的。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;


public class Main {
    public static void main(String[] args) {
        ReferenceQueue<String> queue = new ReferenceQueue<String>();
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
        System.out.println(pr.get());
    }
}

3.2.4 对象的两次生命
java中使用新生代和老生代来进行对象回收,即一个对象有两次生命,在第一次并不会直接死亡;当在第二次回收前还是没有新的引用,就会被二次回收(),这才算是最终死亡。
多提一点:对象在回收前会执行一次finalize方法,在这个方法中对象可以自救(重新让自己被引用),但是一个对象的finalize方法只能被系统调用一次,所以只能自救一次。(这个可能并没有实际应用)

3.2.5 方法区的回收

方法区跟堆一样,被所有的线程共享。用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。就是类的一些信息。
一般而言垃圾回收是对堆来说的,因为大部分垃圾都是由对产生的,即对于的对象。所以称方法区为永久代,同样常量池也是永久代。常量池的回收就是看是或否有引用。而对于方法区需要满足以下三个条件:(无用的类)

  • 1没有该类对应的实例,即实例都已经被回收(实例
  • 2加载该类的ClassLoader已经被回收-加载该类用的(正向加载
  • 3该类对象的所有java.lang.Class对象没有任何地方引用(即无法通过反射来访问该类)

3.3 垃圾收集算法
书中的垃圾收集算法只是基本的,当前的垃圾收集算法可能已经更加完善

3.3.1 标记-清除算法
即对无用对象进行标记,然后统一回收。两个缺点:

  • 速度问题:标记清除效率都不高,为什么不高?书中并没有详说(这里的效率应该是指针的问题)
  • 空间碎片问题:每次标记清除后都会产生大量的不连续碎片,在之后分配较大的对象时,找不到足够大的空间来存储,从而导致不断地触发垃圾回收操作

3.3.2 复制算法(改进1)
即将内存分为两个部分,每次只是用一个部分,将另外一部分作为备用。这样在标记清除后,将还存在的对象复制到另一部分,同时清理当前的内存部分。这样就解决了内存碎片化的问题,同时由于分配内存时是连续分配的,所以堆指针连续移动,运行更加高效。

其实现在更加高效的垃圾回收算法就是采用这种方式,只是不是简单地分为均等的两个部分。因为经过研究发现一般新生代的对象中98%的对象都是“朝生夕死”的,

3.3.3 标记-整理算法
标记整理算法是在标记清除算法的基础上进行改进的。使用整理来代替清除,从而解决了碎片空间的情况。具体方法是,在对对象进行标记后,不是直接进行清除,而是先整理,将所有存活的对象向一边移动,之后将另一边的对象全部清理

3.3.4 分代收集算法
这是当前商业虚拟机主要使用的方法,主要是集成了复制算法和标记整理算法的集合。首先将对象分为新生代和老年代,新生代中只有大部分对象存活,所以使用复制算法;而在老年代中,大部分对象都会存活下来,所以使用标记整理算法


3.4 HotSpot的算法实现
>

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值