Java面试---jvm及GC之深入理解JVM的内存结构及GC机制

目录

jvm内存结构

forName与loadClass的区别

什么是双亲委派机制

jvm垃圾回收的流程;哪些对象会被认为是垃圾;有一个对象A它有一个属性是B,B这个对象他又有一个属性是A,这个对象最终会不会被认为是垃圾;

GC root哪些对象会被认为是root;

jvm里面有一个存储虚拟s1和s2

什么样的数据会往老年代里面迁移呢;

如果老年代内存也不够用了怎么办呢;

fullGC的时候会有什么现象吗;有没有遇到到fullGC的时候影响业务的场景;

CMS

G1

jvm报错,OOM;


jvm内存结构

 其中,

    线程私有的:程序计数器,虚拟机栈,本地方法栈

    线程共享的:堆,方法区,直接内存

1 程序计数器

  程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。

  java虚拟机的多线程是通过线程轮流切换并分配CPU的时间片的方式实现的,因此在任何时刻一个处理器(如果是多核处理器,则只是一个核)都只会处理一个线程,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,因此这类内存区域为“线程私有”的内存。

  从上面的介绍中我们知道程序计数器主要有两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理
  2. 多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

  注意:程序计数器是唯不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

2 Java 虚拟机栈

  Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。Java虚拟机栈是由一个个栈帧组成,线程在执行一个方法时,便会向栈中放入一个栈帧,每个栈帧中都拥有局部变量表、操作数栈、动态链接、方法出口信息。局部变量表主要存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)和对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

  Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。

  StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。

  OutOfMemoryError:若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。

  Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

3 本地方法栈

  和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

  本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。

4 堆

  堆是Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存(目前由于编译器的优化,对象在堆上分配已经没有那么绝对了,参见:https://www.cnblogs.com/aiqiqi/p/10650394.html)。

  Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:其中新生代又分为:Eden空间、From Survivor、To Survivor空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。从内存分配的角度来看,线程共享的java堆中可能会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。

   

  如图所示,JVM内存主要由新生代、老年代、永久代构成。

  ① 新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。

  新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。

  ② 老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。

  ③ 永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。

  在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。

5 方法区

  方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

  HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。

  相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

6 运行时常量池

  运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)

  既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

  JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

   

7 直接内存

  直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。

  JDK1.4中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

  本机直接内存的分配不会收到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

 

forName与loadClass的区别

java类装载过程分为3步:

   

 

  1:加载

    Jvm把class文件字节码加载到内存中,并将这些静态数据装换成运行时数据区中方法区的类型数据,在运行时数据区堆中生成一个代表这个类

  的java.lang.Class对象,作为方法区类数据的访问入口。

  *释:方法区不仅仅是存放方法,它存放的是类的类型信息。

  2:链接:执行下面的校验、准备和解析步骤,其中解析步骤是可选的

    a:校验:检查加载的class文件的正确性和安全性

    b:准备:为类变量分配存储空间并设置类变量初始值,类变量随类型信息存放在方法区中,生命周期很长,使用不当和容易造成内存泄漏。

    *:类变量就是static变量;初始值指的是类变量类型的默认值而不是实际要赋的值

    c:解析:jvm将常量池内的符号引用转换为直接引用

  3:初始化:执行类变量赋值和静态代码块

因为他们都能在运行时对任意一个类,都能够知道该类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。

  • Classloder.loaderClass(String name)

    其实该方法内部调用的是:Classloder. loadClass(name, false)

    方法:Classloder. loadClass(String name, boolean resolve)

        1:参数name代表类的全限定类名

        2:参数resolve代表是否解析,resolve为true是解析该类

  • Class.forName(String name)

    其实该方法内部调用的是:Class.forName(className, true, ClassLoader.getClassLoader(caller))

    方法:Class.forName0(String name, boolean initialize, ClassLoader loader)

      参数name代表全限定类名

      参数initialize表示是否初始化该类,为true是初始化该类

      参数loader 对应的类加载器

  • 两者最大的区别

    Class.forName得到的class是已经初始化完成的

    Classloder.loaderClass得到的class是还没有链接的

  • 怎么使用

    有些情况是只需要知道这个类的存在而不需要初始化的情况使用Classloder.loaderClass,而有些时候又必须执行初始化就选择Class.forName

