程序计数器.虚拟机栈.本地方法栈随线程而生随线程而灭,栈帧分配多少内存在类结构确定后就确定了。垃圾回收针对的是Java堆和方法区。
一:对象已死吗
- 在垃圾收集器进行回收前,第一件事就是确定这些对象哪些还存活,哪些已经死去。
1.引用计数算法
- 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器减1;其中计数器为0的对象是不可能再被使用的已死对象。
- 当两个对象相互引用时,这两个对象就不会被回收 引用计数算法,不被主流虚拟机采用,主要原因是它很难解决对象之间相互循环引用的问题。
2 可达性分析算法
通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所经过
的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(在图论中称为对象不可达)时,这个对象就是不可用的。
如图object5,6,7虽然有关联,但是到gc roots是不可达的所以是可回收对象。
在java中,可作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
3 引用的分类
- 强引用:是指在程序代码中直接存在的引用,譬如引用new操作符创建的对象。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用:还有用但是并非必需的引用,早系统将要发生内存溢出异常之前会把这些对象列进回收范围中进行二次回收,若还是没有足够的内存,才会抛出内存溢出异常。
- 弱引用:非必需的对象,只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否够用都将回收这些对象。
- 虚引用:一个对象是否有虚引用的存在完全不会对他的生存时间构成影响,也无法通过虚引用来取得一个对象实例。
4 宣告一个对象死亡的过程
要真正宣告一个对象死亡,至少要经历两次标记过程:
- 若对象在进行可达性分析后发现没有与GC Roots相连接的引用链,会被 第一次标记
并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法(如当对象没有重写finalize()方法或者finalize()方法已经被虚拟机调用过则认为没有必要执行)。 - 如果有必要执行则将该对象放置在F-Queue队列中,并在稍后由一个由虚拟机自己建立的、低优先级的Finalizer线程去执行它;稍后GC将对F-Queue中的对象进行第二次标记,如果对象还是没有被引用,则会被回收。
但是作者不建议通过finalize()方法“拯救”对象,因为它运行代价高、不确定性大、无法保证各个对象的调用顺序。
5 回收方法区
永久代(方法区)的垃圾收集主要回收两部分内容:废弃常量和无用的类
- 废弃常量:假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他 地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。
- 无用的类:同时满足下面3个条件的类(实例、类加载器被回收,java.lang.Class对象没有被引用)。①该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
②加载该类的ClassLoader已经被回收。
③该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
二:垃圾收集算法
1.标记清除算法
分为两个阶段:标记和清除
- 标记:首先标记所有需要回收的对象(标记过程在上文宣告一个对象死亡过程中提及)
- 清除:在标记完成后统一回收所有被标记的对象
不足地方:①效率问题,标记和清除过程效率都不高;②空间问题,垃圾回收后较多不不连续的内存碎片,导致分配较大对象时找不到足够的连续内存而不得不提前触发又一次垃圾回收动作.
2.复制算法(新生代)
- 将可用内存按容量分为两个块,每次只用其中之一。当这一块内存用完之后,将还存活的对象复制到另一边去,然后清除所有已经使用过的部分。
- 优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
- 缺点:代价是将内存缩小为了原来的一半,未免太高了一点。
解决方法:新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
在HotSpot里,考虑到大部分对象存活时间很短将内存分为Eden和两块Survivor,默认比例为8:1:1。代价是存在部分内存空间浪费,适合在新生代使用。
备注:不保证每次不多于10%的对象存活,survivor空间不够时,依赖其他内存分配担保(老年代)
3 标记-整理算法(老年代算法)(Mark-Compact)
对象存活率太高情况下,进行较多复制操作效率将会变低,应对诸如所以对象都存活情况下,所有老年代不采用复制算法.
- 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4.分代收集算法
- 当前商用虚拟机都采用了这种算法,根据对象的存活周期将内存划分为几块,一般是把Java堆分为新生代和老生代,根据各个年代采用适当的收集算法。
- 新生代一般采用复制算法(Copying)。
- 老生代一搬采用 标记-清理(Mark-Sweep) 或者标记-整理(Mark-Compact) 进行回收。
新生代:
收集器 | 收集对象和算法 | 收集器类型 |
---|---|---|
Serial | 新生代 ,复制算法 | 单线程 |
ParNew | 新生代,复制算法 | 并行的多线程收集器 |
ParallelScavenge | 新生代,复制算法 | 并行的多线程收集器 |
parallel scavenge:系统响应时间和垃圾回收造成停顿时间有关,缩小这个时间扩大响应速度的代价是牺牲新生代空间和吞吐量来换取的
老年代:
收集器 | 收集对象和算法 | 收集器类型 |
---|---|---|
Serial Old | 老年代,标记整理 | 单线程 |
ParallelOld | 老年代,标记整理 | 并行的多线程收集器 |
CMS | 老年代,标记清除算法 | 并行与并发收集器 |
G1 | 跨新生代和老年代;标记整理 + 化整为零 | 并行与并发收集器 |
CMS垃圾回收器工作流程:(标记清除算法)
1.初始标记→暂停所有工作线程,GC线程做初始标记,仅标记GC Root直接关联的对象.
2.并发标记→工作线程和GC线程同时运行,GC线程这时候做可达性分析,找存活对象
3.重新标记→暂停所有工作线程,修正并发标记时期标记产生变动的那一部分对象的标记记录(在并发标记阶段对象发生变化的,虚拟机会记录在线程里)。
4.并发清除→工作线程和GC同时运行
总结:
①第一步耗时最短,第三步耗时也较短用了多线程重新标记。
②第二第四步耗时长但是不停顿。
优点:效率较高,暂停工作线程时间短
缺点:①占用CPU②浮动垃圾(第四步并发清除的时候还会产生垃圾)③内存碎片
G1垃圾回收器(算法:复制算法和标记整理)
G1把堆划分为多个大小相等的独立区域,新生代和老年代不再物理隔离。
1.初始标记-同CMS
2.并发标记-同CMS
3.最终标记-同CMS
4.筛选回收:对各个区域中的回收价值和成本排序,根据用户期望的停顿时间制定回收计划。此阶段可以和用户线程并发执行,不过是只回收一部分Region区域,时间是用户可控的,停顿用户线程将大大提高回收效率。
特点:1.无内存碎片;2.可预测的停顿(化整为零)
G1收集器有计划地避免在整个堆中全区域垃圾回收,所以建立了可预测的停顿时间模型。(大内存建议用G1)
具体实现:G1跟踪各个区域的垃圾堆积的价值大小(回收获得的空间及回收所需的时间),后台维护优先列表,优先回收价值最大的区域(也就是Garbage-first名称的由来)。这种使用划分区域及优先级的回收方式,保证了G1收集器在有限的时间内尽可能高的回收效率。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。