第二章 Java内存区域与内存溢出异常
- 整章内容可以整理为下图:
- 两种对象访问定位方式。 Java虚拟机栈中的局部变量表中,除过常见基本类型外,还会存储对象引用和returnAddress。其中对象引用关联的真实信息存储在堆里面,为了定位具体的对象,有两种方式:句柄(handle——谁他妈翻译的句柄)访问对象和指针直接访问对象。但是无论哪种方式类型数据都需要通过2次转发。 通过下图可以看出来,类型数据属于方法区,实例数据存储在堆中。也可以看出来,一个对象的数据有存放在堆中的,也有一部分存放在方法区。这里面有个细节:Java堆中存储的是类型指针,真实的类型数据在方法区,那么对象头是在堆中还是方法区呢?
第三章 垃圾收集器与内存分配策略
- 垃圾收集器需要完成的三件任务:1)哪些内存需要回收?——定义哪些是垃圾。2)什么时候回收?3)如何回收?——回收方式。
- 垃圾回收已经很自动化了,那么为什么还需要了解呢?因为当出现内存溢出、内存泄露问题时,当垃圾收集器成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
- 线程私有的内存区域随着线程的生灭一起变化,并且他的大小是在类确定之后基本上确定的。但是堆和方法区有着很明显的不确定性:一个接口的多个实现类需要的内存可能会不一样;一个方法所执行的不同条件分支所需要的内存可能也不一样,只有处于运行期间,我们才知道程序究竟会创建哪些对象(实例数据——堆和类型数据——方法区),需要对少内存。所以垃圾回收主要体现在堆和方法区。
- 如何判断对象已死? 4.1 引用计数法 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器数值加一;当引用失效时就减一。任何时刻只要计数器为零的对象就是不可能再被使用的。 但是引用计数法最大的缺陷是不能解决循环依赖的问题。 4.2 可达性分析 基本思路是通过一系列(强调GC Roots不唯一) 被称为“GC roots”的根对象为起始节点集,从这些节点集开始往下搜索,无法到达的对象就是不再被使用的。 GC Roots的对象包括以下几种: 1) 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法对战中使用到的参数、局部变量、临时变量等 2)在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量 3)在方法区中常量引用的对象,例如字符串常量池(String table)里的引用。 4)在本地方法栈中JNI(通常所说的Native)引用的对象。 5)Java虚拟机内部的应用,如基本数据类型对应的Class对象,一些常驻异常对象(例如NPE,OOM),还有系统加载类。 6)所有被同步锁(synchronizee关键字)持有的对象 7)反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
可以这样记忆和理解。GC Roots肯定是能够确定一定在使用的,那哪些是一定确定的呢?1)正在被线程使用的——Java虚拟机栈和本地方法栈中被引用的对象;2)方法区中的常量;3)系统常用的;4)被锁锁定的;5)系统自用的。
- 再谈引用 无论是可达性分析还是计数器,都离不开引用。在JDK 1.2之前引用的定义很传统:如果reference类型的数据中存储的述职代表的是另外一块内存的起始地址,就称为引用。但是这种太简单粗暴了,无法对一些“食之无味,弃之可惜”的对象区分出来,所以就有了软引用、弱引用和虚引用,通过这种拆分来更加精细化的管理。
- 强引用:就是正常引用。
- 软引用:垃圾回收期二次回收时终止。例如第一次回收发现内存不够,会进行二次回收,此时会回收软引用的对象,如果还不够就报OOM。
- 弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生为之(连二次都不需要),垃圾收集器工作不管内存足不足,就被回收。例如TreadLocal等;
- 虚引用:虚引用只是为了获取这个对象被收集时收到一个系统通知。
- 生存还是死亡 并不是不可达就立马被回收,而是会进行二次标记。二次标记之后会放入F-Queue队列,然后又一条有虚拟机自动简历、低调度优先级的Finalizer线程去执行他们的finalize()方法。被调用但是并不一定承诺成功,因为可能她执行慢或死循环,那么就会导致其他对象的释放被阻塞。
- 回收方法区 回收新生代的收益差不多是70-90%,但是回收方法区的收益很低。主要是常量池的清理和类卸载。所以如果是大量使用反射、动态代理等,通常需要使用该能力,防止对方法区造成过大的内存压力。
- 垃圾收集算法 8.1 基于两个分代假设理论,当前商业虚拟机的垃圾收集器,大都采用了“分代收集”。分代理论假设是基于经验法则得出的,详情是: 1)弱分代假设,绝大部分对象都是朝生暮死的;2)强分代假设,熬过越多次垃圾回收的对象越难以消亡。 但是真实的情况是会出现跨代引用,所以为了找到真实的引用可能需要扫描另外一个代,所以就有了第三种假设。 3)跨代引用假设,跨代引用相对于同代引用来说仅占极少数。 有了第三代假设,就不需要为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在宽带引用,只需要在新生代上建立一个全局的数据结构(该结构成为记忆集,Remembered Set),这个结构把老年代划分成若干小块,表示出老年代的哪一块内存存在跨代引用。此后当发生Minor GC的时候,只需要判断是否包含了跨代小块内存里面的对象才会被加入到GC Roots进行扫描。 8.2 名词解释
- 部分收集(Partial GC):指目标不是完整收集Java堆的垃圾收集,又可以分为: 1)新生代收集(Minor GC/Young GC),指目标只是新生代的垃圾收集。 2)老年代收集(Major GC/Old GC):只目标只是老年代的垃圾收集,目前只有CMS收集器会有单独收集老年代的行为。同时,要注意Major GC现在有多种含义,一种是整堆收集。 3)混合收集(Mixed GC):收集整个新生代和部分老年代,目前只有G1收集器会有这种行为。
- 整堆收集(Full GC):指目标是收集整个Java堆和方法区的垃圾收集。 9 收集算法概述
前面说的程序计数法和可达性分析属于如何判定是垃圾,但是具体的清除又分为3中:1)标记-清除算法;2)标记-复制;3)标记——整理。
- 标记-清除算法 如名字所示一样,算法分为标记和清除两个阶段。但是有2个缺点:1)执行效率不稳定,如果Java堆中有大量对象需要被清理,那么标记和清理过程耗时久;2)内存空间的碎片化严重。
- 标记-复制算法 主要为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。他把内存分为两块,每次只使用其中一块,当一块内存用完了之后,就把还活着的对象复制到另外一块空间。 优点:对于存活低的情况,效率很高,只需要复制很少的数据;缺点:1)内存使用率不高,只有一半。2)存货对象多时效率不高。 基于上面的特点,所以现代虚拟机把该算法用在新生代的垃圾回收。同时基于“大部分对象活不过第一次垃圾回收的原理”,像HotSpot实现时把新生代分为三块:Eden区和2块较小的Survivor空间,其中Eden:Survivor=8:1。每次垃圾收集时把Eden区和Survivor任存活的对象复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的Survivor空间。如果未使用的Survivor不够一次Minor GC呢?那就需要依赖其他内存区域(一般是是老年代)进行担保。
- 标记——整理算法 标记-复制的特性决定了他不适合老年代——存活对象多,需要外部担保的空间多,效率低。针对老年代的特性,提出了一种标记-整理算法。 标记整理算法和标记清除算法相似,知识标记整理算法需要移动对象地址:把存活的对象都移到内存空间的一端,然后直接清理掉边界以为的内存。即把标记完的清理动作变为标记完之后往内存一端移动。 优点:内存利用率变高。 缺点:如果存活对象多,那么整理的过程是一种很耗时的事情,同时这个过程必须暂停用户应用程序才能进行(Stop The World)。 标记-清除算法会导致空间碎片化,对内存分配和访问都提出了调整,同时内存的访问是用户程序最高频的操作,所以很影响吞吐量。 所以基于以上两点,是否移动都存在弊端——移动则内存回收复杂;不移动分配和使用复杂。所以如果注重吞吐量,那么建议采用标记-整理;如果在意延时,那么就采用标记-清除。 同时还可以把这两种结合起来使用,即平时多用标记-清理方法,当碎片化大到影响对象分配时,则采用标记-整理一次。
- HotSpot的算法细节实现