OutOfMemoryError

出现异常:
java.lang.OutOfMemoryError: Java heap space提示对内存异常

  1. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  2. at java.util.Arrays.copyOf(Arrays.java:3210)
  3. at java.util.Arrays.copyOf(Arrays.java:3181)
  4. at java.util.ArrayList.grow(ArrayList.java:265)
  5. at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
  6. at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
  7. at java.util.ArrayList.addAll(ArrayList.java:583)
  8. at com.zy.gm.jvmTest.outOfMonyTest01.main(outOfMonyTest01.java:18)

为什么出现这样的问题?
原因:对象不能被分配到堆内存中
为了解决这种OutOfMemoryError异常,了解完类加载机制我们更需要了解jvm内存模型。

JVM内存结构图

jvm内存模型 - 图1

JVM内存结构主要有三大块:堆内存方法区。堆内存是JVM中最大的一块由年轻代老年代组成,而年轻代内存又被分成三部分,Eden空间From Survivor空间To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。

jvm各区域的内存大小
jvm内存模型 - 图2

控制参数:

  • -Xms设置堆的最小空间大小。
  • -Xmx设置堆的最大空间大小。
  • -XX:NewSize设置新生代最小空间大小。
  • -XX:MaxNewSize设置新生代最大空间大小。
  • -XX:PermSize设置永久代最小空间大小。
  • -XX:MaxPermSize设置永久代最大空间大小。
  • -Xss设置每个线程的堆栈大小

没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制

老年代空间大小 = 堆空间大小 - 年轻代大空间大小

JVM各区域详情

区域详情图

jvm内存模型 - 图3

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。

特点:

  • 线程私有
  • JVM规范中唯一没有规定OutOfMemoryError情况的区域
  • 如果正在执行的是Native 方法,则这个计数器值为空

问题一

为什么没有规定OutOfMemoryError ? 程序计数器存储的是字节码文件的行号,而这个范围是可知晓的,在一开始分配内存时就可以分配一个绝对不会溢出的内存

问题二

为什么执行Native方法,值为空? Native方法大多是通过C实现并未编译成需要执行的字节码指令,也就不需要去存储字节码文件的行号了。


虚拟机栈

  • 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
  • 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
  • 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  • 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

注意:
虚拟机栈是一个后入先出的栈。栈帧是保存在虚拟机栈中的,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素
jvm内存模型 - 图4

栈帧
jvm内存模型 - 图5

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常。


  • Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存
  • Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

堆详情图

jvm内存模型 - 图6

  1. // jvm堆的配置
  2. 1. JVM运行时堆的大小   
  3. -Xms堆的最小值   
  4. -Xmx堆空间的最大值
  5. 2. 新生代堆空间大小调整   
  6. -XX:NewSize新生代的最小值   
  7. -XX:MaxNewSize新生代的最大值   
  8. -XX:NewRatio设置新生代与老年代在堆空间的大小
  9. -XX:SurvivorRatio新生代中Eden所占区域的大小
  10. 3. 永久代大小调整【jdk8元空间】   
  11. -XX:MaxPermSize
  12. 4. 其他  
  13. -XX:MaxTenuringThreshold,设置将新生代对象转到老年代时需要经过多少次垃圾回收,但是仍然没有被回收。
  • 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。
  • 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
  • 堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式
  • 在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。这就是所谓的“堆属性”,并且这个属性对堆中的每一个节点都成立。

例子:

jvm内存模型 - 图7

这是一个最大堆,因为每一个父节点的值都比其子节点要大。10 比 7 和 2 都大。7 比 5 和 1都大。
根据这一属性,那么最大堆总是将其中的最大值存放在树的根节点。而对于最小堆,根节点中的元素总是树中的最小值。堆属性非常有用,因为堆常常被当做优先队列使用,因为可以快速地访问到“最重要”的元素。
注意:
堆的根节点中存放的是最大或者最小元素,但是其他节点的排序顺序是未知的。例如,在一个最大堆中,最大的那一个元素总是位于 index 0 的位置,但是最小的元素则未必是最后一个元素。—唯一能够保证的是最小的元素是一个叶节点,但是不确定是哪一个。


