程序计数器、栈、堆、方法区、本地方法栈
其中程序计数器、栈和本地方法栈是和线程绑定在一起的,当创建了线程,就会申请内存,当线程结束的时候,想关的内存就会被销毁。方法区主要是类对象,类加载的时候就会申请这里的内存,“类卸载”操作实际上是很少会涉及到的。
因此,垃圾回收机制主要回收的对象就是堆
,并且垃圾回收释放内存,实际上是在以对象为单位进行释放,因为内存的申请是以对象为单位进行申请的,当整个对象的内存都不在使用时,即没有引用指向这个对象
时,就可以将其进行释放
判断对象是否可以被回收的根本准则就是,已经没有任何引用指向该对象,该对象已经没有办法被访问到
该方法并没有被 JAVA 采纳,但是 Python 、PHP等编程语言的垃圾回收机制有用到这样的方法
使用原理:
每个对象实例都有一个
引用计数器
每次创建一个引用指向了该对象,计数器就会加1,每次有一个引用不在指向这个对象,计数器就会减1
当引用计数为零时,该对象就被当做垃圾进行回收
Demo t1 = new Demo();
Demo t2 = t1;
t1 = null;
t2 = null;
优点:
执行效率高,程序受影响少,而且用法简单
缺点:
- 空间利用率比较低。用引用计数的方法会需要内存中腾出额外的空间来保存引用计数,如果对象本身就比较小,所占用的空间少,此时空间的利用率就非常的低下
- 无法检测出
循环引用
的情况,导致内存泄漏
class Demo {
Demo t = null;
}
Demo t1 = new Demo();
Demo t2 = new Demo();
t1.t = t2;
t2.t = t1;
t1 = null;
t2 = null;
当执行到 t1 = null;t2 = null;的时候就会发现纵然引用计数器不为0,但是想要获取第一个对象,就必须先拿到第二个对象,但第二个对象的引用在第一个对象中,就需要先拿到第一个对象,陷入僵局,外界已经没有办法获取到这两个对象,这两个对象理应被视为垃圾,但是由于引用计数又不为零,垃圾回收机制就不一定认为这两个对象是垃圾
该方案就是 Java 采取的方案
所谓的可达
就是通过GC Roots
(可达性分析的出发点)一层一层的往下找,通过一系列的引用操作来成功访问到对应的对象
。反之,不可达就是无论如何进行引用操作,也没有办法来访问到这个对象
就像一棵二叉树,从根节点总能通过引用来访问到任意一个节点,但如果二叉树中某一个引用设置为 null,可能就会引起一下节点无法再被访问到,就是不可达的
可达性分析算法就是通过判断对象的引用链是否可达来决定对象是否可以被回收,把所有能够被访问到的对象都标记成“可达”
,那么剩下的不可达对象就都是垃圾,JVM 就会把未标记可达的对象统统释放
优点:
空间利用率较高
不会出现循环应用导致内存泄漏问题
缺点:
每个周期都需要遍历大量的对象,当代码中的引用关系错综复杂时,遍历操作开销较大
垃圾清除首先要标记处需要回收的垃圾对象,然后就直接进行释放,虽然内存被释放了,但是却引入了很多缺点
缺点:
效率比较低下
,标记和清除对象这两个过程的效率是比较低下的- 标记清除后会
产生大量的不连续的内存碎片
,当之后有比较大的对象实例想要在堆上申请较大内存空间时需要连续的内存空间,内存碎片的产生可能导致无法找到足够大的空间而不得不触发另一次的垃圾收集
使用原理:
- 将内存按容量划分成大小相等的两块,每次是只使用其中的一块
- 当正在使用的内存空间需要进行垃圾回收的时候,就会将存活着的对象复制到另一半内存空间中
- 之前使用的旧的内存空间只剩下垃圾对象,就可以全部回收,此时新的内存空间时连续的
优点:
解决碎片问题
缺点:
- 内存的使用率低,每次只有一半的空间正在使用
- 如果正在使用的空间中存活对象较多,需要释放的垃圾对象比较少,将存活对象进行复制来复制去的操作消耗成本是比较高的
使用原理:
- 首先进行标记操作
- 将存活的对象都向其中的一段进行移动,然后处理掉边界以外的内存
优点:
- 不会出现空间碎片问题
- 当存活的对象较多时,避免复制算法带来的复制效率问题
以上的回收方法适用的场景各有不同,实际上,JVM 将将上述的多种算法结合在了一起,来解决整体的垃圾回收问题
分代回收是根据对象的年龄来划分的,所谓的年龄指的就是GC的次数。GC 的可达性分析是周期性的,经历的周期越多,对象年龄就越大
Java堆分为新生代和老年代
,新生代包括伊甸区(Eden)和两块生存区
使用原理:
新创建出的对象诞生在伊甸区
,新对象的生命周期大多是比较短的,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法
- 如果伊甸区的对象活过了第一轮 GC ,就会进入
生存区
中的其中一块。生存区有两块,是等长的,同样也是通过复制算法
进行释放对象- 当对象在两块生存区中来回的熬过
若干轮 GC
,就可以升级为老年代
,就会将这个对象拷贝到老年代中- 进入老年代中的对象也需要进行定期的 GC 筛选,扫描的频率会比之前低一些,老年代中对象存活率高,没有额外空间对它进行分配担保,就要采用
标记整理
算法释放内存
(1)Minor GC
即新生代GC
,指的是发生在新生代的垃圾收集。当年轻代空间不足的时候会触发 Minor GC(这里指的是伊甸区空间不足),因为Java对象大多都具备朝生夕灭的特性,因此Minor GC非常频繁,一般回收速度也比较快,采用复制算法
(2)Major GC
即老年代GC
,指的是发生在老年代的垃圾收集。当老年代空间不足的时候,会先尝试触发 Minor GC,如果之后空间仍然不足,则触发 Major GC。经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。采用标记算法
(3)Full GC
针对新生代、老年代的GC
只不过一般 Minior GC 工作量比较小. 开销也小, 对于程序影响不大.
一般我们可以把 Major GC 近似视为 Full GC
垃圾回收器是 JVM 针对垃圾回收算法的一些具体实现
Serial [ˈsɪəriəl]
这个收集器是一个单线程串行
的收集器,它只会使用一条收集线程去完成垃圾收集工作,并且在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World,译为停止整个程序,简称 STW)
SafePoint
Safepoint 可以认为是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,线程会暂停所有的用户线程。
在 SafePoint 保存了其他位置没有的一些当前线程的运行信息,供其他线程读取,才知道这个线程用了哪些内存,没有用哪些。只有线程处于 SafePoint 位置,这时候对 JVM 的堆栈信息进行修改,例如回收某一部分不用的内存,线程才会感知到,之后继续运行
所以,GC 一定需要所有线程同时进入 SafePoint,并停留在那里,等待 GC 处理完内存,再让所有线程继续执行,像这种所有线程进入 SafePoint等待的情况,就是 Stop the world
这个收集器是一个多线程并行
的收集器,其余功能和 Serial 收集器差不多
Parallel [ˈpærəlel] Scavenge [ˈskavɪn(d)ʒ]
该收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程
收集器,相当于ParNew 收集器的升级版
相当于 Serial 收集器的老年代版本
。它同样是一个单线程串行
收集器,使用标记整理
算法
相当于 Parallel Scavenge收集器的老年代版本
,它同样是一个多线程串行
收集器,使用标记整理
算法
该收集器的目的就是尽可能的减小 STW 带来的影响
,在垃圾回收的几个阶段只会有一小部分会触发 STW,大部分的操作是可以和用户线程并发
的
步骤
初始标记
:初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要 STW并发标记
:相当于要把整个代码中所有的对象都遍历一遍,这是工作量最大的地方,但该过程适合用户线程并发的,没有使用 STW重新标记
:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要 STW并发清除
:清除垃圾对象,和用户线程并发执行
步骤中并发标记和并发清除过程耗时最长,但收集器线程可以与用户线程一起工作。因此,总的来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
将整个堆内存分成了很多很小的区域每个区域称为一个 region ,这里会针对这些区域,分成了不同的角色,每一轮 GC 只需要处理其中的一部分,经过多轮的 GC 才处理完整个内存区域,这样就可以把每一轮的 GC 时间会足够的短
E表示该region属于Eden内存区域
S表示属于Survivor内存区域
T表示属于Tenured内存区域
空白的表示未使用的内存空间
完!