说俩句
早在当年面试的时候,就总有面试官喜欢问java怎么回收内存的,当时只是死记硬背,没有理解,所以总是会忘记,甚至会记错,甚至当时我都不知道什么叫“理解”,如何去理解,现在想想,所谓理解,就是可以“推理”,知道基本信息,和机制,就可以推理出是怎么一回事。
现在,我总是用现实中的生活场景去理解计算机的机制,我总认为,计算机中的协议,都是人约定的,来源于人的生活,所以是一定可以理解的,不像自然科学,比如物理学定律,这些定律是怎么来的?谁创立的?是不是有什么目前还没发现条件以至于哪天这条定律就要重写了?不知道┑( ̄Д  ̄)┍
本文主要参考《深入理解java虚拟机》
tcp三次握手
三次握手是俩个服务器建立连接,开始会话,那么就可以把俩个服务器想像成俩个人,那么俩个人是怎么建立会话的呢?
想象在一个办公室里,A 和B隔着挡板,A要和B咨询一个问题,那么过程大体如下
A -> B:嘿,B,我有个问题想要请教你
如果这时B啥反应都没,那么A一定不知道B有没有听到,所以需要B给个回应
B -> A:哦,什么问题?
如果这个时候,A一扭头,继续干原来的事情,什么也不说,那么B就不知道A是不是听到了自己的回应,所以需要A再给个回应
A -> B:是这样,…
是不是可以模拟tcp三次握手的过程呢?虽然实际上还是有些差别的
java内存分区
java的内存分区实际上和linux上进程和线程的分区很像,实际上大家都差不太多つ﹏⊂
- 线程私有
- 程序计数器,指向当前的进程的正在执行的字节码
- 虚拟机栈,存放java方法
- 本地方法栈,存放native方法
- 线程共享
- 方法区,保存在着被加载过的每一个类的信息
- 包括一个运行时常量池,存放编译期生成的各种字面量和符号引用
- 堆,存放各种类的实例对象
- 方法区,保存在着被加载过的每一个类的信息
- 直接内存
如果了解linux进程地址空间,从上到下分区如下
1 | |
发现,其实都差不多?好吧,还是有区别的,但是最核心的就是栈和堆,可以自行体味下“栈是线程私有的,堆是线程共享的”
可达性分析
python为了解决引用循环的问题,也引入了可达性分析的问题,与其这样,不如直接使用可达性分析。
从根对象(GC ROOT)出发,随着引用链,标记被引用的对象,这些是不可回收的,没被标记的对象,或者说标记为可以回收的对象,就是可以回收的。
同样的问题:怎么找这些根对象?
以下对象可以作为GC root
- 虚拟机栈中引用的对象
- 方法区中静态属性、常量引用的对象
- Native方法引用的对象
对比下python,这些对象是不是很像python的非容器对象?
OopMap
实际寻找GC Root过程中,java做了优化,这个优化操作就是OopMap,这个映射表存放着GC Root,程序在运行的时候,会把可作为GC ROOT的对象的信息存入OopMap,GC时,直接查看这个OopMap时就可以开始可达分析了
安全区和安全点
python在进行可达性分析的时候,会stop the world,实际上python的GIL就保证了其他线程处于阻塞状态。
但是Java没有GIL,线程可能真的是并行的,如果在可达性分析的时候,阻塞其他线程,那么就会让效率降低,为了避免这个问题,就标记安全点,所谓安全点就是,当线程处于安全点时,不会修改内存中对象引用关系,
GC时,可以通知各个线程,等线程运行到安全点的时候,告知GC,然后线程自己stop the world再进行可达性分析
那么有的线程处于阻塞状态无法进行响应咋办,首先,处于阻塞状态的线程,就不会修改对象引用关系,在进入阻塞状态前,就标记自己进入了安全区,这样GC就知道这个线程不会影响引用关系,放心的进行可达性分析。
finalize
标记为可回收的对象,也不是立即就回收了,实际上如果对象实现了finalize函数,那么在第一次被标为可回收的时候,且被判定为有必要执行finalize的时候,就会将这个对象放入F-queue中,稍后由Finalizer线程去执行这些对象的finalize函数,如果在finalize函数中,又重新将改对象连接到GCroot,那么这个对象就被移出可被回收集合,否则,就真的被回收了
实际上python也有这个机制
标记清除和标记整理
可达性分析就是标记过程,如果
- 直接清除这个对象占用的内存空间,然后啥也不干,这就是标记清除,这样很容易产生内存碎片
- 将一般的内存空间拿出来,专门用于将标记的对象挪过来,这就是标记复制,这样内存利用率不高,但是没有内存碎片
- 先标记清除,然后将所有的对象向一个方向挪,好比军训站队时,有人出列后,教官喊向右看齐,队列中剩下的人就把出列人的空间给占了
分代收集
同样的,这个是根据统计学规律设计出的机制,将内存分为新生代和老年代,新生代中的对象每次都有大量对象被回收,老年代中对象存活率较高
垃圾收集器
java有很多垃圾收集器,其实是随着各种机制被设计出来,收集器不断地演进
下图是书里给出的各个收集器的关系

Serial
单线程收集器,收集垃圾时就是会stop the world,(没错,就像空条承太郎的替身白金之星的技能,the world!!!),就像打扫屋子的时候的让人出来
ParNew
就是多线程版本的Serial
Parallel Scavenge
以达到一个可控制的吞吐量为目标,提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio俩个参数
Serial Old
Serial的老年代,单线程,使用标记整理
Parallel Old
Parallel Scavenge收集器的老年代版本,使用多线程和标记整理
CMS
Concurrent Matk Sweep,以最短回收停顿时间为目标的收集器,标记清除,主要步骤如下
- 初始标记
- 需要stop the world,仅标记GC Root能直接关联的对象,这样速度就很快,
- 并发标记
- 从GC Root出发,不断标记对象,和用户线程一起
- 重新标记
- 在并发标记阶段,引用关系可能会发生改变,关注引用的新增,在三色模型中,当有白色的对象被改为黑色对象的引用,就把这个黑色对象,改成灰色对象,然后在这个阶段,重新标记
- 并发清除
- 回收对象
缺点
- CMS收集器对CPU资源十分敏感
- 无法处理浮动垃圾
- 标记清除产生大量内存碎片
G1
特性
- 可以并行与并发
- 分代收集
- 标记整理
- 可预测停顿
其操作大概如下
- 将堆内存划分大小相等的独立区域Region,
- 跟踪各个Region的垃圾堆积的价值大小,即回收所获得的空间大小以及回收所需的时间的经验值,优先回收价值最大的Region
- 每个Region都有一个RememberSet的数据结构,保存着其他Region中的对象引用本Region中对象的关系
- 步骤
- 初始标记
- 并发标记
- 最终回收
- 筛选回收