方法区

  • 用于储存已被虚拟机加在的类信息、常量、静态变量、即时编aaa译器编译后的代码
  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
  • 对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。
  • Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。
  • 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
  • JDK1.8使用元空间MetaSpace替代方法去,元空间并不在JVM中,而使用本地内存。
  • 元空间的大小仅受本地内存限制,可以通过以下参数来指定元空间大小 ```java -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值 -XX:MaxMetaspaceSize,最大空间,默认是没有限制的 -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

Java8为什么要将永久代替换成Metaspace?

1、字符串存在永久代中,容易出现性能问题和内存溢出。 2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困 难,太小容易出现永久代溢出,太大则容易导致老年代溢出。 3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。 4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

  1. **注意:**
  2. - jvm在运行应用时要大量使用存储在方法区中的类型信息。在类型信息的表示上,设计者除了要尽可能提高应用的运行效率外,还要考虑空间问题。根据不同的需求,**jvm的实现者可以在时间和空间上追求一种平衡**。
  3. - 方法区是被所有线程共享的,所以必须考**虑数据的线程安全**。假如两个线程都在试图找lava的类,在lava类还没有被加载的情况下,只应该有一个线程去加载,而另一个线程等待。
  4. ![](https://zhangyu-blog.oss-cn-beijing.aliyuncs.com/img/image-20211201092836636.png#crop=0&crop=0&crop=1&crop=1&id=NrKfH&originHeight=279&originWidth=635&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  5. <a name="1b331c33"></a>
  6. ## 对象jvm生命周期
  7. ![](https://zhangyu-blog.oss-cn-beijing.aliyuncs.com/img/1383365-20200311164542466-1534427458.png#crop=0&crop=0&crop=1&crop=1&id=y4mGm&originHeight=456&originWidth=998&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  8. <a name="1709ea74"></a>
  9. # GC垃圾回收机制
  10. <a name="1dc1cd5c"></a>
  11. ## 为什么进行垃圾回收 ?
  12. > 在了解jvm内存模型中,我们知道各个区域都是有一定大小的,那么如果不进行垃圾回收,内存迟早都会被消耗空,程序就无法运行,所以垃圾回收是必须的。
  13. <a name="6fad6fd7"></a>
  14. ## 那些内存需要回收?
  15. > 不可能再被任何途径使用的对象。找到这些对象,通过下面的算法
  16. <a name="1cc16e24"></a>
  17. ### 1. 引用计数法
  18. > 给对象中添加一个**引用计数器**,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。**任何时刻计数值为0的对象就是不可能再被使用的**。
  19. 这种算法使用场景很多,但是,Java中却没有使用这种算法,因为这**种算法很难解决对象之间相互引用**的情况<br />示例:
  20. ```java
  21. // 虚拟机参数 –verbose:gc在虚拟机发生内存回收时在输出设备显示信息
  22. public class GcTest {
  23. private Object instance = null;
  24. private static final int _1MB = 1024 * 1024;
  25. /** 这个成员属性唯一的作用就是占用一点内存 */
  26. private byte[] bigSize = new byte[2 * _1MB];
  27. public static void main(String[] args)
  28. {
  29. GcTest objectA = new GcTest();
  30. GcTest objectB = new GcTest();
  31. objectA.instance = objectB;
  32. objectB.instance = objectA;
  33. objectA = null;
  34. objectB = null;
  35. System.gc(); //显示调用gc()触发fullGC
  36. }
  37. }
  38. /**
  39. 输出GC日志
  40. [GC (System.gc()) 9339K->1010K(251392K), 0.0009745 secs]
  41. [Full GC (System.gc()) 1010K->916K(251392K), 0.0040652 secs]
  42. **/

看到,两个对象相互引用着,但是虚拟机还是把这两个对象回收掉了,这也说明虚拟机并不是通过引用计数法来判定对象是否存活的

2. 可达性分析法

通过GC Root的对象,开始向下寻找,看某个对象是否可达。 能作为GC Root:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。

可达性分析算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的

Java语言中,可以作为GCRoots的对象包括下面几种:

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 被 Synchronized 持有的对象
  • 本地方法栈中JNI(Native方法)引用的对象。

jvm内存模型 - 图8

由图可知,obj8、obj9、obj10都没有到GCRoots对象的引用链,即便obj9和obj10之间有引用链,他们还是会被当成垃圾处理,可以进行回收。

java四种引用状态

在JDK1.2之前,Java中引用的定义很传统:如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过于狭隘,一个对象只有被引用或者没被引用两种状态。我们希望描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次减弱。

1. 强引用

"Object o = new Object()" 就是一种强引用关系,这也是我们在代码中最常用的一种引用关系。无论任何情况下,只要强引用关系还存在,垃圾回收器就不会回收掉被引用的对象。