什么是双亲委派机制

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类

类加载器的类别

BootstrapClassLoader(启动类加载器)

c++编写,加载java核心库 java.*,构造ExtClassLoaderAppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作

ExtClassLoader (标准扩展类加载器)

java编写,加载扩展库,如classpath中的jrejavax.*或者
java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。

AppClassLoader(系统类加载器)

java编写,加载程序所在的目录,如user.dir所在的位置的class

CustomClassLoader(用户自定义类加载器)

java编写,用户自定义的类加载器,可加载指定路径的class文件

源码分析

 

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {}
                if (c == null) {
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

委派机制的流程图

 

双亲委派机制的作用

1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

jvm垃圾回收的流程;哪些对象会被认为是垃圾;有一个对象A它有一个属性是B,B这个对象他又有一个属性是A,这个对象最终会不会被认为是垃圾;

允许GC之后,开始查找那些允许被回收的(两个算法)-> 开始回收(四个算法)

第一步:那些对象是垃圾:

         1,引用计数法:通过对引用的遍历,找到对应的实例,让对应的实例计数加  1 ,如果引用取消,或者指向null,实例的引用减  1 。把找到的引用都遍历一遍之后,如果发现有对象实例的计数是0。那么这个对象 就是垃圾对象了。在通过垃圾回收算法对其进行 回收即可。

       缺点:想想一下,有两个类,互相引用,也就是A对象的实例(也就是对象的全局变量)是一个指向B对象的引用,B对象实例是一个指向A对象的引用。那么这两个对象的引用计数,永远不可能是0 。也就不可能对其进行回收了。

        2,可达性分析法:这个算法类似于树的遍历,学过数据结构的小伙伴应该会好理解。简单来说,按照一定的规则说明那些可以作为一个根节点(GC root),然后以这些根节点去访问其引用的对象,被访问的对象又会有其他对象的引用。想象一下,是不是像极了树的遍历。这个路径称作引用链,但凡是在引用链上的对象,都是可用的。注意,引用连的起始点都是GC root 哦。虽然有其他对象存在类似于引用链的结构,但是,起始点不是GC root的那一些,都是垃圾,可以被回收的。

一般情况下,都是使用的 可达性分析法去查找垃圾类实例。

GC root哪些对象会被认为是root;

 

哪些可以作为gc root
1、栈中的引用对象。
如:

void test() {
  B b = new B(); // 引用对象b
}

2、方法区中类静态属性引用的对象。
如:

public class B {
  private static A a; // 类静态属性引用对象
}

3、方法区中常量引用的对象。
如:

public class B {
  private static final A a; // 类静态属性引用对象
}

4、栈中JNI中引用的对象。
如:

void test() {
  JNI引用对象
}

 

jvm里面有一个存储虚拟s1和s2

年轻代里面有一个复制算法,这个就要说到

第二步:垃圾回收器算法(标记-清除、复制算法、标记-整理、分代算法)

       1,标记-清除:找到垃圾类之后,标记一下。然后直接 清除即可。(算法很快)

           缺点:产生空间碎片,不利于大对象的安排进去。

       2,复制算法:将内存分为四块:新生代(Eden),生存代(Survivor * 2),老年代。有五种内存分配策略,讲完之后再说。类的升级流程是Eden->Survivor->老年代;

         算法流程:1),先找到垃圾类,将可以使用的类移动到Survivor2,将Eden + 另一块Survivor1中的内存全部清除。

                           2),将新生成的类实例优先分配到Eden,分配不下时,放到Survivor2。进行GC时,将Survivor2中对象的满足一定条件(例如对象年龄达到某一个标准)的对象分配到老年代中。将本次GC存活下来的分配到Survivor1中,在清除Eden + Survivor2 。依次循环即可。

       缺点:很容易发现吧,Survivor中每次都会浪费一个Survivor的内存没有使用,所以为了减少浪费,一般将Eden的内存扩大,Survivor的内存设置小一点。例如:HotSpot(HotSpot是8中的jvm默认虚拟机) 中设置的是 8 : 1 : 1;

    3,标记-整理:看名字是不是感觉很熟悉,没错。跟标记-清除很像,也是直接标记。改算法使用到了前面两个算法的精华,改善了缺点。

        算法流程:1),直接标记

                          2),集中,无缝隙的移动到一端,此时会发现,剩下的垃圾类,都会在其他地方。移动完成之后就会发现有一个边界,就是可用类跟其他空间的一个边界,下一步直接把边界以外的空间直接清除掉就可以了。

       缺点:看起来很完美,但是越完美的,往往在时间上过不去。

    4,分代算法:根据在哪里清除,选用算法不一样。

        算法流程:1),新生代采用复制算法

                          2),老年代采用标记-清除算法(老年代GC很少访问,类也很少去直接分配到里面,内存碎片的可怕性就显得不那么重要了)

