算法 自动采集列表(Java虚拟机-维基百科,自由的百科全书不过,作为一个爱思考的在校大学生)

优采云 发布时间: 2022-02-12 00:06

  算法 自动采集列表(Java虚拟机-维基百科,自由的百科全书不过,作为一个爱思考的在校大学生)

  前言如果这篇文章有什么不对的地方,欢迎各界diss~~当然,如果你看了这篇文章有收获,那就疯狂点赞吧,你的喜欢是对我最大的鼓励。顺便加个关注,回家别迷路,不定时更新博客~~

  周志明的《深入理解JAVA虚拟机》这本书看了一遍又一遍,终于有勇气在这里写一篇关于JVM的博客了!!!现在,我将在这里开始记录我所了解的并与我的朋友分享!!!

  相信点击这个文章的小伙伴们一定知道JVM是什么吧?什么,还不知道?好吧,看看我想你会明白的 wiki:Java 虚拟机 - 维基百科,免费的百科全书

  不过,作为一个有思想的大学生,我也总结了以下三点:

  关于JVM是什么的介绍,还是一样的,我们来看看这个文章的结构:

  

  运行时数据区

  什么是运行时数据区?

  Java程序在运行时,会为JVM分离出一块内存区域,而这块内存区域又可以划分为运行时数据区域。运行时数据区大致可以分为五个部分:

  

  从上图中可以看出,有两个颜色不同的区域,红色的是线程共享区域,绿色的是线程私有区域。让我们一个一个说清楚,但是在学习这部分的时候,最好想一想为什么会有这些区域。仅仅是因为它的存在吗?

  堆

  很多做开发的同学都会特别注意堆和栈。这是否从另一个角度解释了堆和栈的重要性?在这种情况下,让我们从学生们关注的点开始。(客气点,有没有感觉眼角又湿了?)

  先放干货。首先,Java堆区具有以下特点:

  那么OutOfMemoryError什么时候发生,StackOverflowError什么时候发生呢?当虚拟机在扩展堆栈时无法申请足够的内存空间时,会抛出 OutOfMemoryError 异常。如果线程请求的堆栈深度超过了虚拟机允许的最大深度,就会抛出 StackOverflowError 异常。

  其实Java堆区也可以分为新生代和老年代,新生代又可以进一步分为Eden区、Survivor 1区、Survivor 2区。具体比例参数可以看下图。

  

  我看图已经解释的很清楚了,不用再用文字解释了吧?关于Java堆对象的创建,以及什么时候会出现内存泄漏,后面应该写一篇专文文章,这里的话只是一些理论介绍。

  虚拟机堆栈(VM 堆栈)

  Java虚拟机栈也是开发者关注的地方。同样,先放干货:

  同样,如果这个文章反响不错,会在实战演练文章之后单独发布。

  本机方法堆栈

  本机方法堆栈实际上可以与 Java 虚拟机堆栈进行比较。唯一的区别是本机方法堆栈是 Java 程序在调用本机方法时创建堆栈帧的地方。和 JVM 栈一样,这个区域也会抛出 StackOverflowError 和 OutOfMemoryError。

  方法区

  方法区域也应该是一个重点区域。同样,方法区的主要特点如下:

  对于方法区,我认为重点应该放在常量池上。常量池可以分为类文件常量池和运行时常量池。Java程序运行后,类文件中的信息被字节码执行引擎加载到方法区,从而形成运行时常量池。

  另外,说到方法区,有些人可能会将其与永久代和元空间混淆。那么它们之间究竟有什么区别呢?方法区是Java虚拟机规范中的定义,是规范,而永久代是实现,一个是标准,一个是实现。但是Java 8之后就没有永久代了,元空间代替了永久代。

  程序计数器寄存器

  程序计数器非常简单。想必大家都不是Java初学者,大家应该了解线程和进程的概念吧?(灵魂拷问,你懂吗?)不懂没关系,我一句话给你解释。

  进程是资源分配的最小单位,线程是CPU调度的最小单位。一个进程可以收录多个线程。Java线程通过抢占获得CPU的执行权。现在考虑以下场景。

  在某一时刻,线程 A 获得了 CPU 的执行权并开始执行内部程序。但是线程A的程序还没有被执行,某个时刻CPU的执行权被另一个线程B抢走了。后来经过线程A的不懈努力,重新获得了CPU的执行权,难道线程A的程序还要从头开始执行?

  这时候程序计数器就来了,它的作用是记录当前线程执行的位置。这样,当线程重新获得 CPU 的执行权时,就直接从记录的位置开始执行,而分支、循环、跳转、异常处理也都是靠这个程序计数器来完成的。此外,程序计数器具有以下特点:

  对象创建和访问对象创建

  正如我们之前所说,对象是在堆中创建的,通常只需要一个新对象。就这么简单吗?真的没那么简单。有了这样一个新关键字,Java virtual 内部就执行了一系列 sao 操作。

  当虚拟机遇到字节码新指令时,会去运行时常量池中查找实例化对象对应的类是否被加载、解析和初始化。如果没有加载,则先加载类的信息,否则为新对象分配内存。

  内存分配有两种方式:

  以上是两种不同的方法。至于虚拟机使用哪种方法,则取决于虚拟机的类型。

  对象的内存布局

  堆中对象的存储布局可以分为三个部分:

  对象访问位置

  正如我们前面提到的,Java 虚拟机堆栈存储基本数据类型和对象引用。我们已经知道基本的数据类型,那么这个对象引用到底是什么?

  就是这样,对象实例存储在Java堆中,通过这个对象引用我们可以找到对象在堆中的位置。但是,不同的 Java 虚拟机对于如何定位这个对象有不同的方法。

  通常,有两种方法:

  

  

  两种访问对象的方法都有各自的优势。使用直接指针访问,可以直接定位对象,减少了指针定位的时间开销(如果使用句柄,对象会通过句柄池的指针重定位),最大的好处是它是比较快的。但是使用句柄意味着当对象移动时,不需要更改存储在堆栈中的引用,只需要更改句柄池中指向实例数据的指针即可。

  垃圾采集算法理论对象死了吗?

  在上一部分中,我们讨论了对象。一个对象可以被创建,那么这个对象什么时候被销毁呢?一般来说,有两种方法可以判断一个对象是否已经被销毁:

  

  如上图所示,绿色部分的对象在GC Roots的引用链上,不会被垃圾回收器回收。灰色部分的对象不在参考链中,自然确定为可回收对象。

  那么问题来了,这些 GC Roots 是什么?下面列出了可以用作 GC Roots 的对象:

  现在,我们知道哪些物品是可回收的。那么回收对象应该采取什么方法呢?垃圾回收算法主要有三种,分别是mark-sweep、mark-copy和mark-sort。这三种垃圾回收算法其实都比较容易理解。我先介绍一下概念,然后依次总结。

  标记扫描算法

  顾名思义,mark-to-clear算法就是对无效对象进行标记,然后将其清除。如下所示:

  

  对于mark-sweep算法,你肯定会看到垃圾回收之后,堆空间中的碎片很多,并且有不规则的地方。为大对象分配内存时,由于找不到足够的连续内存空间,不得不再次触发垃圾回收。另外,如果Java堆中有大量垃圾对象,垃圾回收中必须进行大量的标记和清除动作,势必会降低回收效率。

  标记--复制算法

  标记复制算法是将Java堆分成两部分,每次垃圾回收只使用其中一个,然后将所有幸存的对象移动到另一个区域。如下所示:

  

  mark-copy算法有一个明显的缺点,就是每次只使用一半的堆空间,导致Java堆空间使用率下降。

  现在Java虚拟机的垃圾采集器大多使用标记复制算法,但是Java堆空间的划分并不是简单的一分为二。

  还记得这张照片吗?

  

  前面讲Java内存结构的时候,提到了Java堆的具体划分,所以现在说一下。

  首先,我们要从两种代际采集理论说起:

  正是这两个代际假设,让设计者对Java堆的划分更加合理。接下来说一下GC的分类:

  好了,知道了GC的分类,是时候了解一下GC的过程了。

  通常,第一次创建的对象存放在新生代的Eden区。第一次触发 Minor GC 时,Eden 区的幸存对象被转移到 Survivor 区的某个区域。以后再次触发 Minor GC 时,Eden 区的对象会连同一个 Survivor 区的对象一起转移到另一个 Survivor 区。可以看出,我们一次只使用了两个Survivor区域中的一个,只是浪费了一个Survivor区域。

  一个对象每经过一次垃圾回收,它的世代年龄就加1。当世代年龄达到15时,直接存入老年代。

  还有一种情况,给大对象分配内存时,Eden区的内存空间不足。这个时候我该怎么办?在这种情况下,大对象将直接进入老年。

  标记排序算法

  mark-to-clean 算法是一种妥协的垃圾采集算法。在对象标记的过程中,执行与前两个相同的步骤。但是标记后,存活对象被移动到堆的一端,存活对象以外的区域可以直接清理掉。这样就避免了内存碎片,也没有堆空间的浪费。但是,每次进行垃圾回收时,都必须暂停所有用户线程,特别是对于老年代的对象,需要更长的回收时间,这对用户体验非常不利。如下所示:

  

  HotSpot的算法详解根节点枚举

  根节点枚举其实就是寻找可以作为GC Roots的对象。在这个过程中,所有的用户线程都必须停止。到目前为止,几乎没有虚拟机可以与用户线程并发执行 GC Roots 遍历。当然,可达性分析算法中寻找参考链最耗时的过程已经可以与用户线程并发执行。那么,为什么需要在根节点枚举的时候停止用户线程呢?

  其实不难考虑。如果GC Roots遍历时用户线程没有挂起,根节点集的对象引用关系还在变化,所以遍历的结果不准确。那么,Java虚拟机在寻找GC Roots时真的需要进行全局遍历吗?

  事实上,情况并非如此。HotSpot 虚拟机可以通过称为 OopMap 的数据结构知道对象引用的存储位置。这样,GC Roots的遍历时间就大大减少了。

  安全点

  安全点是线程可以被中断的点。当我们遍历 GC Roots 时,我们必须停止用户线程。问题是,线程可以停在任何位置吗?为了让线程停在最近的安全点,有两种思路:

  安全区

  安全区是安全点的延伸和延伸。安全点解决了如何停止线程,但没有解决如何让虚拟机进入垃圾回收状态。

  安全区是指在某个代码片段中,可以保证引用关系不会发生变化的区域。因此,一旦线程进入安全区,就可以忽略安全区内的这些线程。当线程离开安全区时,虚拟机检查根节点枚举是否完成。

  记忆套装和卡片纸

  不知道你有没有考虑过这样的问题?既然Java堆分为新生代和老年代,那么会不会有跨代的对象引用呢?如果有跨代,如何解决遍历老年代的GC Roots的问题?

  首先,存在跨代参考。因此,垃圾采集器在年轻代中构建了一个称为memoized set的数据结构,以避免将整个老年代作为GC Roots的扫描范围。

  内存集是一种抽象的数据结构,卡表是内存集的具体实现。这种关系类似于方法区和元空间。

  写屏障

  写屏障的作用很简单,就是维护和更新卡表。

  并发可达性分析

  前面我们提到了为什么要暂停所有用户线程(这个动作也叫Stop The World)?这实际上是为了防止用户线程改变对 GC Roots 对象的引用。试想一下,如果用户线程可以任意将死对象重新标记为活动对象,或者将活动对象标记为死对象,是不是会导致程序出现意外错误。

  经典垃圾采集器

  我知道很多垃圾采集的理论,但具体到某一种垃圾采集器,它的实现并不完全一样。以下是一些常见的垃圾采集器。

  串行采集器

  Serial 采集器是最基本、最古老的采集器。它在垃圾采集期间挂起所有工作线程,直到垃圾采集过程完成。下面是Serial垃圾采集器的运行*敏*感*词*:

  

  ParNew 采集器

  ParNew 垃圾采集器实际上是串行垃圾采集器的多线程版本。这种多线程是 ParNew 垃圾采集器可以使用多个线程进行垃圾采集。

  

  并行清除采集器

  它也是新一代的垃圾采集器,也是基于标记复制算法实现的。它最大的特点是可以控制吞吐量。

  那么什么是吞吐量?

  

  串行老采集器

  Serial Old 采集器是 Serial 采集器的老一代版本。垃圾采集器的工作原理与串行采集器相同。

  

  平行老采集器

  Parallel Old 采集器也是 Parallel Scavenge 采集器的旧版本,支持多线程并发采集。下面是它的运行*敏*感*词*:

  

  cms 采集器

  如前所述,Parallel Scavenge 采集器是一个可以控制吞吐量的垃圾采集器。现在来说说cms采集器,它是一个追求最短暂停时间的垃圾采集器,基于mark-sweep算法。cms 垃圾采集器的操作过程比前面的要复杂一些。整个过程可以分为四个部分:

  垃圾第一采集器

  Garbage First(简称G1))采集器是垃圾采集器发展史上的里程碑式成果,主要面向服务端应用。另外,虽然G 1采集器仍然保留了新生代和老年代的概念,但是新生代和老年代不是固定的,它们是一系列区域的动态采集。

  好了,垃圾采集器的介绍就到这里。至于G 1采集器,还是有很多值得关注的地方。朋友可以查看相关信息。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线