解决方法:一文详解JVM垃圾收集机制,动图帮你轻松理解大厂面试难点

优采云 发布时间: 2022-10-03 07:18

  解决方法:一文详解JVM垃圾收集机制,动图帮你轻松理解大厂面试难点

  前言

  前面的文章介绍了 JVM 的体系结构和运行时数据区域(内存区域),本文章将介绍 JVM 的重点 - 垃圾回收。众所周知,相比C/C++等语言,Java可以省去手动管理内存的繁琐操作,极大地解放了Java程序员的工作效率,这是由于JVM的垃圾回收机制和内存分配策略。我们在编写程序时通常不会感觉到这一点,但如果是在生产环境中,JVM的不同配置对服务器性能的影响非常大,因此掌握JVM调优是高级Java工程师的必备技能。正如所谓的“基础不扎实,地在摇”,在此之前,我们先来了解一下底层的JVM垃圾回收机制。

  由于您要介绍垃圾回收机制,因此需要澄清以下问题:

  哪些内存区域需要垃圾回收?

  如何判断物品是否可回收?

  如何为新对象分配内存?

  垃圾回收是如何完成的?

  本文将通过以下结构进行扩展,以逐一回答上述问题。

  需要垃圾回收的内存区域;

  确定对象是否可恢复的方法;

  主流垃圾回收算法介绍;

  JVM 的内存分配和垃圾回收机制。

  让我们从正文开始,或者用图片和文字开始旧食谱,然后走上去。

  需要垃圾回收的一、内存区域

  让我们从回顾 JVM 的运行时数据区域开始:

  JVM 运行时数据范围

  其中,程序计数器、Java虚拟机栈和局部方法栈都是线程私有的,对应的线程是共生的,与线程同生,与线程同死,堆栈中的堆栈帧也随着方法的进入和退出有序堆叠和输出。因此,这些区域的内存分配和回收是非常确定的,并且在方法的末尾或线程的末尾,内存也会被释放,因此无需考虑这些区域的内存回收问题。

  堆和方法区域为

  不一样,Java对象几乎都是在堆上创建的,方法区存储着虚拟机加载的类型信息、常量、静态变量、编译器编译的代码缓存等数据,而运行时常量池中的方法区存储着各种文字和符号引用,以上大部分数据都可以在运行时确定, 因此,需要动态内存管理。

  还应该注意的是,JVM中垃圾回收器的主要关注对象是Java堆,因为这里的垃圾回收的“性价比”最高,特别是在新一代(稍后在代际算法上描述)中,它可以一次回收70%-99%的内存。该方法区由于垃圾回收决策条件,特别是类型卸载决策条件相当苛刻,其采集成本性能很低,所以一些垃圾采集器根本不支持或不完全支持垃圾采集的方法区域,如ZGC采集器在JDK 11中不支持类型卸载。

  二、确定对象是否可回收的方法2.1 引用计数

  引用计数方法的实现很简单,向对象添加一个引用计数器,每次有对它的引用时,计数器值就会递增一个;当引用失败时,计数器值减去1;任何具有零时间计数器的对象都不再可以使用。在大多数情况下,此方法有效,但在存在循环引用的情况下,引用计数是无能为力的。例如,在以下情况下:

  public class Student {<br /> // friend 字段<br /> public Student friend = null;<br /> <br /> public static void test() {<br /> Student a = new Student();<br /> Student b = new Student();<br /> a.friend = b;<br /> b.friend = a;<br /> a = null;<br /> b = null;<br /> System.gc();<br /> }<br />}

  上面的代码创建了两个a和b的Dentor实例,并将它们各自的友元字段分配给彼此,除此之外,这两个对象没有引用,然后将它们都分配给null,在这种情况下,这两个对象不能再被访问,但是因为它们相互引用,所以它们的引用计数不为零, 并且引用计数算法无法回收它们。如下图所示:

  循环引用

  但是在Java程序中,a和b可以回收,因为JVM不使用引用计数来确定对象是否可回收,而是使用可访问性分析。

  2.2 可达性分析

  该算法的基本思想是使用一系列称为“GC Roots”的根对象作为起始节点集(GC Root Set),从这些节点开始,根据参考关系向下搜索,搜索过程行进的路径称为“参考链”,如果对象和GC Roots之间没有参考链, 这意味着该物体不再使用,可以回收利用。要执行可访问性分析,您需要首先枚举根节点(GC Roots),并且在枚举根节点的过程中,为了防止对象的引用关系发生变化,需要暂停所有用户线程(垃圾回收以外的线程),并调用暂停所有用户线程的这种行为(停止世界)。可达性分析方法如下图所示:

  可达性分析

  图中的绿色是 GC 根集中的 GC 根,与它们关联的所有对象都是可访问的并标记为蓝色,而所有与它们没有关联的对象都是不可访问的,并标记为灰色。即使无法访问的对象也不一定被回收,如果对象同时满足以下条件,则仍有可能“逃脱”:

  对象具有重写的终止() 方法(Object 类中的方法);

  

  最终化()方法将自身链接到参考链;

  合资管理公司

  以前没有调用对象的 finalizize() 方法(因为 JVM 在采集可回收对象时只调用对象的 finalizize() 方法一次)。

  但是,由于 finalizize() 方法的运行成本高昂、不确定且不保证调用各个对象的顺序,因此不建议使用此方法。那么,GC根在哪里是神圣的呢?在 Java 语言中,可用作 GC 根的固定对象包括:

  虚拟机堆栈中引用的对象(堆栈帧中的局部变量表),例如每个被调用的线程在方法堆栈中使用的参数、局部变量、临时变量等。

  方法区域中类的静态属性所引用的对象,例如 Java 类的引用类型的静态变量。

  由方法区域中的常量引用的对象,如字符串表中的引用。

  本地方法堆栈中 JNI(通常称为本机方法)引用的对象。

  Java 虚拟机内部的引用,例如与基元数据类型对应的类对象、一些常见的异常对象(例如

  空点访问、内存输出错误等,以及系统类加载程序。

  同步关键字保存的所有对象。

  JM XBean、在 JVM TI 中注册的回调、反映 Java 虚拟机内部条件的本地代码高速缓存等。

  三、垃圾回收算法引入了3.1 标记清除算法

  标签清除算法的思想很简单,顾名思义,该算法的过程分为两个阶段:首先标记出所有需要回收的对象,其中标记过程是使用可访问性分析来确定对象是否为垃圾的过程。标记完成后,所有标记的对象被统一回收,幸存的对象也可以反向标记,所有未标记的对象都被统一回收。原理图如下:

  标签清除算法

  虽然这种算法很简单,但它有两个明显的缺点:

  执行效率不稳定。如果Java堆收录大量对象,并且大部分需要回收,则必须执行大量的标记和清除操作,导致标记和清除过程的执行效率随着对象数量的增加而降低;

  导致内存空间碎片。标记和清理后,会出现大量不连续的内存碎片,过多的空间碎片可能导致将来在程序运行期间需要分配大对象时找不到足够的连续内存,不得不提前触发另一次垃圾回收操作,极大地影响了程序运行效率。

  3.2 标签复制算法

  标签复制算法,

  通常被称为复制算法,解决了在面对大量可回收对象时标签清除算法执行效率低下的问题。实现也很容易理解:在可用内存中划分两个相同大小的区域,一次只使用其中一个,另一个块保持空闲状态,当第一个块用完时,所有幸存的对象都被复制到第二个区域,然后第一个块全部清空。如下图所示:

  标记复制算法

  此算法是

  非常适合低对象生存,因为它只关注幸存的对象,而不关心可回收的对象,因此该算法用于JVM中的新一代垃圾回收。但是,它的缺点也很明显,每次都浪费一半的内存太奢侈了,但是JVM中的新一代内存划分更加精细,这更好地解决了这个问题,见下文。

  3.3 标签组织算法

  该算法完美地解决了标记清理算法的空间碎片化问题,其标记过程与“标记干净”算法相同,但下一步不是直接清理可回收对象,而是让所有幸存的对象移动到内存空间端,然后直接清理边界外的内存。

  标记排序规则算法

  虽然这种算法可以很好地解决空间碎片的问题,但每次垃圾回收都需要移动幸存的对象,更新引用这些对象的地方,并且对象移动的操作也需要在整个过程中暂停用户线程(Stop The World)。

  3.4 代际采集算法

  与其说它是一种算法,不如说是一种理论。今天的大多数虚拟机实现都是根据“代际采集”理论设计的,这可以被视为经验性的,因为开发人员在开发过程中发现,JVM中幸存对象的数量与其年龄之间存在一定的规律,如下所示

  :

  

  JVM 中幸存对象的数量与年龄之间的关系

  在此基础上,提出了以下假设:

  绝大多数物体都是生死的。

  一个物体在垃圾回收过程中存活的次数越多,它就越难死亡。

  根据这两个假设,JVM的堆内存大致可以分为新一代和旧时代,大多数新一代对象都有很短的生存时间,每次恢复只关心如何保留少量的生存,而不是标记大量将被回收的物体, 你可以以较低的成本回收到大量的空间,所以这个区域一般使用标记复制算法进行垃圾回收,频率比较高。过去是一些难以死亡的对象,垃圾采集可以使用标记清理和标记分类算法进行,并且频率可以更低。

  根据热点虚拟机的实现情况,新一代和老一代的垃圾回收分为不同的类型,并且有不同的术语,如下所示:

  部分 GC:指目标不是 完全采集的垃圾回收

  整个Java堆,它分为:次要GC/年轻GC:指目标只是新一代的垃圾回收。主要GC/Old GC:指仅针对*敏*感*词*的垃圾采集,目前只有cms采集器的并发采集阶段是单独采集*敏*感*词*的行为。混合GC:目标是从整个新一代以及一些旧一代中采集垃圾回收,目前只有G1采集器才能实践。

  完整 GC:从整个 Java 堆和方法区域采集垃圾回收。

  人们经常混淆Major GC和全GC,但这也是可以理解的,因为这两种GC行为都包括老年垃圾回收,而单独的Major GC采集相对较少,在大多数情况下,只要收录老年采集,就会是一个完整的GC,但最好区分一下。

  四、 JVM 的内存分配和垃圾回收机制

  在之前的准备之后,现在终于可以瞥见JVM的内存分配和垃圾回收机制。

  4. 1 JVM 堆内存的划分

  JVM 堆内存分区,不再从 Java 8 开始永久生成

  Java 堆是由 JVM 管理的最大内存块,也是垃圾回收器的管理区域。大多数垃圾采集器将堆内存划分为上图所示的几个区域,整体分为新生代和旧,比例为1:2,新生代进一步分为伊甸园,从幸存者和到幸存者,默认比例为8:1:1,请注意,您可以通过SvivorRatio参数进行设置。请注意,从 JDK 8 开始,JVM 中不再有永久世代的概念。无论 Java 堆上的区域如何,都只存储对象的实例,而细分 Java 堆的目的只是为了更好地回收内存,或者更快地分配内存。

  4.2 代际采集原理4.2.1 新生代物品的分配和采集

  在大多数情况下,对象优先分配在新生代伊甸园区域中,当 Eden 区域没有足够的空间进行分配时,虚拟机会启动次要 GC。伊甸园、从幸存者和幸存者的比例是8:1:1,这是因为绝大多数物体都在死亡,伊甸园在垃圾采集过程中幸存下来的物体数量不太大,幸存者空间也足够小,每个新生代的可用记忆空间是整个新生代容量的90%(伊甸园的80%加上To的10%)。幸存者),只有来自幸存者的空间,或新一代的10%,被“浪费”。它不会像原创标记复制算法那样浪费一半的内存空间。“幸存者”和“致幸存者”中的空格不是固定的,而是在 S0 和 S1 之间动态转换的,第一个小 GC 选择 S1 作为“到幸存者”,并将 Eden 中幸存的对象复制到其中,将对象的年龄增加 1,请注意,新一代使用的垃圾回收算法是标记复制算法的改进版本。下面是一个*敏*感*词*,注意颜色变化的第一步是为了突出,虚拟机只做标记活体的操作。

  第一个小气相色谱的*敏*感*词*

  在随后的小指导性案例中,S0和S1交替转换为“从幸存者”和“幸存者”,而《伊甸园》和“幸存者”中的幸存对象被复制到“幸存者”中,年龄增加1。如下图所示:

  后续次要 GC 原理图

  4.2.2 升入老年的物品

  在这些情况下,受试者将被提升到老年。

  长期幸存者将进入老年

  每当一个物体在幸存者区域中的小GC中存活时,它的年龄就会增加1年,当它的年龄增加到一定水平(默认为15)时,它就会被提升到中年。对象前进到老年的年龄阈值可以通过参数-XX:MaxTenuringThreshold设置,此参数的最大值为15,因为对象年龄信息存储在对象头中,占4位内存,最大可以表示的数量是15。

  促进长期幸存者进入老年的*敏*感*词*

  2.大型物品可以直接进入老年

  对于大型对象,尤其是长字符串,或具有大量元素的数组,如果在 Eden 中分配,很容易过早地填满 Eden 空间,从而导致次要 GC,并且还存在较大的内存复制开销,用于在 Eden 和两个幸存者之间来回复制。因此,我们可以通过设置虚拟机参数 -XX:预缩文件大小阈值来让大型对象直接进入旧时代。

  3.动态对象的时代

  为了更好地适应不同程序的内存条件,热点虚拟机并不总是要求物体的年龄必须达到-XX:MaxTenuringThreshold才能提升到老年,如果幸存者空间中所有相同年龄的对象大小之和大于幸存者空间的一半, 大于或等于年龄的对象可以直接进入老年,而不必等到 -XX:最大收缩阈值中所需的年龄。

  4.处理促销

  当幸存者空间不足以容纳在次要GC之后存活的物体时,它依靠其他内存区域(实际上,在大多数情况下,是老年)进行分配保证。在次要GC发生之前,虚拟机必须首先检查旧时代的最大可用连续空间是否大于新一代中所有对象的总空间,如果此条件为真,那么这次次要GC可以保证是安全的。如果没有,虚拟机首先检查-XX:处理升级失败参数的设置值是否允许处理提升失败;如果允许,它将继续检查*敏*感*词*的最大可用连续空间是否大于过去晋升为老年的对象的平均大小,如果大于,将尝试次要GC,尽管这次次要GC是有风险的;如果小于或 -XX:处理升级失败设置不允许冒险,则应改为执行完整 GC。

  小技巧:掌握吸引蜘蛛的四大技巧,轻松增加SEO优化的收录量

  SEO优化作为一种实用的推广手段,是近年来企业宣传的必备条件。但是,即使你做SEO一样,你也可能不一样,有些人有更多的网站收录,几十万,有些人收录很少,甚至一页,后者显然比前者遭受的损失更大。那么,如何提高网页收录呢?如何让蜘蛛抓取更多网站页面?以下是与您分享的四个技巧。

  技能一、合理稳定地提高网站基本体重

  高重量的蜘蛛经常抓取,而低重量的蜘蛛以较低的速度爬行。如果想让蜘蛛抓得更多,就得考虑增加网站的重量,新站的重量很容易达到1,只要网站质量通过,技术是有规律的,达到重量1的速度非常快,至于从1到2, 它要慢得多。不过,对于SEO网站管理员来说,这也足够了。

  

  提示二、网站内容更新频率合理

  蜘蛛总是喜欢更新频率高的网站,因为它们每天都会来抓,如果你的更新频率降低,蜘蛛会随着时间的推移而变得懒惰。当然,如果能每天更新定点就更好了,通过日志分析,你会发现蜘蛛会多次来到这个点。当然,如果您不知道蜘蛛何时会爬行,担心您的更新无效,建议您通过日志分析获取蜘蛛的抓取规则,这样可以更方便蜘蛛抓取收录页面。当然,蜘蛛每次抓取页面都会在数据库中存在,下次抓取时会比较之前的数据,如果没有更新,蜘蛛会降低抓取的频率,甚至不抓取。随着时间的流逝,蜘蛛将不再光顾你的网站。如果你之前网站更新得很低,现在想提高收录,让蜘蛛抓得更多,建议在固定更新频率的基础上,给一些文章发送一些网站链接,这样蜘蛛就可以被引到你的网站,增加爬行的机会。

  提示三、朋友链和外部链接选择最好的

  很多SEO优化者觉得做优化就是做外部链接,所以不管站内,一味追求站外。这种发送大量反向链接的做法在开始时确实产生了一定的效果,但是随着算法的变化,现在已经没有效果了。当然1,反向链接有利于网站排名和收录,适当地添加反向链接对网站有好处。但是,做SEO并不像发送外部链接那么简单,有能力的SEO优化器,即使不发送外部链接,也可以有很好的排名,这就是为什么一些网站优化器觉得外部链接没用的原因。如果你想让蜘蛛知道你网站链接,你需要去一些蜘蛛经常爬行的地方放网站链接,比如百度有专门的网站链接提交平台,它提交网站链接,帮助蜘蛛抓取收录。你也可以在贴吧和其他地方发布链接,至于你是否可以保留它取决于个人技能。至于好友链,它是一种外部链接,但实际效果比外部链接高,因为好友链往往是一些排名相似的相关链接,带来更多的权重和好处。

  

  技巧四、科学地控制对页面的访问深度

  一方面,深度越深,

  对于用户来说,搜索和点击越困难,另一方面,蜘蛛爬行并不容易,更不用说收录了,所以通常,每个人都会控制目录的第二和第三层的页面深度,而不是没有。当然,也有一些深度比较大的网站收录,往往是行业网络或者门户网络,单个企业站点能做的很少。

  看完这四点,不知道你有没有掌握网站 收录爬行的技巧?这里也提醒大家,蜘蛛喜欢高质量的原创文章,尽量更新高质量的原创文章,增加收录好。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线