什么样的数据会往老年代里面迁移呢;

每次进行垃圾回收的时候,比如说当前这个对象存活下来了,计数器就会给他+1,默认的话,是当计数器达到16的时候就会放到老年代里面;

GC的作用域:

JVM内存分配原则:

        1,对象优先分配到Eden区域;

        2,大对象直接分配到老年区:大对象的就是,对象里面有很大数组或者很大的字符串;

        3,长时间存活的对象存入老年区:就是上面复制算法里面说的那个对象升级流程;

         4,动态对象年龄判定:jvm并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才可以进入老年代,如果Survivor空间中年龄相同的所有对象的总空间>=本servivor中的一半,那么年龄>=本年龄的对象可以直接进入老年区;

        5,空间分配原则:简单来说,就是在发生Minor GC(在新生代进行GC)情况下,为了防止发生在Minor GC后,Eden有大量存活的对象,导致survivor不能全部存入,这时需要老年代去担保,把这些对象放入老年代,但是要确保老年要存的下。

               1),再发生Minor GC之前,检查老年区的可用的连续空间是否是大于新生代(Eden)的所有对象的总空间,如果是,直接全部晋升老年代,保证Minor GC的安全;

               2),如果不行,就检查HandlePromotionFailure(可以手工设定)参数时候允许担保失败,允许的话,直接分配。不能的话,发生一次full GC(或者是Major GC   在老年代进行GC)。

              3),不允许担保失败,发生一次 full GC。

            为什么不直接进行full GC ,因为速度慢呀。而且经常GC 也 效果不大,因为老年代都是一些长期存活的对象。

如果老年代内存也不够用了怎么办呢;

            他会进行fullGC

fullGC的时候会有什么现象吗;有没有遇到到fullGC的时候影响业务的场景;

如果频繁的fullGC会出现cpu内存飙升的问题

     收集器有:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(G1是目前最好的收集器)

上图是HotSpot的垃圾收集器的使用范围,HotSpot是现在主流的 jvm。

CMS

CMS(Concurrent Mark Sweep)一种以获得最短停顿时间为目标的收集器,非常适用B/S系统。

使用 Serial Old 整理内存。

CMS 运行过程:

640?wx_fmt=png

(注:图片来源于零壹技术栈)

1、初始标记

标记 GC Roots 直接关联的对象,需要 Stop The World 。

2、并发标记

从 GC Roots 开始对堆进行可达性分析,找出活对象。

3、重新标记

重新标记阶段为了修正并发期间由于用户进行运作导致的标记变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,也需要 Stop The World 。

4、并发清除

除垃圾对象。

CMS 缺点:

1、对 CPU 资源要求敏感。

CMS 回收器过分依赖于多线程环境,默认情况下,开启的线程数为(CPU 的数量 + 3)/ 4,当 CPU 数量少于 4 个时,CMS 对用户本身的操作的影响将会很大,因为要分出一半的运算能力去执行回收器线程。

2、CMS无法清除浮动垃圾。

浮动垃圾指的是CMS清除垃圾的时候,还有用户线程产生新的垃圾,这部分未被标记的垃圾叫做“浮动垃圾”,只能在下次 GC 的时候进行清除。

3、CMS 垃圾回收会产生大量空间碎片。

