以上是两种不同的方法,至于虚拟机使用哪一种方法,这个就取决虚拟机的类型了。
对象的内存布局
对象在堆中的存储布局可以分为三个部分:
-
对象头
-
第一类信息:存储对象自身的运行时数据,例如哈希码、GC分代年龄、锁状态标志等等。
-
第二类信息:指针类型,Java虚拟机通过这个指针来确定该对象是那个类的实例。
-
实例数据:对象真正存储的有效信息。
-
对齐填充:没有实际的意义,起着占位符的作用。
对象的访问定位
我们前面说到过,Java虚拟机栈中存储的是基本数据类型和对象引用。基本数据类型我们已经很清楚了,那么,这个对象引用又是什么鬼?
是这样的,对象实例存储在Java堆中,通过这个对象引用我们就可以找到对象在堆中的位置。但是,对于如何定位到这个对象,不同的Java虚拟机又有不同的方法。
通常情况下,有下面两种方法:
-
使用句柄访问,通常会在Java堆中划分一块句柄池。
-
使用直接指针,这样Java虚拟机栈中存储的就是该对象在堆中的地址。
这两种访问对象的方法各有优势。使用直接指针进行访问,就可以直接定位到对象,减小了一次指针定位的时间开销(使用句柄的话会通过句柄池的指针二次定位对象),最大的好处就是速度更快。但是使用句柄的话,就是当对象发生移动的时候,可以不用改变栈中存储的reference,只需要改变句柄池中实例数据的指针。
垃圾收集算法
论对象已死?
前面一部分我们都在讲对象,一个对象能够被创建,那么这个对象在什么时候被销毁了?通常,判断一个对象是否被销毁有两种方法:
-
引用计数算法: 为对象添加一个引用计数器,每当对象在一个地方被引用,则该计数器加1;每当对象引用失效时,计数器减1。但计数器为0的时候,就表白该对象没有被引用。
-
可达性分析算法: 通过一系列被称之为“GC Roots”的根节点开始,沿着引用链进行搜索,凡是在引用链上的对象都不会被回收。
就像上图的那样,绿色部分的对象都在GC Roots的引用链上,就不会被垃圾回收器回收,灰色部分的对象没有在引用链上,自然就被判定为可回收对象。
那么,问题来了,这个GC Roots又是什么?下面列举可以作为GC Roots的对象:
-
Java虚拟机栈中被引用的对象,各个线程调用的参数、局部变量、临时变量等。
-
方法区中类静态属性引用的对象,比如引用类型的静态变量。
-
方法区中常量引用的对象。
-
本地方法栈中所引用的对象。
-
Java虚拟机内部的引用,基本数据类型对应的Class对象,一些常驻的异常对象。
-
被同步锁(synchronized)持有的对象。
现在,我们已经知道哪些对像是可以回收的。那么又要采取什么方式对对象进行回收呢?垃圾回收算法主要有三种,依次是标记-清除算法、标记-复制算法、标记-整理算法。这三种垃圾收集算法其实也比较容易理解,下面我先介绍概念,然后在依次总结一下。
标记–清除算法
见名知义,标记–清除算法就是对无效的对象进行标记,然后清除。如下图:
对于标记–清除算法,你一定会清楚看到,在进行垃圾回收之后,堆空间有大量的碎片,出现了不规整的情况。在给大对象分配内存的时候,由于无法找到足够的连续的内存空间,就不得不再一次触发垃圾收集。另外,如果Java堆中存在大量的垃圾对象,那么垃圾回收的就必然进行大量的标记和清除动作,这个势必造成回收效率的降低。
复制算法
标记–复制算法就是把Java堆分成两块,每次垃圾回收时只使用其中一块,然后把存活的对象全部移动到另一块区域。如下图:
标记–复制算法有一个很明显的缺点,那就是每次只使用堆空间的一半,造成了Java堆空间使用率的的下降。
现在大部分Java虚拟机的垃圾回收器使用的就是标记–复制算法,但是,对于Java堆空间的划分,并不是简单地一分为二。
还记得这张图么?
前面讲Java内存结构的时候,提到过Java堆的具体划分,那现在就来好好的说一说。
首先得从两个分代收集理论说起:
-
弱分代假说:大多数对象的生命存活时间很短。
-
强分代假说:经过越多次垃圾收集的对象,存活的时间就越久。
正是这两个分代假说,使得设计者对Java堆的划分更加合理。下面,来说一下GC的分类:
-
Minor GC/Young GC:针对新生代的垃圾收集。
-
Major GC/Old GC:针对老年代的垃圾收集。
-
Full GC:针对整个Java堆以及方法区的垃圾收集。
好了,知道了GC的分类,是时候知道GC的流程了。
通常情况下,初次被创建的对象存放在新生代的Eden区,当第一次触发Minor GC,Eden区存活的对象被转移到Survivor区的某一块区域。以后再次触发Minor GC的时候,Eden区的对象连同一块Survivor区的对象一起,被转移到了另一块Survivor区。可以看到,这两块Survivor区我们每一次只使用其中的一块,这样也仅仅是浪费了一块Survivor区。
每经历过一次垃圾回收的对象,它的分代年龄就加1,当分代年龄达到15以后,就直接被存放到老年代中。
还有一种情况,给大对象分配内存的时候,Eden区已经没有足够的内存空间了,这时候该怎么办?对于这种情况,大对象就会直接进入老年代。
标记–整理算法
标记–整理算法算是一种折中的垃圾收集算法,在对象标记的过程,和前面两个执行的是一样步骤。但是,进行标记之后,存活的对象会移动到堆的一端,然后直接清理存活对象以外的区域就可以了。这样,既避免了内存碎片,也不存在堆空间浪费的说法了。但是,每次进行垃圾回收的时候,都要暂停所有的用户线程,特别是对老年代的对象回收,则需要更长的回收时间,这对用户体验是非常不好的。如下图:
HotSpot的算法细节
根节点枚举
根节点枚举,其实就是找出可以作为GC Roots的对象,在这个过程中,所有的用户线程都必须停下。到目前为止,几乎还没有虚拟机可以做到GC Roots遍历与用户线程并发执行。当然,可达性分析算法中最耗时的寻找引用链的过程已经可以做到和用户线程并发执行了。那么,为什么需要在根节点枚举的时候停止用户线程?
其实也不难考虑,如果进行GC Roots遍历的时候,用户线程没有暂停,根节点集合的对象引用关系还在不断发生变化,这样遍历到的结果是不准确的。那么,Java虚拟机在查找GC Roots的时候,是真的需要进行全局遍历?
其实不是这样的,HotSpot虚拟机通过一个叫做OopMap的数据结构,可以知道哪些地方存储了对象引用。这样,大大减小了GC Roots的遍历时间。
安全点
安全点,是线程能够中断的点。我们在GC Roots遍历的时候,是一定要让用户线程停下来的。问题来了,线程是可以在任意位置停下来吗?为了使得线程到达最近的安全点停下来,有两种思路:
-
抢先式中断: 暂停所有的用户线程,如果哪条线程没有在安全点,就恢复这条线程执行,直到它跑到安全点上在中断。不过没有Java虚拟机采用这种思路。
-
主动式中断: 不对线程进行操作,仅仅设置一个简单的标志位,线程执行的时候不断区轮询这个标志位,当这个标志位为真的时候,线程就在离自己最近的安全点挂起。
安全区域
安全区域是安全点的拉伸和扩展,安全点解决了如何让线程停下,却没有解决如何让虚拟机进入垃圾回收状态。
安全区域是指能能够确保在某一代码片段中,引用关系不会发生变化的区域。因此,一旦线程进入了安全区域,就可以不去理会这些处于安全区域的线程。当线程离开安全区域的时候,虚拟机就会检查是否完成了根节点枚举。
记忆集与卡表
不知道大家是否考虑过这样的一个问题?既然Java堆有新生代老年代的划分,那么对象引用是否会存在跨代?如果存在跨代,又该如何解决老年代的GC Roots遍历问题?
首先,跨代引用是存在的。因此,垃圾收集器在新生代建立了一个叫做记忆集的数据结构,用来避免把整个老年代假如GC Roots的扫描范围。
记忆集是抽象的数据结构,而卡表是记忆集的具体实现,这种关系就类似与方法区与元空间。
写屏障
写屏障的作用很简单,就是对卡表进行维护和更新。
并发的可达性分析
前面我们说到过为什么要暂停所有的用户线程(这个动作也被称之为Stop The World)?这其实是为了不让用户线程改变GC Roots对象的引用。试想,如果用户线程能够随便把死亡的对象重新标记为存活,或者把存活的对象标记为死亡,这岂不是会使的程序发生意想不到的错误。
经典的垃圾收集器
知道了不少的垃圾收集理论,但是具体到某一类的垃圾收集器,其实现方式又不全然相同。下面就介绍一些常见的垃圾收集器。
Serial 收集器
Serial 收集器是最基础、历史最悠久的收集器,它在进行垃圾收集的时候会暂停所有的工作线程,直到完成垃圾收集过程。下面是Serial垃圾收集器的运行示意图:
ParNew 收集器
ParNew 垃圾收集器实则是Serial 垃圾收集器的多线程版本,这个多线程在于ParNew垃圾收集器可以使用多条线程进行垃圾回收。
Parallel Scavenge 收集器
也是一款新生代垃圾收集器,同样的基于标记–复制算法实现的。它最大的特点是可以控制吞吐量。
那什么是吞吐量呢?
Serial Old 收集器
Serial Old 收集器是Serial 收集器的老年代版本。其垃圾收集器的运行原理和Serial 收集器是一样的。
Parallel Old 收集器
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
为什么我不完全主张自学?
①平台上的大牛基本上都有很多年的工作经验了,你有没有想过之前行业的门槛是什么样的,现在行业门槛是什么样的?以前企业对于程序员能力要求没有这么高,甚至十多年前你只要会写个“Hello World”,你都可以入门这个行业,所以以前要入门是完全可以入门的。
②现在也有一些优秀的年轻大牛,他们或许也是自学成才,但是他们一定是具备优秀的学习能力,优秀的自我管理能力(时间管理,静心坚持等方面)以及善于发现问题并总结问题。
如果说你认为你的目标十分明确,能做到第②点所说的几个点,以目前的市场来看,你才真正的适合去自学。
除此之外,对于绝大部分人来说,报班一定是最好的一种快速成长的方式。但是有个问题,现在市场上的培训机构质量参差不齐,如果你没有找准一个好的培训班,完全是浪费精力,时间以及金钱,这个需要自己去甄别选择。
我个人建议线上比线下的性价比更高,线下培训价格基本上没2W是下不来的,线上教育现在比较成熟了,此次疫情期间,学生基本上都感受过线上的学习模式。相比线下而言,线上的优势以我的了解主要是以下几个方面:
①价格:线上的价格基本上是线下的一半;
②老师:相对而言线上教育的师资力量比线下更强大也更加丰富,资源更好协调;
③时间:学习时间相对而言更自由,不用裸辞学习,适合边学边工作,降低生活压力;
④课程:从课程内容来说,确实要比线下讲的更加深入。
应该学哪些技术才能达到企业的要求?(下图总结)
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
比线下而言,线上的优势以我的了解主要是以下几个方面:
①价格:线上的价格基本上是线下的一半;
②老师:相对而言线上教育的师资力量比线下更强大也更加丰富,资源更好协调;
③时间:学习时间相对而言更自由,不用裸辞学习,适合边学边工作,降低生活压力;
④课程:从课程内容来说,确实要比线下讲的更加深入。
应该学哪些技术才能达到企业的要求?(下图总结)
[外链图片转存中…(img-tzkcq7eT-1712922854874)]
[外链图片转存中…(img-ogaYAjiS-1712922854875)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!