当前位置: 首页 > 科技观察

浅谈JVM内存生成和垃圾回收

时间:2023-03-17 18:56:16 科技观察

最近看了周志明老师的书《深入理解 Java 虚拟机》,收获颇丰。以下是阅读上半部分后的一些阅读笔记。结合本书内容,我将简要记录和分享一些相关信息。JVM内存生成和垃圾回收相关内容。大家都知道JVM内存区分为5个部分。有疑问可以参考上一篇文章——JVM内存区介绍。这里也简要列出了JVM的五部分程序计数器。这是一个很小的内存空间。它的作用可以看作是当前线程执行的字节码的行号指示器,线程是私有的。Java虚拟机栈是Java方法执行的内存模型。每个方法被调用到执行完成的过程对应虚拟机栈中一个栈帧从入栈到出栈的过程,线程是私有的。本地方法栈类似于虚拟机栈,但是本地方法栈是用来执行本地方法的,线程是私有的。Java堆的这个区域的唯一用途就是存放对象。应用程序中几乎所有的对象实例都在这里分配内存,由所有线程共享。方法区用于存放虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,为所有线程共享。众所周知OOM,任何应用启动后,操作系统分配给它的内存肯定是有限的,因此如何合理有效地管理内存就显得尤为重要。从上一节可以看出,我们一般讨论的对象内存分配是发生在Java堆上的。所以这里所说的内存管理在大多数情况下指的是Java堆内存。程序计数器和虚拟机栈是随着线程一起出生和死亡的,所以它们的内存比较容易管理,出问题的也比较少。一个应用程序启动后,会不断地运行,执行命令,创建对象,而这些对象大部分都存放在堆内存区。这部分区域的大小是有限的,但是需要生成的对象是无限的。当创建了一次对象,发现堆内存中没有空间可以创建对象时,JVM会突发OutOfMemoryError异常(以下统称OOM),程序会挂掉。以上只是为了说明外观。其实OOM远没有上面说的那么简单。如果你想了解OOM,这里还有一些其他的知识需要解释一下。在OOM发生之前,JVM实际上会进行内存垃圾回收(GC)。垃圾回收有许多不同的实现算法。为了更好的管理内存,堆内存被分代。新生代和老年代的堆内存垃圾回收算法不一致。其实这里的知识需要全面的了解,才能对OOM有全面的了解。内存生成当应用程序启动时,操作系统会为其分配一个初始内存大小。从上面可以看出,这部分内存应该大部分属于堆内存。为了更好的利用和管理这部分内存,JVM对这部分内存进行了划分。一部分成为年轻一代,另一部分称为老一代。一开始,对象的创建发生在新生代。随着对象的不断创建,如果新生代中没有空间创建新的对象,就会发生GC。此时的GC称为MinorGC。新生代中的对象每次经过MinorGC后,如果该对象还没有被回收,则将其标记号加1。这个标记号用来标识对象经历了多少次MinorGC。对于Sun的Hotspot虚拟机,如果超过15个,则将Objects移动到老年代。随着时间的推移,如果老年代没有足够的空间容纳对象,老年代也会尝试发起GC,此时的GC称为FullGC。与MinorGC相比,FullGC发生的频率较低,但每次FullGC发生时,都需要对整个堆内存区域进行垃圾回收,对程序性能的影响比MinorGC大得多。所以我们应该尽量避免或减少FullGC的发生。同时,在堆内存区,最频繁的GC情况就是新生代的MinorGC,因为所有的对象都会先去新生代开辟空间,所以这块内存变化很快,并且只有当内存不够用的时候才会发生GC,但是一般MinorGC的执行速度要比FullGC快很多。为什么?因为新生代和老年代的垃圾回收算法是不一样的。垃圾收集算法标记-清除算法(Mark-Sweep)这是最基本的收集算法。就像它的名字一样,该算法分为两个阶段:“标记”和“清除”。首先,标记所有需要回收的对象。标记完成后,统一收集所有标记对象。之所以说它是最基础的集合算法,是因为后来的集合算法都是基于这个思想,在其不足之处进行改进。它有两个主要缺点:一是效率问题,标记和清除过程的效率不高;另一种是空间问题,标记和清除后会产生大量不连续的内存碎片,过多的空间碎片可能会导致,当程序在后面的运行过程中需要分配更大的对象时,找不到足够的连续内存并且必须提前触发另一个垃圾收集动作。复制算法(Copying)为了解决效率问题,出现了一种叫做“复制”(Copying)的收集算法,它将可用内存按照容量分成两块大小相等的,每次只使用其中一块.当这块内存用完后,将存活的对象复制到另一块中,然后一次性清理掉已使用的内存空间。这样每次就回收一块内存,分配内存时就不用考虑内存碎片等复杂情况。只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这个算法的代价是将内存减少到原来的一半大小,有点太高了。但是这种算法的效率是相当高的,所以目前商业化的虚拟机都是采用这种收集算法来回收新生代。为什么新生代可以使用复制算法呢?根据IBM的专项研究,新生代中98%的对象都是生死存亡的,所以没有必要按照1:1的比例来划分内存空间。鉴于此,新一代采用如下划分策略。现在将新生代分为三个部分,一个较大的伊甸园(Eden)和两个较小的幸存者(survivor)区域。回收时,一次性将Eden和Survivor中的存活对象复制到另一个Survivor空间,最后清理掉刚刚使用过的Eden和Survivor空间。HotSpot虚拟机默认的Eden和Survivor比例为8:1,即每次新生代中的可用内存空间为整个新生代容量的90%(80%+10%),只有10%内存会很“浪费”,清理完成后,原来的Survivor就空了,一直空到下一次MinorGC,它就作为存活对象的地方。这样,两个Survivor就拿走了成为GC过程中新生代存活对象的中转站,但是如果使用复制算法的内存区域存在大量存活对象,复制算法就会变得不堪重负,此时需要更大的Survivorarea需要存放那些幸存的对象,甚至可能需要1:1的比例。所以对于老年代的堆内存区域,有如下算法。mark-compactalgorithm标记过程还是和“标记清除”算法,但下一步是不是直接清理可回收对象,而是将所有存活的对象移到一端,然后直接清理端边界外的内存。这种方式避免了碎片,不需要额外的内存空间,更适合老年代。但是和复制算法相比,虽然这种算法占用的内存空间少,但是垃圾回收的时间比复制算法要长,所以我们上面也说了要尽量避免或者减少FullGC的发生。这两个算法用精炼的语言描述就是replicationalgorithm:usespacefortimemark-sortingalgorithm:usetimeforspace一句话,鱼和熊掌不能兼得,但是对于新生代和老年代一代,他们是最好的选择。综上所述,简单梳理一下文中提到的一些知识点。为了更好的管理堆内存,这个区域分为新生代和老年代。垃圾收集在年轻代中比在老年代中更频繁地发生。新生代发生的垃圾回收成为MinorGC;发生在老年代的GC变成了FullGC。为了更高效地管理新生代的内存,根据复制算法,结合IBM的研究论证,将新生代分为三个部分,一个比较大的Eden区和两个比较小的Survivor区,比例8:1:1供参考《深入理解 Java 虚拟机》-周志明老师