jvm内存模型、垃圾回收机制

JVM内存模型


JVM内存模型包括:线程计数器、本地方法栈、栈、堆、方法区(元空间),类装载子系统,字节码执行引擎。

  • 线程计数器

    线程启动时,程序技术会分配一小块空间分配给当前线程,每个线程都会独享一块程序计数器空间,用于存储下条指令的单元地址

    程序计数器是一块较小的内存空间,用于存储下条指令的单元地址。当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”

  • 本地方法栈(Native Method Stack)

    native本地方法空间

  • 栈(Java Virtual Machine Stack)

    当线程执行,栈会分配一小块空间给当前线程,通常也被称为线程栈。线程栈中包括局部变量表、操作数栈、动态连接、方法出口。

    局部变量表:存储基本数据类型(int,float,double,long,boolean,byte,short,char),如果是引用数据类型则存储堆中的 内存地址。

    操作数栈:用于运算时的临时空间来存储数据。

    动态链接:将代码的符号引用转换为在方法区(运行时常量池)中的直接引用。

    方法出口:存储了栈帧中的方法执完之后回到上一层方法的位置。

  • 堆(heep)

    堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。

  • 方法区/元空间(Method Area)

    方法区是所有线程共享的区域,存储有常量、静态变量、类信息、运行时常量池,操作的是直接内存。方法区域的内存可以是不连续的

  • 类装载子系统(Class Loader SubSystem)

  • 字节码执行引擎(Execution Engine)

    字节码执行引擎是JVM的核心部分之一,它负责执行字节码。字节码解释器以解释的方式执行指令。线程计数器也是由字节码执行引擎来控制的

oracle官网地址

JVM运行原理

举例代码:

public class Math {

    public static final int initData = 666;

    public static User user = new User();

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
        System.out.println("end");
    }
}

JVM垃圾回收机制

标记垃圾算法

1.引用计数法
  • 通过判断对象的引用的数量来决定对象是否被回收

  • 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1


优点: 执行效率高,程序执行受影响小

缺点: 无法检测出循环引用的情况,导致内存泄漏

2.根可达性算法

通过判断对象的引用链是否可达来决定对象是否可以被回收。

将程序中所有的引用链视为一张图,从一系列的GC Root 出发,无法到达的节点则标记为可回收。如下图所示

可以作为GC Root的对象:

  • 虚拟机栈中引用的对象(栈帧中的局部变量表)
  • 方法区中的常量引用的对象
  • 方法区中的类静态属性引用的对象
  • 本地方法栈中JNI的引用对象
  • 活跃线程的引用对象

垃圾回收算法

什么是自动垃圾回收?

Automatic garbage collection is the process of looking at heap memory, identifying which objects are in use and which are not, and deleting the unused objects. An in use object, or a referenced object, means that some part of your program still maintains a pointer to that object. An unused object, or unreferenced object, is no longer referenced by any part of your program. So the memory used by an unreferenced object can be reclaimed.

自动垃圾回收是查看堆内存、识别哪些对象正在使用、哪些对象未使用以及删除未使用对象的过程。正在使用的对象或引用的对象意味着程序的某些部分仍保留指向该对象的指针。程序的任何部分都不再引用未使用的对象或未引用的对象。因此,可以回收未引用对象使用的内存。

In a programming language like C, allocating and deallocating memory is a manual process. In Java, process of deallocating memory is handled automatically by the garbage collector. The basic process can be described as follows.

在像 C 这样的编程语言中,分配和取消分配内存是一个手动过程。在 Java 中,释放内存的过程由垃圾回收器自动处理。基本流程可以描述如下

1:标记清除法 Marking
  • 标记:扫描出未被引用的对象
  • 清除:对堆内存从头到尾进行线性遍历,回收未被引用对象的内存

缺点:碎片化严重

回收前:

回收后:

oracle官网描述:

The first step in the process is called marking. This is where the garbage collector identifies which pieces of memory are in use and which are not.

该过程的第一步称为标记。这是垃圾回收器标识哪些内存正在使用,哪些未使用的地方。

Referenced objects are shown in blue. Unreferenced objects are shown in gold. All objects are scanned in the marking phase to make this determination. This can be a very time consuming process if all objects in a system must be scanned.

引用的对象以蓝色显示。未参照的对象以金色显示。在标记阶段扫描所有对象以做出此决定。如果必须扫描系统中的所有对象,这可能是一个非常耗时的过程。

2. 拷贝算法 Normal Deletion

将管理的内存块分为多个块区,在其中一部分块区中创建对象分配内存(我们称之为对象面)。当对象面内存用完时,将存活的对象复制到没有使用的那些块区(称之为空闲面),再将对象面的所有对象内存清除。适用于对象存活率较低的场景,比如新生代。

  • 解决了碎片化问题

  • 顺序分配内存,简单高效

  • 适用于对象存活率低的场景

oracle官网描述:

