JVM垃圾回收机制GC

一、前言

内存泄漏问题:

在学习C语言动态内存管理的时候

通过malloc申请内存free释放内存

malloc 申请到的内存,生命周期是跟随整个进程的

只要不释放就一直在使用,对于服务器来说,就会占用大量的内存资源

只直申请不释放,就会导致无法在继续申请更多的内存了,这就是 内存泄漏

而在实际开发中,手动调用free,有可能会忘记调用,又或者,代码中出现if(){return}之类的场景,导致没有执行到free语句,而引起内存泄漏

垃圾回收机制GC就是解决这些情况的一种机制

它可以自动自动释放内存资源

二、垃圾回收机制GC

(C/C++目前还没有引入垃圾回收机制)

C是因为摆烂了,没有引入

C++又是一门追求性能的语言

C++的大佬们认为,GC会造成额外的系统开销,影响程序执行性能

因为GC中有一个 STW(STOP THE WORLD)问题

STW

触发垃圾回收机制的时候,很有可能会使当前进程的其他业务逻辑被暂停

经过Java多年来的发展,GC技术的进步,已经可以把STW的时间控制在1ms之内了

GC回收的是JVM中的哪块内存?

1)程序计数器

不需要

程序计数器是固定大小的空间,不会持续增长

2)栈    本地方法栈/虚拟机

不需要

栈存的是方法调用关系局部变量

局部变量在对应代码块执行结束后,会自动销毁(这里并不是因为GC机制,是栈自己的特点

3)元数据区/方法区

不需要(一般来说)

一般只会涉及到“类加载”,而很少涉及到“类卸载”

不需要卸载也就不需要销毁内存空间

4)!!!堆

需要

堆是GC机制主要针对的目标对象

垃圾回收机制,说是回收内存,严谨的说其实是回收“对象”

释放回收单位都是若干个对象

正在被使用的内存:不能进行释放

骑墙派:一半在使用一半不在使用,暂时不释放,等到在使用的那一半不再使用之后再释放

不再使用的内存空间: 进行释放

三、GC工作过程

1)识别出哪些对象是垃圾(不再使用)

判断这个对象,后续是否还要使用

在JAVA中,使用对象就要通过引用的方式来使用 

列外:匿名内部类

比如 new MyThread().start();

当这行代码执行完之后这个MyThread对象就会被当作垃圾

除去例外,如果一个对象没有任何引用指向,就被视为是不再被使用,就可以视为垃圾

比如,引用 t 指向对象 New Test()

t是一个局部变量当代码执行到 )时 t就被回收了

此时 对象New Test() 没有 引用指向他,New Test() 也被回收

但是,如果遇到多个引用指向同一个对象的情况,代码就更为复杂

针对这一情况就引入了两种机制

1.引用计数

引用计数并没有在JVM中使用

但是他广泛应用于其他主流语言的垃圾回收机制中,比如python,PHP

给每个对象安排一个额外的空间,空间中保存指向这个对象的引用的个数

当t和t1都被销毁之后,计数器为0,则New Test()这个对象也要被回收

引用计数存在两个问题
①:消耗额外的内存空间

每个对象都需要一个计数器(假设计数器为2个字节)

看似区区2字节,不足挂齿

一旦,整个程序中存在很多对象,总共消耗的空间就会很多

而且,比如一个对象的大小很小,只有4个字节,那计数器已经赶上对象大小的一半了

②:引用计数的“循环引用问题”

第一阶段

两个引用分别指向两个对象

此时两个对象的引用计数都是 1

第二阶段

两个对象中都存在一个属性,Test t

这里让这个t分别指向对方

此时两个对象的引用计数都为 2

第三阶段:

ab被回收

此时两个引用计数都为 :1

到最后发现,这两个对象互相引用对方,可以有没有引用指向这两个对象

就形成了像死锁一样的东西,他又用不了,也释放不掉

2.可达性分析

本质上是 时间换空间

相比于引用计数,需要消耗更多的时间,但是不会出现循环引用的问题

代码中会定义很多的变量

比如,栈的局部变量,方法区的静态类型变量/常量池中的对象

就可以从这些变量作为起点,去进行遍历

这里的遍历指的就是:沿着这些变量中持有的引用类型的成员,再进一步的访问下去

如果能访问到的对象,就不是垃圾

如果遍历完了也找不到的对象,那就是垃圾

这就是可达性分析的过程

举个列子,比如你有一颗树是由New Test()对象的引用构成的

可达性分析会从这棵树的ROOT节点,开始访问第一个引用,每个引用的LEFT和RIGHT指向的都是另一个引用,一旦某个时刻,无法从这棵树上遍历到某个对象的引用了,则说明这个对象就是垃圾

 

2)把标记为垃圾的对象的内存空间进行释放

具体的释放方法有 3种

1.标记清除

把标记为垃圾的对象,直接释放

比如灰色空间代表的是,垃圾释放之后的空间

这样会导致一个问题,内存碎片

形成小的离散的空闲内存空间

这样会导致后续申请内存空间失败

因为,内存申请都是一次申请一段连续的内存空间

比如你总的空闲空间有1GB

可是由于出现了内存碎片

就会导致你可能连500MB的内存空间都申请不了

2.复制算法

复制算法可以很好的避免内存碎片

复制算法,把内存分为两半,先使用其中的一半

比如2 4 是被标记为垃圾的对象,则先把 不是垃圾的对象 1 3 复制到另一半内存当中

然后再释放左侧空间全部释放掉

算法复制也有他的缺点:

1.总的可用的空间变少了

2.如果要复制的对象很多,复制的开销就会很大

 

当某一轮的GC回收,大部分的对象都要释放,只有少部分还在使用当中,这种情况推荐使用算法复制

3.标记整理

比如粉色是要回收的对象

这里的机制类似于顺序表删除中间元素的操作

把不释放的对象覆盖到将要被释放的对象上

而JVM不会说,直接选择上述哪一种作为固定方案

而是采用一种分代回收(根据不同对象,选择不同方法)

那么对象的不同是以什么来区分呢?

分代回收

指的是,对象的"年龄"

年龄指的是:

一个java进程在运行的时候就需要JVM的专门的线程来不断的进行周期性扫描/释放

一个对象,被线程扫描到一次,并且是可达的(不是垃圾,正在使用中,或者将来要使用)

年龄 + 1   (初始年龄是0)

需要记录年龄信息就需要内存空间来存储,年龄是存储在,对象头中的一个属性中

JVM会根据年龄差异,把堆的内存分成两大部分

1.新生代,年龄小的对象 

2.老年代,年龄大的对象

第一轮GC扫描之后,伊甸区中少数幸存的对象,通过复制算法,将其拷贝到幸存区中

第二轮GC扫描中除了扫描伊甸区也要扫描幸存区的对象,

幸存区中的对象大部分也存活不下来,少数存活的对象,接着拷贝到另一个幸存区中

经过若干轮扫描之后,一个对象在幸存区中存活了若干周期

JVM就会认为,这个对象的周期大概率会很长,就会把他拷贝到老年代中

老年代中的对象也会扫描,不过扫描的频率大大降低

最后,当老年代中的对象也要被释放时,就会按照标记整理的方式进行释放

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值