3、垃圾收集和内存分配策略


程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,内存的分配和回收具有确定性,不需要考虑回收的问题。


堆和方法区需要考虑内存分配 。


判断对象已死的方法


引用计数法 Reference Counting :实现简单,判定效率高;存在循环引用的问题;


可达性分析算法Reachability Analysis: GC Roots作为起始点,向下搜索,对象不可达时就判定为可回收对象。


可作为GC Roots的对象包括:


虚拟机栈(栈桢中的本地变量表)中引用的对象


方法区中类静态属性引用的对象


方法区中常量引用的对象


本地方法栈中JNI(即一般说的Native方法)引用的对象


引用分类:


强引用 Strong Reference: Object obj = new Object()这类的引用,只要强引用存在就不会回收。


软引用 Soft Reference:描述还有用但非必需的对象,当系统将要发生内存溢出之前的GC才会回收。


弱引用 Weak Reference:每次GC Collector工作时都会回收


虚引用 Phantom Reference:设置虚引用关联的唯一目的就是在这个对象被收集器回收时收到一个系统通知。


对象生存还是死亡:


1.首先判断 GC Roots不可达。


2. 将会被标记一次并判断是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被调用过一次,都视为没有必要执行。


任何一个对象的finalize()方法都只会被系统调用一次。


GC算法:


标记-清除算法 Mark-Sweep :效率问题:效率不高;空间问题:产生大量不连续的内存碎片


复制算法 Copying: 将内存分成两块,一块用的完了,就将还存活的对象复制到另一块上;代价是每次使用的内存缩小为原来的一半。


标记-整理算法 Mark-Compact :先标记,然后让存活的对象向一端移动,再清理。


分代收集算法 Generational Collection :新生代适合用复制算法,Eden:Survivor 8:1;


HotSpot算法实现:


枚举根结点 :OopMap的数据结构来直接得知哪些地方存在着对象引用,可以快速准确地完成GC Roots枚举。


安全点 Safepoint :OopMap可能导致引用关系变化,即如果为每一条指令生成对应的OopMap,需要大量的额外空间。


程序执行时只在Safepoint才能暂停.


Safepoint的选定以“是否具有让程序长时间执行的特征”,“长时间执行”最明显的特征就是指令序列利用,如方法调用、循环跳转、异常跳转等,所以这些功能的指令才会产生Safepoint.


如何让GC执行时所有程序跑到最近的Safepoint上,有两种方案


抢占式中断 Preemptive Suspension: 先中断所有线程,发现不在Safepoint上的再恢复,让它跑到最近的Safepoint.


主动式中断 Voluntary Suspension :GC需要中断的时候先设置一个标志,各个线程执行的时候去轮询标志,发现标志为真时就主动挂起。 轮询标志的地方和安全点是重合的。


安全区域 Safe Region: GC的时候线程处于Sleep或Block状态,无法跑到最近的安全点。


线程在执行Safe Region中的代码时,JVM 要发起GC都是安全的,当线程离开Safe Region时,检查系统是否已经完成的根结点枚举或GC,如果完成了就继续执行,如果没完成则等待。


垃圾收集器:


Serial Collector : 新生代的选择,单线程,采用复制算法;进行垃圾收集是必须暂停所有的线程。


简单而高效,Client模式下的JVM的很好的选择。


ParNew Collecto r:多线程。


Server模式下的JVM中首选的新生代Collector;默认开启的线程数与CPU数量相同。


-XX:ParallelGCThreads参数来GC Collector的线程数


并行 Parallel: 指多条垃圾收集线程并行工作,此时用户线程处于等待。


并发 Concurrent: 指用户线程和垃圾线程同时执行。


Parallel Scavenge Collector :它的关注点与其他Collector不同,其它Collector的关注点主要是缩短GC时User Thread stop time,而Parallel Collector的目标是达到一个可控制的吞吐量(Throughput)。


Throughput=用户代码执行时间/(用户代码执行时间+GC执行时间),越大越好。


主要适合在后台运算而不需要太多前台交互的任务。


Serial Old Collector: Serial Collector的老年代版本,Single Thread,Mark-Sweep算法。


Parallel Old Collecto r:Parallel Collector的老年代版本,Multi-thread,Mark-Sweep Algorithm.


CMS (Concurrent Mark Sweep) Collector : Mark-Sweep Algorithm,


步骤:


初始标记 CMS initial mark:


并发标记 CMS concurrent mark:


重新标记 CMS remark:


并发清除 CMS concurrent sweep:


initial mark和remark需要暂停用户线程,concurrent mark和concurrent sweep不需要。


优点:并发收集、低停顿。


缺点:对CPU资源非常敏感;无法处理浮动垃圾;由于采用mark-sweep,有大量空间碎片。


G1(Garbage First) Collector : 面向Server端;整体基于copy算法,局部(两个Region之间)基于mark-compact算法。


并行与并发;分代收集;空间整合;可预测的停顿


步骤:


initial marking


Concurrent Marking


最终标记 Final Marking


筛选回收 Live Data Counting and Evacuation


内存分配与回收策略:


对象优先在Eden分配: 当Eden没有足够空间分配时,触发一次Minor GC


大对象直接进入tenured generation :大对象指需要连续内存空间的java对象。如长字符串或数组。


-XX:PretenureSizeThreshold,大于这个设置值的对象直接在tenured generation分配


长期存活的对象将进入tenured generation : JVM给每个Object定义了一个对象年龄计数器,对象从Eden出生,每经过一次Minor GC并且被移到Survivor后,计数器就加1。


-XX:MaxTenuringThreshold:年龄阈值设置,默认15岁。


动态对象年龄判定 :如果Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就会直接移入tenured generation.


空间分配担保: Minor GC之前,JVM会检查 tenured generation中最大可用连续空间 是否大于Young generation中所有对象大小总和,如果成立,则安全,如果不成立,检查 HandlePromotionFailure 设置值, tenured generation中最大可用连续空间 大于历次晋升需要tenured generation的平均大小。



7、虚拟机类加载机制


虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。


加载、验证、准备、解析、初始化、使用和卸载7个阶段


有且仅有 以下5种情况必须立即对类进行初始化:


1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。


2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。


3)当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。


4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机 会先初始化这个主类。


5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。


这5种是对一个类进行主动引用,除此之外,所有引用类的方式都不会触发初始化,为类的被动引用。


被动引用如:1)通过子类引用父类的静态字段,不会导致子类初始化。


2)通过数组定义来引用类,不会导致此类的初始化


3)常量在编译阶段会被存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发类的初始化。


接口的初始化与类的初始化的差异:接口的初始化并不需要其父类全部都初始化了,只有真正用到父接口的时候(如引用接口中定义的常量)才会初始化。