ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] ## 内存模型 ![](https://img.kancloud.cn/67/a0/67a09c3c5d7c5b7fadf7aa4fb178dc7f_459x299.png) ### 线程私有的数据区 线程私有的数据区 包括 **程序计数器** 、 **虚拟机栈** 和 **本地方法栈** 三个区域,它们的内涵分别如下: ### 程序计数器 我们知道,线程是CPU调度的基本单位。在多线程情况下,当线程数超过CPU数量或CPU内核数量时,线程之间就要根据 时间片轮询抢夺CPU时间资源。也就是说,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,**为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记录其正在执行的字节码指令地址**。 因此,程序计数器是线程私有的一块较小的内存空间,其可以看做是当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的字节码指令的地址;如果正在执行的是 Native 方法,则计数器的值为空。 程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域。 ### 虚拟机栈 虚拟机栈描述的是Java方法执行的内存模型,是线程私有的。**每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息**,而且 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。其中,局部变量表主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和 对象句柄,它们可以是方法参数,也可以是方法的局部变量。 虚拟机栈有两种异常情况:StackOverflowError 和 OutOfMemoryError。我们知道,一个线程拥有一个自己的栈,这个栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss 参数可以设置虚拟机栈大小),若线程请求的栈深度大于虚拟机允许的深度,则抛出 StackOverFlowError 异常。此外,栈的大小可以是固定的,也可以是动态扩展的,若虚拟机栈可以动态扩展(大多数虚拟机都可以),但扩展时无法申请到足够的内存(比如没有足够的内存为一个新创建的线程分配栈空间时),则抛出 OutofMemoryError 异常。下图为栈帧结构图: ![](https://img.kancloud.cn/ee/fb/eefb6c8f669fbfbc2f9d1eb8f11f2e4e_569x265.png) ### 本地方法栈 本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 Native 方法服务。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。 ### 线程共享的数据区 线程共享的数据区 具体包括 Java堆 和 方法区 两个区域,它们的内涵分别如下 ### 方法区 方法区与Java堆一样,也是线程共享的并且不需要连续的内存,**其用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据**。方法区通常和永久区(Perm)关联在一起,但永久代与方法区不是一个概念,只是有的虚拟机用永久代来实现方法区,这样就可以用永久代GC来管理方法区,省去专门内存管理的工作。根据Java虚拟机规范的规定,当方法区无法满足内存分配的需求时,将抛出 OutOfMemoryError 异常。 **运行时常量池**(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用。其中,字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等;而符号引用则属于编译原理方面的概念,包括以下三类常量:类和接口的全限定名、字段的名称和描述符 和 方法的名称和描述符。因为运行时常量池(Runtime Constant Pool)是方法区的一部分,那么当常量池无法再申请到内存时也会抛出 OutOfMemoryError 异常。 运行时常量池相对于Class文件常量池的一个重要特征是具备动态性。Java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如字符串的手动入池方法intern()。 ### 方法区的回收 方法区的内存回收目标主要是针对 **常量池的回收** 和 **对类型的卸载**。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。 判定一个常量是否是“废弃常量”比较简单,**而要判定一个类是否是“无用的类”的条件则相对苛刻许多**。类需要同时满足下面3个条件才能算是“无用的类”: * 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例; * 加载该类的ClassLoader已经被回收; * 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述3个条件的无用类进行回收(卸载),这里说的仅仅是“可以”,**而不是和对象一样,不使用了就必然会回收**。特别地,在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出 ### Java 堆 Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存。Java堆是线程共享的,类的对象从中分配空间,这些对象通过new、newarray、 anewarray 和 multianewarray 等指令建立,它们不需要程序代码来显式的释放。 由于Java堆唯一目的就是用来存放对象实例,因此其也是垃圾收集器管理的主要区域,故也称为称为 GC堆。从内存回收的角度看,由于现在的垃圾收集器基本都采用分代收集算法,所以为了方便垃圾回收Java堆还可以分为 新生代 和 老年代 。新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,对象就会被移入老年代。新生代又可进一步细分为 eden、survivorSpace0 和 survivorSpace1。刚创建的对象都放入 eden,s0 和 s1 都至少经过一次GC并幸存。如果幸存对象经过一定时间仍存在,则进入老年代。 ![](https://img.kancloud.cn/7f/e5/7fe5c102cea43c2b86e928d8e2e291ae_486x122.png) ### Java堆 与 方法区的区别 Java堆是 Java代码可及的内存,是留给开发人员使用的;而非堆(Non-Heap)是JVM留给自己用的,所以方法区、JVM内部处理或优化所需的内存 (如JIT编译后的代码缓存)、每个类结构 (如运行时常量池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。 ## CG JVM 内存模型一共包括三个部分:堆 ( Java代码可及的 Java堆 和 JVM自身使用的方法区)、栈 ( 服务Java方法的虚拟机栈 和 服务Native方法的本地方法栈 ) 和 保证程序在多线程环境下能够连续执行的程序计数器。特别地,我们当时就提到Java堆是进行垃圾回收的主要区域,故其也被称为GC堆;而方法区也有一个不太严谨的表述,就是永久代。总的来说,堆 (包括Java堆 和 方法区)是 垃圾回收的主要对象,特别是Java堆。 ### 哪些内存需要回收 两种经典算法: 引用计数法 和 可达性分析算法 #### 引用计数算法 引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。 引用计数算法是垃圾收集器中的早期策略。在这种方法中,堆中的每个对象实例都有一个引用计数。当一个对象**被创建时**,且将该对象实例分配给一个引用变量,该对象实例的**引用计数设置为 1**。当任何其它变量被赋值为这个对象的引用时,对象实例的引用计数加 1(a = b,则b引用的对象实例的计数器加 1),但当一个对象实例的**某个引用超过了生命周期或者被设置为一个新值时**,对象实例的引用计数减 1。特别地,当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减 1。**任何引用计数为0的对象实例可以被当作垃圾收集**。 ![](https://img.kancloud.cn/84/cd/84cd4cc137bcfd89c9dc1002d68bd9c3_1056x479.png) #### 可达性分析算法 可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。 可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的,如下图所示。在Java中,可作为 GC Root 的对象包括以下几种: * 虚拟机栈(栈帧中的局部变量表)中引用的对象; * 方法区中类静态属性引用的对象; * 方法区中常量引用的对象; * 本地方法栈中Native方法引用的对象; ![](https://img.kancloud.cn/9b/61/9b61c21d5ac052546deec7bf91c070df_690x349.png) ### 什么时候回收 ![](https://img.kancloud.cn/7f/e5/7fe5c102cea43c2b86e928d8e2e291ae_486x122.png) 对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区) #### 年轻代 对象被创建时,内存的分配首先发生在年轻代(大对象可以直接 被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉 年轻代上的内存分配是这样的,年轻代可以分为3个区域**:Eden区和两个存活区(Survivor 0 、Survivor 1)。** 1. 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快; 2. 当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的); 3. 此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0; 4. 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。 5. 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。 #### 年老代 对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC #### 永久代 永久代的回收有两种:常量池中的常量,无用的类信息。 常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点: 1. 类的所有实例都已经被回收 2. 加载类的ClassLoader已经被回收 3. 3类对象的Class对象没有被引用(即没有通过反射引用该类的地方) ### 如何回收 #### 标记清除算法 标记-清除算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收,如下图所示。 ![](https://img.kancloud.cn/01/b1/01b1419d15437129d268d7e9a6bbe82e_558x459.png) 标记-清除算法的主要不足有两个: * 效率问题:标记和清除两个过程的效率都不高; * 空间问题:标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 #### 复制算法 复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如**新生代**。 ![](https://img.kancloud.cn/6d/3c/6d3cc99c6798cb1c865bdc1c92587e2a_738x107.png) #### 标记整理算法 标记整理算法的标记过程类似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(**老年代**) ![](https://img.kancloud.cn/2e/d9/2ed9ee523e61de458bb0016757f3bf63_610x583.png) #### 分代收集算法 **不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率**。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。 ### 不同引用类型的回收状态 #### 强引用 ~~~ Object strongReference = newObject() ~~~ 如果一个对象具有强引用,那垃圾回收器绝不会回收它,当内存空间不足, Java 虚拟机宁愿抛出 OOM 错误,使程序异常 Crash ,也不会靠随意回收具有强引用的对象来解决内存不足的问题.如果强引用对象不再使用时,需要弱化从而使 GC 能够回收,需要: ~~~ strongReference = null; //等 GC 来回收 ~~~ 还有一种情况,如果: ~~~ public void onStrongReference(){     Object strongReference = new Object() } ~~~ 在 onStrongReference() 内部有一个强引用,这个引用保存在 java 栈 中,而真正的引用内容 (Object)保存在 java 堆中。当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为 0 ,这个对象会被回收。 但是如果 mStrongReference 引用是全局时,就需要在不用这个对象时赋值为 null ,因为 强引用 不会被 GC 回收。 #### 软引用 (SoftReference) 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 **软引用可以和一个引用队列ReferenceQueue**联合使用,如果软引用所引用的对象被垃圾回收器回收, java 虚拟机就会把这个软引用加入到与之关联的引用队列中。 ![](https://img.kancloud.cn/d0/5e/d05e1b6adb12ae9bda139929634bd58d_678x664.png) ![](https://img.kancloud.cn/d0/5e/d05e1b6adb12ae9bda139929634bd58d_678x664.png) 注意: 软引用对象是在 jvm 内存不够的时候才会被回收,我们调用 System.gc() 方法只是起通知作用, JVM 什么时候扫描回收对象是 JVM 自己的状态决定的。就算扫描到了 str 这个对象也不会回收,只有内存不足才会回收。 #### 弱引用 (WeakReference) 弱引用与软引用的区别在于: 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,**不管当前内存空间足够与否,都会回收它的内存**。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 **弱引用可以和一个引用队列联合使用**,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 ![](https://img.kancloud.cn/9f/d1/9fd16128dc1fce8f9744d471495e090f_640x664.png) ![](https://img.kancloud.cn/9f/d1/9fd16128dc1fce8f9744d471495e090f_640x664.png) 可见 weakReference 对象的生命周期基本由 GC 决定,一旦 GC 线程发现了弱引用就标记下来,第二次扫描到就直接回收了。 注意这里的 referenceQueuee 是装的被回收的对象。 #### 虚引用 (PhantomReference) ~~~  @Test     public void onPhantomReference()throws InterruptedException{         String str = new String("123456");         ReferenceQueue queue = new ReferenceQueue(); // 创建虚引用,要求必须与一个引用队列关联         PhantomReference pr = new PhantomReference(str, queue);         System.out.println("PhantomReference:" + pr.get());         System.out.printf("ReferenceQueue:" + queue.poll());     } ~~~ 虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列 (ReferenceQueue) 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 #### 总结 | 引用类型 | 调用方式 | GC | 是否内存泄漏 | | --- | --- | --- | --- | | 强引用 | 直接调用 | 不回收 | 是 | | 软引用 | .get() | 视内存情况回收 | 否 | | 弱引用 | .get() | 回收 | 不可能 | | 虚引用 | null | 任何时候都可能被回收,相当于没有引用一样 | 否 | ## 番外:不通过 GC ROOT,仍使用引用计数方式,怎么解决它的循环引用问题? **智能指针**:智能指针以引用计数的方式来标识无用对象,使用智能指针的对象需继承自 RefBase,RefBase 中维护了此对象的强引用数量和弱引用数量。 **若 A 强引用了 B,那 B 引用 A 时就需使用弱引用,当判断是否为无用对象时仅考虑强引用计数是否为 0,不关心弱引用计数的数量** ## 推荐阅读 [学习JVM是如何从入门到放弃的?](https://www.jianshu.com/p/904b15a8281f) [JVM 内存模型概述](https://blog.csdn.net/xinzhou201/article/details/81981282) [图解Java 垃圾回收机制](https://blog.csdn.net/justloveyou_/article/details/71216049)