Normal deletion removes unreferenced objects leaving referenced objects and pointers to free space.
正常删除会删除未引用的对象,将引用的对象和指针保留到可用空间。

3.标记压缩算法 Deletion with Compacting
  • 标记: 从根集合进行扫描,对存活对象进行标记
  • 整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收

oracle官网描述:

To further improve performance, in addition to deleting unreferenced objects, you can also compact the remaining referenced objects. By moving referenced object together, this makes new memory allocation much easier and faster.

为了进一步提高性能,除了删除未引用的对象外,还可以压缩剩余的引用对象。通过将引用的对象移动在一起,这使得新的内存分配变得更加容易和快速。

为什么要进行分代回收

oracle官网是这么说的:

As stated earlier, having to mark and compact all the objects in a JVM is inefficient. As more and more objects are allocated, the list of objects grows and grows leading to longer and longer garbage collection time. However, empirical analysis of applications has shown that most objects are short lived.

如前所述,必须标记和压缩 JVM 中的所有对象是低效的。随着分配的对象越来越多,对象列表越来越大,导致垃圾回收时间越来越长。然而,对应用程序的实证分析表明,大多数对象都是短暂的

  1. Here is an example of such data. The Y axis shows the number of bytes allocated and the X access shows the number of bytes allocated over time.
  2. 下面是此类数据的示例。Y 轴显示分配的字节数,X 访问显示随时间推移分配的字节数。


As you can see, fewer and fewer objects remain allocated over time. In fact most objects have a very short life as shown by the higher values on the left side of the graph.
正如你所看到的,随着时间的推移,分配的对象越来越少。事实上,大多数物体的寿命都非常短,如图左侧较高的值所示。

JVM 代

年轻代

​ 也叫新生代,顾名思义,主要是用来存放新生的对象。新生代又细分为 Eden区、s0区、s1区。

​ 新创建的对象都会被分配到Eden区(如果该对象占用内存非常大,则直接分配到老年代区), 当Eden区内存不够的时候就会触发MinorGC(Survivor满不会引发MinorGC,而是将对象移动到老年代中),

​ 在Minor GC开始的时候,对象只会存在于Eden区和s0区,s1区是空的。

​ Minor GC操作后,Eden区如果仍然存活(判断的标准是被引用了,通过GC root进行可达性判断)的对象,将会被移到s1区。而s0区中,对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是15)的对象会被移动到年老代中,否则对象会被复制到s1区。经过这次GC后,Eden区和s1区已经被清空。

​ s0区和s1区互换角色,原s1成为下一次GC时的s1区, 总之,GC后,都会保证s1区是空的。

​ 奇怪为什么有 s0和s两块区域?这就要说到新生代Minor GC的算法了,拷贝算法把内存区域分为两块,每次使用一块,GC的时候把一块中的内容移动到另一块中,原始内存中的对象就可以被回收了,优点是避免内存碎片

老年代

​ 随着Minor GC的持续进行,老年代中对象也会持续增长,导致老年代的空间也会不够用,最终会执行Major GC(MajorGC 的速度比 Minor GC 慢很多很多,据说10倍左右)。Major GC使用的算法是:标记清除(回收)算法或者标记压缩算法

​ 当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异

永久代(元空间)

​ 在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间,Metaspace)的区域所取代。

​ 值得注意的是:元空间并不在虚拟机中,而是使用本地内存(之前,永久代是在jvm中)。这样,解决了以前永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出,毕竟是和老年代共享堆空间。java8后,永久代升级为元空间独立后,也降低了老年代GC的复杂度。

oracle官网的解释

The information learned from the object allocation behavior can be used to enhance the performance of the JVM. Therefore, the heap is broken up into smaller parts or generations. The heap parts are: Young Generation, Old or Tenured Generation, and Permanent Generation

从对象分配行为中获取的信息可用于增强 JVM 的性能。因此,堆被分解成更小的部分或几代。堆部分是:年轻一代、老一代或终身一代以及永久一代

The Young Generation is where all new objects are allocated and aged. When the young generation fills up, this causes a *minor garbage collection*. Minor collections can be optimized assuming a high object mortality rate. A young generation full of dead objects is collected very quickly. Some surviving objects are aged and eventually move to the old generation.
年轻一代是所有新物品被分配和老化的地方。当年轻一代填满时,这会导致轻微的垃圾收集。假设对象死亡率高,则可以优化次要集合。充满死物的年轻一代很快就被收集起来。一些幸存的物品已经老化,最终会转移到老一代。

Stop the World Event - All minor garbage collections are "Stop the World" events. This means that all application threads are stopped until the operation completes. Minor garbage collections are always Stop the World events.
停止世界事件 - 所有次要垃圾回收都是“停止世界”事件。这意味着所有应用程序线程都将停止,直到操作完成。次要垃圾回收始终是“停止世界”事件。

