首页 > 系统相关 >jvm内存模型、垃圾回收机制

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

时间:2024-05-11 10:56:11浏览次数:17  
标签:对象 回收 GC 内存 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:每个线程栈空间大小

标签:对象,回收,GC,内存,jvm,线程,垃圾
From: https://www.cnblogs.com/MC-Bonnie/p/18177682

相关文章

  • (EDAC) 内存错误检测与纠正
    EDAC即errordetectionandcorrection(错误检测与纠正),是Linux系统内部的机制。在上面的日志中,可以清楚地看出是内存读错误。MC即memorycontroller(内存控制器)。CE则代表correctableerror,是ECC内存中可以纠正的错误,相对地还有UE(uncorrectableerror)。为了摸清是哪些内存出了问题,找......
  • synchronized原理-字节码分析、对象内存结构、锁升级过程、Monitor
    本文分析的问题:synchronized字节码文件分析之monitorenter、monitorexit指令为什么任何一个Java对象都可以成为一把锁?对象的内存结构锁升级过程Monitor是什么、源码查看字节码分析synchronized的3种使用方式作用于实例方法,对对象加锁作用于静态方法,......
  • JVM
    JVM探究请你谈谈你对JVM的理解?java8虚拟机和之前的变化更新?什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?JVM的常用调优参数有哪些?内存快照如何抓取,怎么分析Dump文件?谈谈JVM中,类加载器你的认识?1、JVM的位置2、JVM的体系结构3、类加载器(类装载器......
  • 探讨:ARC(Automatic Reference Counting)与手动内存管理的区别及工作原理
    在iOS和macOS开发中,内存管理是一个至关重要的话题。在过去,手动内存管理是一项繁琐且容易出错的任务,而引入了ARC(AutomaticReferenceCounting,自动引用计数)之后,内存管理变得更加简单和安全。本文将详细讨论ARC和手动内存管理之间的区别,并解释ARC的工作原理。1.ARC与手......
  • 一次glide内存泄漏排查分析
    glide是一款非常优秀的图片加载框架,目前很多项目在使用。提供了非常方法,在此,笔者就不一一列举了,可以到官网查找。目前项目在做内存排查,因为是车机项目,之前开发的时候没有注意内存方面的问题(车机项目你懂的),现在ota期间系统提出让我们优化内存,说出现过应用内存一直增加的情况。一......
  • prometheus JVM监控
     prometheusJVM监控为了使用Prometheus监控JVM,你需要一个能够采集JVM指标的工具,比如jmx_exporter。以下是如何设置jmx_exporter以监控JVM的步骤:下载并运行jmx_exporter:bashwget"https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.16.1/jmx_......
  • Springboot项目镜像制作&传递环境变量、设置hostname、动态设置JVM参数、cmd&entrypoi
    实现制作一个springboot的镜像,并且可以传递环境变量实现动态JVM参数和端口。0.准备&cmd、entrypoint区别1.准备springboot项目一个简单的springboot项目,默认启动8001端口,里面只有一个接口。xxx%curllocalhost:8081indexdocker环境2.CMD、entrypoint区......
  • .NET 缓存:内存缓存 IMemoryCache、分布式缓存 IDistributedCache(Redis)
    .NET缓存:内存缓存IMemoryCache、分布式缓存IDistributedCache(Redis)  .NET缓存里分了几类,主要学习内存缓存、分布式缓存一、内存缓存IMemoryCache1、Program注入缓存builder.Services.AddMemoryCache();2、相关方法及参数Get、TryGetValue、GetOrCreate、GetOrCrea......
  • docker 容器查看jvm参数配置
    1、登录服务器查看有哪些容器dockerps2、进入到相应的容器中dockerexec-it容器ID或名称bash3、找到java线程top 4、使用jcmd查询java也就是jvm的配置jcmdPIDVM.flags这里我查看的是线程1:jcmd1VM.flags 复制信息出来,格式化,具体参数什么意思,可网......
  • 记一次线上Redis内存占用过高、大Key问题的排查
    问题背景在一个风和日丽的下午,公司某项目现场运维同学反馈,生产环境3个Redis的Sentinel集群节点内存占用都很高,达到了17GB的内存占用量。稍加思索,应该是某些Key的Value数据体量过大,占用了过多的内存空间,我们在使用Redis的过程中,单个Value或者单个集合中的元素应该保证不超过10KB,......