2. 软引用

描述有些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java中的类SoftReference表示软引用。

3. 弱引用

描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java中的类WeakReference表示弱引用。
「ThreadLocal」 中就使用了弱引用来防止内存泄漏。

4. 虚引用

这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java中的类PhantomReference表示虚引用。
jvm内存模型 - 图9
对于可达性分析算法而言,未到达的对象并非是“非死不可”的,若要宣判一个对象死亡,至少需要经历两次标记阶段。

  1. 如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程去处理。
  2. 对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。如下代码演示了一个对象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。具体代码如下: ```java package com.zy.gm.GcTest;

/**

  • 此代码演示了两点:
  • 1.对象可以再被GC时自我拯救
  • 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 **/

public class FinalizeEscapeGC { public String name; public static FinalizeEscapeGC SAVE_HOOK = null;

  1. public FinalizeEscapeGC(String name) {
  2. this.name = name;
  3. }
  4. public void isAlive() {
  5. System.out.println("yes, i am still alive :)");
  6. }
  7. @Override
  8. protected void finalize() throws Throwable {
  9. super.finalize();
  10. System.out.println("finalize method executed!");
  11. System.out.println(this);
  12. FinalizeEscapeGC.SAVE_HOOK = this;
  13. }
  14. @Override
  15. public String toString() {
  16. return name;
  17. }
  18. public static void main(String[] args) throws InterruptedException {
  19. SAVE_HOOK = new FinalizeEscapeGC("leesf");
  20. System.out.println(SAVE_HOOK);
  21. // 对象第一次拯救自己
  22. SAVE_HOOK = null;
  23. System.out.println(SAVE_HOOK);
  24. System.gc();
  25. // 因为finalize方法优先级很低,所以暂停0.5秒以等待它
  26. Thread.sleep(500);
  27. if (SAVE_HOOK != null) {
  28. SAVE_HOOK.isAlive();
  29. } else {
  30. System.out.println("no, i am dead : (");
  31. }
  32. // 下面这段代码与上面的完全相同,但是这一次自救却失败了
  33. // 一个对象的finalize方法只会被调用一次
  34. SAVE_HOOK = null;
  35. System.gc();
  36. // 因为finalize方法优先级很低,所以暂停0.5秒以等待它
  37. Thread.sleep(500);
  38. if (SAVE_HOOK != null) {
  39. SAVE_HOOK.isAlive();
  40. } else {
  41. System.out.println("no, i am dead : (");
  42. }
  43. }

} / 输出结果: leesf null finalize method executed! leesf yes, i am still alive :) no, i am dead : ( /

  1. 对象拯救了自己一次,第二次没有拯救成功,因为**对象的finalize方法最多被虚拟机调用一次**。此外,从结果我们可以得知,一个堆对象的this(放在局部变量表中的第一项)引用会永远存在,在方法体内可以将this引用赋值给其他变量,这样堆中对象就可以被其他变量所引用,即不会被回收。
  2. <a name="4419165f"></a>
  3. ## 垃圾收集算法
  4. <a name="cf190049"></a>
  5. ### 1. 标记-清除(Mark-Sweep)算法
  6. > 标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象
  7. 阶段一:**【标记】**找出内存中需要回收的对象,并且把它们标记出来。此时堆中所有的对象都会被**扫描一遍**,从而才能确定需要回收的对象,比较耗时<br />![](https://zhangyu-blog.oss-cn-beijing.aliyuncs.com/img/image-20211201151251500.png#crop=0&crop=0&crop=1&crop=1&id=JTvh6&originHeight=184&originWidth=674&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />阶段二:**【清除】** 清除掉被标记需要回收的对象,释放出对应的内存空间。<br />![](https://zhangyu-blog.oss-cn-beijing.aliyuncs.com/img/image-20211201151409291.png#crop=0&crop=0&crop=1&crop=1&id=fyGVe&originHeight=225&originWidth=725&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />**结论:**
  8. 1. 标记和清除两个过程都比较耗时,效率不高,有两次扫描动作
  9. 1. 会**产生大量不连续的内存碎片**,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得**不提前触发另一次垃圾收集动作**。
  10. <a name="685ffc01"></a>
  11. ### 2. 复制(Copying)算法
  12. 复制算法是为了解决效率问题而出现的,**它将可用的内存分为两块,每次只用其中一块(Form/To),当这一块内存用完了,就将还存活着的对象复制到另外一块上面(Form/To),然后再把已经使用过的内存空间一次性清理掉**。这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。复制算法的执行过程如图:<br />![](https://zhangyu-blog.oss-cn-beijing.aliyuncs.com/img/image-20211201152049880.png#crop=0&crop=0&crop=1&crop=1&id=b69L8&originHeight=421&originWidth=931&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  13. 不过这种算法有个缺点,**内存缩小为了原来的一半,这样代价太高了**。现在的商用虚拟机都采用这种算法来**回收新生代**,不过研究表明1:1的比例非常不科学,因此**新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。**每次回收时,将EdenSurvivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。
  14. <a name="906574b3"></a>
  15. ### 3. 标记-整理(Mark-Compact)算法
  16. 复制算法在对象存活率较高的场景下要进行大量的复制操作,效率很低。万一对象100%存活,那么需要有额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,因此一般不能直接选用复制算法。根据老年代的特点,有人提出了另外一种标记-整理算法,过程与标记-清除算法一样,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。标记-整理算法的工作过程如图:
  17. ![](https://zhangyu-blog.oss-cn-beijing.aliyuncs.com/img/image-20211201152444094.png#crop=0&crop=0&crop=1&crop=1&id=CW7lZ&originHeight=414&originWidth=824&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  18. <a name="d8a1086f"></a>
  19. ### 4. 分代收集算法
  20. 用一张图概括一下堆内存的布局<br />![](https://zhangyu-blog.oss-cn-beijing.aliyuncs.com/img/image-20211201153105155.png#crop=0&crop=0&crop=1&crop=1&id=uyy16&originHeight=283&originWidth=558&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  21. **大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法**。
  22. <a name="99f1077c"></a>
  23. ## 垃圾收集器
  24. [垃圾收集器](https://www.cnblogs.com/xiaoxi/p/6486852.html) 待续.......
  25. <a name="594f8f69"></a>
  26. ## GC日志
  27. 每种收集器的日志形式都是由它们自身的实现所决定的,换言之,每种收集器的日志格式都可以不一样。不过虚拟机为了方便用户阅读,将各个收集器的日志都维持了一定的共性,来看下面的一段GC日志:
  28. ```java
  29. [GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
  30. [GC [DefNew: 2242K->0K(2368K), 0.0018814 secs] 2242K->2241K(7680K), 0.0019172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  31. [Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K), [Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  32. Heap
  33. def new generation total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000, 0x0000000006ea0000)
  34. eden space 2176K, 2% used [0x00000000052a0000, 0x00000000052aaeb8, 0x00000000054c0000)
  35. from space 256K, 0% used [0x00000000054c0000, 0x00000000054c0000, 0x0000000005500000)
  36. to space 256K, 0% used [0x0000000005500000, 0x0000000005500000, 0x0000000005540000)
  37. tenured generation total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000, 0x000000000a6a0000)
  38. the space 5312K, 3% used [0x0000000006ea0000, 0x0000000006ed0730, 0x0000000006ed0800, 0x00000000073d0000)
  39. compacting perm gen total 21248K, used 2982K [0x000000000a6a0000, 0x000000000bb60000, 0x000000000faa0000)
  40. the space 21248K, 14% used [0x000000000a6a0000, 0x000000000a989980, 0x000000000a989a00, 0x000000000bb60000)
  41. No shared spaces configured.
  • 日志的开头“GC”、“Full GC”表示这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有Full,则说明本次GC停止了其他所有工作线程(Stop-The-World)。看到Full GC的写法是“Full GC(System)”,这说明是调用System.gc()方法所触发的GC。
  • “GC”中接下来的“[DefNew”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。
  • 后面方括号内部的“310K->194K(2368K)”、“2242K->0K(2368K)”,指的是该区域已使用的容量->GC后该内存区域已使用的容量(该内存区总容量)。方括号外面的“310K->194K(7680K)”、“2242K->2241K(7680K)”则指的是GC前Java堆已使用的容量->GC后Java堆已使用的容量(Java堆总容量)
  • 再往后“0.0269163 secs”表示该内存区域GC所占用的时间,单位是秒。最后的“[Times: user=0.00 sys=0.00 real=0.03 secs]”则更具体了,user表示用户态消耗的CPU时间、内核态消耗的CPU时间、操作从开始到结束经过的墙钟时间。后面两个的区别是,墙钟时间包括各种非运算的等待消耗,比如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以如果看到user或sys时间超过real时间是完全正常的。
  • “Heap”后面就列举出堆内存目前各个年代的区域的内存情况。