第三章 垃圾收集器与内存分配策略
书中笔记:
也许并不会死:
要宣告回收一个对象死亡,至少要经历两次标记过程:
当可达性分析发现一个对象不可达的时候,将标记第一次并进行筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize或者已被调用过,则虚拟机认为此对象没必要执行finalize, 如果判断有必要执行,则此对象将会被放入一个F-Queue队列中,之后会被一个优先级比较低的Finalizer线程去调用,但是并不会等待他执行完毕,因为此对象的finalize并不可靠,可能会死循环之类的,如果在finalize方法中该对象重新与GC Root建立关联则他就逃离被回收的命运了。
回收方法区的时候:
如何判断一个类是无用类:
1、该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例
2、加载该类的ClassLoader已经被回收
3、该类对应的Class对象没有再任何地方被引用,无法在任何地方通过反射访问该类的方法。
内存分配策略:
1、对象优先在Eden分配,当Eden区无法分配,此时Eden区的内容Survivor区也无法存放时,将触发一次Minor GC
2、大对象直接进入老年代,比如字符串以及数组之类的
3、长期存活的对象将进入老年代。对象在Eden出生并经历过一次Minor GC之后,age+1.
默认15岁时会被加入老年代,当然也不绝对,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄>=改年龄对象就直接进入老年代。
4、空间分配担保:当Minor GC发生之后,极端情况下所有对象任然存活,就需要老年代担保空间了,如果老年代剩余空间能容纳下新生代则没有影响,如果不够则会触发Full GC
网上补充:
一:垃圾回收机制的意义
java 语言中一个显著的特点就是引入了java回收机制,是c++程序员最头疼的内存管理的问题迎刃而解,它使得java程序员在编写程序的时候不在考虑内存管理。由于有个垃圾回收机制,java中的额对象不在有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存;
内存泄露:指该内存空间使用完毕后未回收,在不涉及复杂数据结构的一般情况下,java的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有是也将其称为“对象游离”;
二:垃圾回收机制的算法
java语言规范没有明确的说明JVM 使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做两件基本事情:(1)发现无用的信息对象;(2)回收将无用对象占用的内存空间。使该空间可被程序再次使用。
1.引用计数法(Reference Counting Collector)
1.1算法分析
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
1.2优缺点
优点:
引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:
无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
2.tracing算法(Tracing Collector) 或 标记-清除算法(mark and sweep)
2.1根搜索算法
java中可作为GC Root的对象有
1.虚拟机栈中引用的对象(本地变量表)
2.方法区中静态属性引用的对象
3. 方法区中常量引用的对象
4.本地方法栈中引用的对象(Native对象)
2.2tracing算法的示意图
2.3标记-清除算法分析
标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
年轻代(Young Generation)
1.所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)
年老代(Old Generation)
1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代(Permanent Generation)
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。
三.GC(垃圾收集器)
新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge
老年代收集器使用的收集器:Serial Old、Parallel Old、CMS
Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。
Parallel Old收集器(标记-整理算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先
CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择
CMS使用的是标记-清除算法。执行过程分为4个部分:
1、初始标记 --->stop the world 仅仅只是标记以下GC roots能直接关联到的对象,速度很快。
2、并发标记 --->和G1差不多一个意思
3、重新标记 --->stop the world,和G1差不多一个意思
4、并发清除 --->与G1相比少了排序之类的筛选功能。
G1(Garbage-First):
G1能充分利用多CPU、多核环境下的硬件优势,来所谓stop the world的停顿时间,部分其他收集器需要停顿Java线程来执行GC动作,G1仍然可以通过并发的方式让Java程序继续执行。
G1可以不用与其他收集器配合就可以管理整个堆,使用类似标记-整理的算法,局部上看是复制算法,所以他不会产生内存碎片。
基本实现:使用G1收集器的时候,它将整个Java堆划分为多个大小相等的独立区域(region),任然保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的,他们都是region的集合。
G1能让用户指定M毫秒内,GC的时间小于N豪秒,这个原理是G1会跟踪各个Region里面的垃圾堆积的价值大小,然后维护一个优先队列,每次根据用户指定的回收时间,优先回收价值最大的Region。
不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
1、初始标记 --->仅仅只是标记以下GC Roots能直接关联的对象,需要停顿线程,但耗时很短。
2、并发标记 --->并发标记对堆中对象进行可达性分析,找出存活对象,耗时长,但可以和用户线程并发执行。
3、最终标记 --->修正在并发期间因用户程序继续运行而导致标记产生变化的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段,需要把logs里面的内容合并到Remembered Set中。
4、筛选回收 --->对region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,也可以并发执行。
四、GC的执行机制
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
1.年老代(Tenured)被写满
2.持久代(Perm)被写满
3.System.gc()被显示调用
4.上一次GC之后Heap的各域分配策略动态变化
五、Java有了GC同样会出现内存泄露问题
1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。
Static Vector v = new Vector();
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
在这个例子中,代码栈中存在Vector 对象的引用 v 和 Object 对象的引用 o 。在 For 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为, GC 在跟踪代码栈中的引用时,会发现 v 引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。
2.各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。