引用:
java 和 c++ 之间有一堵 由动态内存分配和垃圾回收技术所围成的高墙。墙外的人想进入 而墙内的人却想出来。
回收内存的位置
内存回收的位置主要在 jvm 的堆和方法区中(线程共享的区域)。而程序计数器、虚拟机栈和本地方法栈都是线程独立的,随线程而生 随线程而灭,当线程结束或方法结束后内存自然就回收了。
回收机制 (可达性分析法)
- GC Root节点
- 本地方法栈和虚拟机栈中引用的对象。
- 方法区中静态属性 static 引用的对象和方法区中常量 final 引用的对象。
- 引用类型
- 强引用(肯定不回收)
- 软引用(内存溢出前进行回收)
- 弱引用(肯定回收)
- 虚引用(只是一种引用类型,不对生存时间构成影响,唯一目的就是能在这个对象被回收时收到一个系统通知)
- 对象自我拯救的逃脱机制
可达性分析检索时,会将那些不在GC Root引用链上的对象进行第一次标记并筛选。 筛选的条件是:对象是否有必要执行 finalize() 方法。
判断条件是:当对象没有覆盖 finalize()或者finalize()方法已被执行,jvm则认为没有必要执行。
如果判断对象有必要执行 finalize()方法,则将对象放入一个叫做F-Queue的队列中,稍后会有虚拟机建立一个低优先级的 Finalize 线程执行它(这里的执行仅仅指挥触发它,而不一定等它执行完毕)。finalize() 方法是对象逃脱的最后一个机会。之后GC会对F-Queue队列中的对象进行第二次的标记,若此时对象还未与GC Root引用链建立联系,那么这个对象基本上就真的被回收了。
注意:任何一个对象的 finalize() 方法都只会被系统执行一次。
- 方法区的回收
方法区主要回收:废弃的常量和无用的类这两部分内容。
- 废弃常量:没有任何对象引用常量池中的常量。
- 无用的类:
- 该类的所有实例已被回收,也就是堆中不存在该类的任何实例。
- 加载该类的
ClassLoad
类加载器已被回收。 - 该类所对应的
Class
对象已没有在任何地方引用,也就是说不能再任何地方通过反射来获取该类的实例。
垃圾回收算法
- 标记-清除
分为标记和清除两个阶段
也就是对GC 第二次标记过的那些不在GC Root引用链上的对象,执行清除操作。
缺点是:会产生很多不连续的内存碎片,在之后的运行中无法分配大对象。 - 标记-整理
对标记-清除之后的产生的大量不连续的内存碎片,进行整理重排序使存活的对象向一端移动,从而产生连续的大空间。 - 复制
最初始时 是将内存分为大小相等的两个区域,假定为A和B,每次使用的时候只使用其中的一个区域A,当发生GC时,将A中存活的对象复制到B区域中,接着清除掉A区域后再将B区域中的对象复制到A区域中。 - 分代收集
当前的商业虚拟机采用的都是分代收集的算法,这种算法并没有提出新的思想,只是根据对象存活周期的不同将内存分为几块,一般是将内存分为新生代和老年代。在新生代中使用复制
算法,老年代中试用标记整理
算法。
新生代中的复制算法经过IBM的研究表明,新生代中的对象朝生夕死,将内存分按1:1进行分配浪费。而是将新生代分为一块较大的Eden
空间 E 和两块较小的Survivor
空间 S1 和 S2(默认是按照8:1:1
的比例进行分配)。每次使用的时候 只使用 E 和 S1 大小为新生代的90%,当发生GC的时候,将两块空间中存活的对象复制到S2中。
但是,谁都不能保证每次GC时只有不多于90%的对象存活,也就是说S2 空间不足以存放存活的对象,这时就需要其他内存(这里指老年代)进行分配担保。
但是还有个问题,谁又能保证,即使有老年代做了担保,就一定能保证一定能存放存活的对象呢?这里的问题稍后进行解释…
HotSpot的算法实现
- 枚举根节点
GC时需要进行可达性分析找到GC Root的引用链,这里有两个问题,第一 如何建立GC Root引用链?GC Root全局性的引用,分布在执行上下文中,如果要逐个检查里面的引用,必定为消耗很长的时间。第二:GC 停顿。可达性分析时,必须要在一个能够确保一致性的快照中进行,可就是说在可达性分析时,整个执行系统看起来向被冻结在一个时间点上,不可以再出现不断变化的引用关系。
其实,在类加载的时候,虚拟机使用一组称为OopMap的数据结构,将对象的位置进行记录。这样GC在扫描时,直接查找OopMap就可以得知。 - 安全点
GC也不是说在程序执行的任何地方都可以执行的,而是在特定的位置,也就是安全点。
当需要GC时,只是设置一个GC的标识,线程执行时主动轮询这个标识(标识的位置和安全点重合),当判断中断标识为真时,自己中断挂起。 - 安全区域
针对安全点而言,当需要GC时,设置一个标识,线程主动轮询这个标识。
但是当线程处于 sleep 或 Blocked 状态时,线程是无法响应虚拟机的请求,也就无法走到就近的安全点。
安全区域:指在一段代码片段中,引用关系不会发生变化。在这个区域的任何位置开始GC都是安全的。
当安全区域中的线程执行完成,离开安全区域时,会询问GC是否完成,若GC完成了,则线程继续执行。否则,就必须等待,直到GC完成 发出可以离开的指令。
在这里有个疑问:安全点线程的中断是什么方式的中断。中断的线程又是如何接收GC完成的信号,难到使用的是唤醒所有安全点中被挂起的线程? orn
垃圾收集器
JDK7 update 14之后的Hotspot使用的都是G1收集器。
详情稍后补充。
内存分配和回收策略
往大方向来讲,主要是在堆上进行分配。
1. 对象优先在新生代(eden)上分配,当新生代没有足够空间事,jvm进行一次Minor GC,新生代采用复制算法收集内存。
2. 大对象直接进入老年代。指那些超长的字符串或数组。
3. 长期存活的对象进入老年代。对象每熬过一次GC年龄增长一岁。默认15岁之后,进入老年代。
4. 动态年龄的判断
当新生代中相同年龄所有对象大小的总和 大于 新生代空间的一半,则年龄大于或等于该年龄的对象都可以直接进入老年代。
5. 空间分配担保
当发生Minor GC(新生代GC)之前,虚拟机会检查老年代的最大可用连续空间是否大于新生代所有对象的总空间。如果大于,那么Minor GC可以确保是安全的。如果不成立,虚拟机会检查HandlePromotionFailure设置值是否允许担保失败。若允许担保失败,那么会继续检查 老年代最大可用连续控件是否大于历代晋升到老年代对象的平均大小,如果大于,将会尝试进行Minor GC,尽管是否风险的;如果小于或者设置为不允许担保失败,那此时要改为先进行一次Full GC(老年代 GC)。