The Old Generation is used to store long surviving objects. Typically, a threshold is set for young generation object and when that age is met, the object gets moved to the old generation. Eventually the old generation needs to be collected. This event is called a *major garbage collection*.
老一代用于存储长期幸存的物品。通常,为年轻一代对象设置一个阈值,当达到该年龄时,该对象将移动到老一代。最终,老一代需要被收集起来。此事件称为主要垃圾回收。

Major garbage collection are also Stop the World events. Often a major collection is much slower because it involves all live objects. So for Responsive applications, major garbage collections should be minimized. Also note, that the length of the Stop the World event for a major garbage collection is affected by the kind of garbage collector that is used for the old generation space.
主要的垃圾收集也是停止世界事件。通常,主要集合的速度要慢得多,因为它涉及所有活动对象。因此,对于响应式应用程序,应尽量减少主要垃圾回收。另请注意,主要垃圾回收的“停止世界”事件的长度受用于旧代空间的垃圾收集器类型的影响。

The Permanent generation contains metadata required by the JVM to describe the classes and methods used in the application. The permanent generation is populated by the JVM at runtime based on classes in use by the application. In addition, Java SE library classes and methods may be stored here.
永久生成包含 JVM 所需的元数据,用于描述应用程序中使用的类和方法。永久生成由 JVM 在运行时根据应用程序使用的类进行填充。此外,Java SE 库类和方法可以存储在此处。

Classes may get collected (unloaded) if the JVM finds they are no longer needed and space may be needed for other classes. The permanent generation is included in a full garbage collection.
如果 JVM 发现不再需要类,并且可能需要空间来存储其他类,则可能会收集(卸载)类。永久生成包含在完整的垃圾回收中。

GC的分类
  • Minor GC:年轻代的垃圾收集,采用复制算法。执行较为频繁,尽可能快速地回收生命周期短的对象。一般回收速度较快。

  • Major GC: 老年代回收。Major GC次数和时间指标很能显示系统性能问题。如果这两个指标很大,很大程度上说明了程序中有问

    ​ 题,垃圾一直回收不掉。

  • Full GC:是清理整个堆空间—包括年轻代和老年代。

垃圾收集的影响

STW(Stop The World):GC导致的暂停用户线程

常见的垃圾回收器

年轻代垃圾回收器

1.Serial收集器(-XX:UseSerialGC, 拷贝算法)

  • 单线程收集,进行垃圾收集时,必须暂停所有工作线程

  • 简单高效,Client模式下默认的年轻代收集器

    2.ParNew收集器(-XX:UseParNewGC, 拷贝算法)

  • 多线程收集,其余的行为特点和Serial收集器一样

  • 与CMS搭配,使用在用户体验优先的程序中

    3.Parallel Scavenge收集器 (-XX:UseParallelGC, 拷贝算法)

  • 比起关注用户线程停顿时间,更关注系统的吞吐量(这里吞吐量是指 运行用户代码时间/(运行用户代码时间+垃圾收集时间))

  • 在多核下执行有优势,Server模式下默认的年轻代收集器

老轻代垃圾回收器

1.Serial Old收集器(-XX:UseSerialOldGC, 标记-整理算法)

  • 单线程收集,进行垃圾收集时,必须暂停所有工作线程

  • 简单高效,Client模式下默认的老年代收集器

    2.Parallel Old收集器(-XX:UseParallelOldGC,标记-整理算法)

  • 多线程, 吞吐量优先

    3.CMS收集器(-XX:UseConcMarkSweepGC, 标记清除算法)

CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器,对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。

回收流程:

  • 初始标记:在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
  • 并发标记:这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
  • 并发预清理:并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。
  • 重新标记:这个阶段会stop the world,收集器线程扫描在CMS堆中剩余的对象。扫描从"根对象"开始向下追溯,并处理对象关联。
  • 并发清理:清理垃圾对象,收集器线程和应用程序线程并发执行
  • 并发重置:这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。

CMS不会整理、压缩堆空间,这样就带来一个问题:经过CMS收集的堆会产生空间碎片,CMS不对堆空间整理压缩节约了垃圾回收的停顿时间,但也带来的堆空间的浪费。为了解决堆空间浪费问题,CMS回收器不再采用简单的指针指向一块可用堆空 间来为下次对象分配使用。;而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来hold住这个对象。

G1 收集器(-XX: UseG1GC, 复制+ 标记整理算法,独立的垃圾回收器包含新生代、老年代)

​ G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。JDK 9以后的默认垃圾收集器,取代了CMS 回收器。
oracle官网地址

常用的调优参数

  • -XX:SurvivorRatio: Eden和Survivor的比值,默认8:1
  • -XX:NewRatio: 老年代和新生代内存比例,默认2:1
  • -XX:MaxTenuringThreshold: 新生代进入老年代的年龄值, 默认15
  • -XX: PretenuerSizeThreshold: 多大的对象直接分配在老年代
  • -Xms:初始化堆内存大小
  • -Xmx:最大堆内存大小
  • -Xss:每个线程栈空间大小
posted @ 2024-05-11 10:47  呀啦嗦  阅读(58)  评论(0编辑  收藏  举报