CMS 使用的是标记-清除算法,所有在垃圾回收的时候回产生大量的空间碎片。

注意:CMS 收集器中,当老生代中的内存使用超过一定的比例时,系统将会进行垃圾回收;当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时采用 Serial Old 算法进行清除,此时的性能将会降低。

线程类型: 多线程

使用算法: 标记-清除

指定收集器: -XX:+UseConcMarkSweepGC

G1

G1 GC 这是一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。

G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。

640?wx_fmt=png

G1 吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时 CMS 已经在 JDK 9 中被标记为废弃(deprecated),所以 G1 GC 值得深入掌握。

G1 运行过程:

1、初始标记

标记 GC Roots 直接关联的对象,需要 Stop The World 。

2、并发标记

从 GC Roots 开始对堆进行可达性分析,找出活对象。

3、重新标记

重新标记阶段为了修正并发期间由于用户进行运作导致的标记变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,也需要 Stop The World 。

4、筛选回收

首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。这个阶段可以与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的。

线程类型: 多线程

使用算法: 复制、标记-整理

指定收集器: -XX:+UseG1GC(JDK 7u4 版本后可用)

jvm报错,OOM;

JVM 发生OOM的四种情况

1、Java堆溢出:heap

Java堆内存主要用来存放运行过程中所以的对象,该区域OOM异常一般会有如下错误信息;
java.lang.OutofMemoryError:Java heap space
此类错误一般通过Eclipse Memory Analyzer分析OOM时dump的内存快照就能分析出来,到底是由于程序原因导致的内存泄露,还是由于没有估计好JVM内存的大小而导致的内存溢出。

堆占物理虚拟机的四分之一

2、溢出:stack

栈用来存储线程的局部变量表、操作数栈、动态链接、方法出口等信息。如果请求栈的深度不足时抛出的错误会包含类似下面的信息:
java.lang.StackOverflowError

另外,由于每个线程占的内存大概为1M,因此线程的创建也需要内存空间。操作系统可用内存-Xmx-MaxPermSize即是栈可用的内存,如果申请创建的线程比较多超过剩余内存的时候,也会抛出如下类似错误:

java.lang.OutofMemoryError: unable to create new native thread
3、运行时常量溢出   constant
运行时常量保存在方法区,存放的主要是编译器生成的各种字面量和符号引用,但是运行期间也可能将新的常量放入池中,比如String类的intern方法。
如果该区域OOM,错误结果会包含类似下面的信息:
java.lang.OutofMemoryError: PermGen space

4、方法区溢出   directMemory
方法区主要存储被虚拟机加载的类信息,如类名、访问修饰符、常量池、字段描述、方法描述等。理论上在JVM启动后该区域大小应该比较稳定,但是目前很多框架,比如Spring和Hibernate等在运行过程中都会动态生成类,因此也存在OOM的风险。
如果该区域OOM,错误结果会包含类似下面的信息:

java.lang.OutofMemoryError: PermGen space

强引用 、软引用、 弱引用、虚引用分别是什么

 

强引用:基本是用到的95%都是强引用

当内存不足 jvm开始垃圾回收 对于强引用的对象 就算是出现oom也不会对该对象进行回收 死也不收

在java中最常见的就是强引用 把一个对象赋给一个引用变量 这个引用变量就是强引用 当一个对象被强引用变量引用的时 他处于不可达状态 他是不可能被垃圾回收的 这也是强引用造成内存泄漏的主要原因之一

对于一个普通的对象 如果没有其他的引用关系 只要超过引用的作用域或者将相应的强引用赋值为null一般认为就是可以被回收的。

/**
    只会收集obj1
*/
Object obj1=new Object();//默认强引用
Object obj2=obj1;//引用赋值
obj1=null;//置空
Syetem.gc();//垃圾回收

软引用:当内存充足时不会回收 内存不足时回收

/**
内存充足时 软引用不会回收
*/
public static void stofRef_Momory_Enough(){
    Object o1=new Obect();
    SoftReferenceObject> softReerence = new SoftReference>(o1);
    o1=null;
    System.gc();
}

