0、mind
本节主要介绍jvm中自动内存管理的内容, 当然在此之前要简单讲一下jvm中的内存区域是怎么样的。随后会介绍垃圾回收的过程,垃圾回收的方法以及垃圾收集器
一、基础
1.1 JDK框架
JDK全名是Java Development ToolKit,java开发工具包,这整个工具包包括JRE(java运行环境)以及一些java工具相关api。jre又包括jvm,Java SE api。
1.2 Java内存区域
这里我们介绍一下JVM在运行java代码的时候,它的内存区域是怎么样的。方法区和堆都是线程共享的,右边三个是线程隔离的。
1)程序计数器
在多线程切换的时候,每个程序计数器来记录当前线程的执行状态和位置,便于上下文切换的时候能恢复场景
2)虚拟机栈
虚拟机栈也是线程隔离的,用来存放线程运行的时候产生的局部变量表等信数据,局部变量表里存放的就是编译器已知的各种基本类型和对象引用
常说的堆内存和栈内存其实是继承自C/C++的内存分配风格,放在java中,栈内存指的就是这块虚拟机栈的内存空间
3)本地方法栈
与虚拟机栈相比,本地方法栈是为本地方法服务
4)堆
堆是jvm中内存区域最大的一块地方,这个区域唯一存放的就是对象实例,几乎所有的对象实例都在这里分配内存。堆也是垃圾收集器管理的内存区域。
5)方法区
方法区用来存放一些常量,静态变量等信息
6)运行时常量池
方法区的一部分
7)直接内存
直接内存并不是jvm虚拟机运行内存的一部分,但是也是java运行时经常使用的内存区域
二、垃圾收集
2.1 垃圾收集的流程
对虚拟机对象中的垃圾进行收集,首先要考虑三件事
- 哪些需要回收?哪些是垃圾哪些不是
- 什么时候回收?
- 怎么回收?用什么收集算法
2.2 判断对象是否存活
首先我们回收的肯定是垃圾对象,死亡对象,那么我们就需要判断一个对象是否存活
1)引用计数算法
比较简单易用的方法。在对象中添加一个引用计数器,当有其他对象引用的时候,该计数器加一;当引用失效的时候,计数器减一。
但是这个方法也有明显的缺点,就是无法解决循环引用的问题。
这个方法原理简单,并且高效,但是需要考虑很多例外情况,所以主流的jvm虚拟机并不使用该方法。
2)可达性算法
目前主流程序语言使用的是可达性算法。
通过GC Rtoots的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,如果某个对象和root之间没有引用链,就是说它不可达,即死亡
3)什么是引用
上节我们提到了java中的引用,在这里我们更深入的了解一下。
在jdk1.2之前,引用就是很传统的其内存为另一个对象的地址。在这之后,分成四种
- 强引用
- 软引用
- 弱引用
- 虚引用
强引用就是传统的引用定义,只要有强引用,对象就存活
软引用则是描述一些还有用,但不是必须的对象。第一次内存溢出时,软引用对象会拉入第二轮回收范围,如果第一次垃圾回收后内存仍然不够就会清除
弱引用和软引用一样,也是非必需对象,但是被弱引用关联的对象能活过第一轮垃圾收集,但是下一轮会被回收
4)死亡救赎
因为程序在不断运行的过程中,一个对象可能在清除的某一个过程中“存活”被引用,所以即便在可达性算法判定为不可达对象时,也并非“非死不可”的。
这个时候处于缓冲状态,要真正宣告对象死亡需要两个过程。
第一次是可达性分析,没被引用的进入筛选。
进入筛选阶段的对象,再判断有无必要进行回收。
这个过程中,只要对象被拯救了,和GC Roots中有对象关联,即存活了。
2.3 收集算法
如果用判断对象是否存活方法来分类,垃圾收集算法分为引用计数式和追踪式,我们这里讨论追踪式,因为并没有使用引用计数方法。
1)分代收集理论
当前大多的收集器都遵循分代收集的原则。它建立在两个假说之上
- 弱分代假说:绝大多数对象都是朝升夕灭的
- 强分代假说:熬过越多次垃圾收集的对象就越越难消亡
根据这个假说,大部分的垃圾收集器都分成新生代和老年代。
分出不同区域之后,可以在不同区域分别进行垃圾回收,但是这又有一个问题,倘若新生代和老年代之间有引用怎么办?这样子每次还是需要扫描全部空间。
如何解决跨代问题?增加了第三条经验法则
- 跨代引用假说:跨代引用对于同代引用来说仅占少数
2)标记——清除算法
把标记的死亡的对象直接清除。这是最基础最简单最高效的办法。缺点就是会造成内存碎片,并且当有大量对象需要回收时,会造成效率较低的情况。
3)标记——复制算法
为了解决标记清除算法回收大量对象效率低的情况,提出一种半区复制的方法。把一块内存区域分成两块,一次只用一边。进行垃圾回收时,标记存活的对象,复制到另一边去。这样的缺点就是浪费较多的空间,并且当对象存活率较高的情况下效率较低。
现在大部分收集器的新生代是采用这个方法的
4)标记——整理算法
针对标记复制方法的缺陷,提出了一种适合老年代的算法。标记存活对象后,让存活对象向一边移动。
移动势必会导致“stop the word”,但是不移动则会造成内存碎片
对于延迟控制来说,不移动的方法更为优秀;但是对于程序的整体吞吐量来说,移动则更为优秀
2.4 垃圾收集器
在开始之前,要简单介绍一下收集器中,并行并发的概念。
- 并行指的是多条垃圾收集器线程之间的关系,多个线程在协同工作,此时用户线程是在等待状态。
- 并发指的是垃圾收集器和用户线程之间的关系,用户线程并未被冻结。
1)Serial
最基础,历史最悠久的收集器,因为简单快速,现在也常在使用。
是单线程收集器,在收集过程中需要“stop the world"。新生代采用复制算法,老年代配合Serial Old采用整理算法
即便stop the world带来较高延迟的问题,但是这个收集器需要的内存极其小,而且延迟在绝对意义上,只要内存小也影响不大,所以对于客户端模式下的虚拟机还是非常不错的。
2)parNew
是serial的多线程并行版本,其他的和serial完全一致。
3)Parallel Scavenge
基于复制算法实现的收集器,也是并行的多线程收集器。这个收集器的关注点是吞吐量,希望垃圾收集的时间尽量短。
4)Serial Old
serial收集器的老年代版本
5)Parallel Old
老年代版本,支持多线程并发收集,使用整理算法。
6)CMS收集器
以最短回收停顿时间为目标。服务器响应速度优先,基于标记清除算法。
收集的过程分为四个步骤
- 初始标记:仅标记GC Roots能直接关联的对象,速度很快
- 并发标记:从GC Roots关联的对象遍历整个对象图,耗时长但是不需要停止用户线程
- 重新标记:修正并发标记期间,由于用户线程所导致的部分对象记录
- 并发清除:清除阶段
这之间初始标记和重新标记都需要“stop the world”。由于使用清除算法,会导致较多的内存碎片
7)Garbage First
G1收集器是垃圾收集器发展史上里程碑成果。
它摒弃了之前收集器分为老年代新生代的做法,采用局部收集的设计思路和基于Region的内存布局,收集“最具价值”的区域。G1是面向服务端的垃圾收集器。
- 初始标记:标记GC Roots能直接关联的对象,stop the world
- 并发标记:获取对象图
- 最终标记:stop the world 修正标记
- 筛选回收:对各个region的回收价值和成本来排序。这里的操作设计对存活对象的移动,需要stop the world
t
G1收集器是垃圾收集器发展史上里程碑成果。
它摒弃了之前收集器分为老年代新生代的做法,采用局部收集的设计思路和基于Region的内存布局,收集“最具价值”的区域。G1是面向服务端的垃圾收集器。
- 初始标记:标记GC Roots能直接关联的对象,stop the world
- 并发标记:获取对象图
- 最终标记:stop the world 修正标记
- 筛选回收:对各个region的回收价值和成本来排序。这里的操作设计对存活对象的移动,需要stop the world