/**
内存不足时 软引用会回收  (-xms5m -Xmx5m -xx:PrintGcDetails)
*/
public static void stofRef_Momory_NotEnough(){
    Object o1=new Obect();
    SoftReferenceObject> softReerence = new SoftReference>(o1);
    o1=null;
    System.out.println(softRefere.get());
    o1=null;
    try{
        byte[] bytes =new byte[30 * 1024 * 1024]
    }catch(Throwable e){
        e.pintStacTrace();
    }finally{
        System.out.println(o1);
        System.out.println(softRefere.get());
    }

}

弱引用:不管内存是否够用 只要有gc一定回收

弱引用需要用java.lang.ref.WeakReference类来实现 他比软引用的生存期更短

对于只有弱引用的对象来说 只要垃圾回收机制一运行 不管JVM的内存空间是否足够 都会回收该对象占用的内存

public class WeakReferenceDemo{

    public static void main(String[] args){
        Object o1=new Object();
        WeakReference<Object> weakReference=new WeakReference<>(o1);
        System.out.println(o1);
        System.out.println(weakReference.get());

        o1=null;
        System.gc();
          
        System.out.println(o1);
        System.out.println(weakReference.get());
    }
}

软引用和弱引用的适用场景

软引用:

假如有一个应用需要读取大量的本地图片

       如果每次读取图片都从硬盘读取则会严重影响性能

       如果一次性全部加载到内存中又可能造成内存溢出

此时使用软引用来解决这个问题。

设计思路是:用一个HashMap来保存图片的路径和相应图片对象的软引用之间的映射关系 在内存不足时 JVM会自动回收这些内存图片对象所占用的空间 从而避免oom的问题

Map<String,SoftReference<Bitmap>> imageCache=new HashMap<String,SoftReference<Bitmap>>();

WeakHashMap:

WeakHashMap<Integer ,String> map =new WeakHashMap<>();
Integer key=new Integer(1);
String value = "HashMap";
map.put(key , value);
key=null;
System.gc();
System.out.println(map);//清空

虚引用:

需要java.lang.ref.PhantomReference类来实现

顾名思义 就是形同虚设 与其他引用都不同 虚引用并不会决定对象的生命周期

如果一个对象仅保持有虚引用 那么他就和没有任何引用一样 在任何时候都可能被垃圾回收 他不同蛋蛋也不能通过他访问对象 虚引用必须和引用队列(ReferenceQueue)联合使用

虚引用的主要作用是跟踪对象被垃圾回收的状态仅仅是提供了一种确保对象被finalize以后 做事情的机制

PhantomReference的get方法总是返回null 因此无法访问对应的引用对象 其意义在于说明 一个对象已经进入finalization阶段 可以被gc回收 用来实现比finalization机制更加灵活的回收操作

换句话说 设置虚引用关联的唯一目的 就是在这个对象被垃圾回收器回收的时候手袋一个系统通知或者后续添加进一步的处理

java技术允许使用finalize()方法在垃圾谁手气将这个对象从内存清除出去之前做必要的清理工作

引用队列(软引用):

Object o1=new Object();
RefereceQueue<Object> refereQueue = new RefenceQueue();
WeakReference<Object> weakReference =new WeakReference<>(o1,referenceQueue);
System.out.println(o1);
System.out.println(weakRerenceQueue.get());
System.out.println(rerenceQueue.poll());

o1=null;
System.gc();
Thread.sleep(500);
System.out.println(o1);//null
System.out.println(weakRerenceQueue.get());//null
System.out.println(rerenceQueue.poll());//回收之前都会放入引用队列里面

引用队列(虚引用):

Object o1=new Object();
RefereceQueue<Object> referenceQueue = new RefenceQueue();
PhantomReference<Object> phantomReference =new PhantomReference<>(o1,referenceQueue);
System.out.println(o1);
System.out.println(phantomReference.get());//null
System.out.println(referenceQueue.poll());//null

o1=null;
System.gc();
Thread.sleep(500);
System.out.println(o1);//null
System.out.println(phantomReference.get());//null
System.out.println(referenceQueue.poll());//回收之前都会放入引用队列里面

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值