对象实例化
在实例化对象的过程中,JVM发生了什么化学反应?
(1)下面从最简单的 Object ref = new Object(); 进行分析,查看字节码如下:
18: new #8 // class java/lang/Object 21: dup 22: invokespecial #9 // Method java/lang/Object."<init>":()V 25: astore_2 26: return
NEW:如果找不到Class对象,则进行类加载。加载成功之后,在堆中分配内存,从Object开始到本类路径上的所有属性值都要分配内存。分配完毕之后,进行零值初始化。分配过程中,注意引用是占内存空间的,它是一个变量,占4个字节。NEW指令完成后,将指向实例对象的引用变量压入虚拟机栈顶。
DUP:在栈顶复制该引用,这时,栈顶前两个元素是指向堆内实例对象的引用变量。如果<init>方法有参数,还需要把参数压入操作栈。这两个引用变量的目的不同,其中底下的引用用于赋值,或者保存到局部变量表中;位于上层的引用变量作为句柄调用相关方法。
INVOKESPECIAL:调用对象实例方法,通过栈顶的引用变量调用<init>方法。注意:<clinit>是类初始化时执行的方法,而<init>是对象初始化时执行的方法。
(2)从执行步骤的角度看:
第一步,确认类元信息是否存在。当JVM接收到new指令,首先在metaspace中检查需要创建的类元信息是否存在。若不存在,则在双亲委派模式下对.class文件进行加载,如果找不到,就会抛出ClassNotFoundException异常,加载成功后会生成对应的Class对象。
第二步,分配对象内存。首先计算对象占用空间大小,如果实例成员变量是引用变量,则仅分配引用变量空间(4字节),接着在对内存划分一块内存空间给新对象。分配空间时,需要进行同步操作(如CAS、区域加锁等)。
第三步,设置默认值。成员变量值都需要设定默认值,即各种不同形式的零值。
第四步,设置对象头。设置新对象的哈希码、GC信息、锁信息、对象所属的类元信息。
第五步,执行init方法。初始化成员变量,执行实例代码块、调用类的构造方法,把堆内对象的首地址赋给引用变量。
垃圾回收
1.GC Roots
它是一组活跃的引用。可达性分析算法将从这些引用节点开始往下搜索,搜索所走的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,此对象是不可用的。如下图所示,
Object5、Object6、Object7到GC Roots是不可达的,所以它们会被判定为可回收对象。
在Java语言中,可以作为GC ROOTS的对象:
个人理解,GC Roots指的是一个引用,而不是实际的对象实例。
- 虚拟机栈中引用的对象,即局部变量。如一个方法中:Person p = new Person();p就可以作为GC Roots。
- metaspace中类静态属性引用对象。如 private static Person p = new Person();
- metaspace中常量引用对象。如 private final static Person p = new Person();
- 本地方法中的JNI引用的对象。
2.如何判断生存还是死亡?
要宣告一个对象真正死亡,需要两次标记:
第一次,进行可达性分析后,对象没有引用链项链,它会被第一次标记。然后判断是否有必要执行对象的finalize()方法。如果对象的finalize()方法没有被重写或者已经被调用过,则没有必要执行,无需进入第二次标记。判定死亡。
如果需要执行finalize()方法,则此对象会被放入F-Queue队列中。并稍后由JVM创建的Finalizer线程执行F-Queue队列中对象的finalize()方法。对象可以在finalize()方法中重新与引用链上的任一引用关联完成自救。然后,GC将对F-Queue中的对象进行二次标记,如果此时自救成功,对象将被移出“即将回收的集合”,否则,此对象基本就被回收了。
3.垃圾回收算法
(1)标记-清除算法
该算法从每个GC Roots触发,依次标记有引用关系的对象,最后将没有标记的对象清除。
不足:
- 这种算法会带来大量的空间碎片,导致需要分配一个较大连续空间时容易触发FGC。
- 效率,标记和清除效率都不高。
(2)复制算法
将内存空间划分为大小相等两块,每次只使用其中的一块。当一块内存用完了,就把还存活的对象复制到另外一块上面,然后再把使用过的内存空间一次清理掉。
优点:对整个半区进行内存回收,内存分配时不用考虑内存碎片,只要移动堆顶指针,按顺序分配内存即可,实现简单高效。
不足:内存缩小为原来的一半。
新生代中98%的对象都是“朝生夕死”的,所以并不需要1:1的比例划分内存空间,是将内存区分为较大的Eden区和两块较小的Survivor区。当回收时,将Eden区和Survivor存活的对象一次性复制到另外一个survivor空间,最后清理掉原来的两块空间。Hotspot默认的比例是8:1。也就是说,每次新生代中可用的内存空间为整个新生代空间的90%,因此只有10%的空间被浪费。
(3)标记-整理算法
复制算法在对象存活率较高时要进行较多的复制操作,效率将会变低。老年代一般不能直接选用这种算法。
标记-整理算法标记的过程标记-清除一样,但是后续不是直接清理,而是把所有的存活对象都向一端移动,然后直接清理掉边界以外的内存。
(4)分代收集算法
它是基于复制算法的。按照对象生命周期的不同划分区域以及采用不同的垃圾回收算法,提高JVM的回收效率。
jdk1.8的堆内存
年轻代存活率低,可以使用复制算法,可以减少复制和空间浪费;老年代存活率高,可以采用标记整理算法。
GC分类
Miinor GC : 由于新生代的更替非常快,具有朝生夕灭的状态,因此新生代会频繁的触发minorGC
Full GC :一般对老年代进行full gc。老年代存活比较久,并且占用的空间比较大,一次full gc的开销比较大,所以也JVM也不会经常触发full gc。
年轻代中,尽可能快速地收集那些生命周期短的对象。
它分为一个Eden区和两个Survivor区。
新生代对象一般都是分配到Eden区,然后,当Eden区首次填满了之后,会触发一次Minor GC,此时会把Eden区中存活的对象复制到其中一块Survivor区,此处设置为Survivor0,并将存活对象的生命期+1,并清空Eden区。
当下次Eden区再次填满了之后,又会触发MinorGC,然后会将Eden区和Survivor0存活的对象复制到Survivor1区,并且清空Eden和Survivor0区。
survivor0和survivor1就如此交替使用。
一般来说,年轻代对象的生命期阈值为15,如果超过了阈值,就要移到老年代。
年轻代如何晋升为老年代:
- 经历一定Minor次数依然存活的对象
- 回收时Survivor对存不下的对象
- Eden都放不下的大对象
老年代中,存放生命周期较长的对象。采用标记-清理算法或标记-整理算法进行回收。
触发full gc条件:
- 老年代的空间不足
- 永久代空间不足(jdk8之前)
- CMS GC时出现promotion failed,concurrent mode failure。
- Minor GC晋升到老年代的平均大小大于老年代的剩余空间
- 调用System.gc()
4.垃圾收集器
两个收集器之间存在连线,说明它们可以搭配使用。
(1)Serial / Serial Old收集器
单线程收集器,在工作时只会使用一个CPU或一个线程去完成垃圾收集工作,并且必须暂停其他所有的工作线程,直到收集结束。
“Stop the world”是由虚拟机后台自动发起的,在用户不可见的情况下把用户正常工作的线程都停止掉,对很多应用来说是可以接受的。
其中Serial收集器是用于新生代(年轻代)的收集,采用复制算法;Serial Old收集器用于老年代,采用标记整理算法。
Serial是JVM Client模式下默认的垃圾收集器。
优点:简单高效,没有线程交互的开销。
(2)ParNew收集器
Serial的多线程版本。它是Server模式下的默认新生代收集器。
ParNew收集器在单线程环境下绝对不会有比Serial收集器更好的效果。它默认开启的线程数等于cpu的个数。
(3)Parallel Scavenge收集器
它是一个新生代的收集器,也是采用复制算法,又是采用多线程,它与ParNew不同的是,PS收集器的目标的是达到一个可控制的吞吐量,吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),
它无法与CMS收集器配合使用。
参数:
- -XX:MaxGCPauseMills:最大垃圾回收停顿时间。此参数并不是越小越好,GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的
- -XX:GCTimeRatio:0-100,指垃圾收集时间占总时间的比率,是吞吐量的倒数。
- -XX:+UseAdaptiveSizePolicy:开启此参数,虚拟机会根据当前系统的运行情况来收集性能监控信息,动态调整SurvivorRatio等参数,以提供合适的停顿时间或最大吞吐量
(4)Parallel Old收集器。
它是Parallel Scavenge的老年代版本,在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel scavenge + Parallel Old的组合。
(5)CMS(Concurrent Mark Sweep)收集器
它是一种以获取最短回收停顿时间为目标的收集器。它的运作过程包括
- 1.初始标记(Initial Mark):标记GC roots 能够直接关联的对象。
- 2.并发标记(Concurrent Mark):进行GC roots Tracing。
- 3.重新标记(Remark):修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
- 4.并发清除(Concurrent Sweep)
其中,初始标记和重新标记依然会触发STW(Stop the word),暂停所有工作线程。而并发标记和并发清除是可以跟用户线程并行的。
CMS的缺点:
- 对CPU的资源非常敏感,并发阶段,会占用一部分线程而导致程序变慢,总吞吐量降低。
- 无法处理浮动垃圾。CMS并发清理阶段会有用户线程存在,这部分线程仍然会产生垃圾,而当次的CMS回收是不能清理的。
- 基于标记清除算法,容易产生碎片。
参数:
- -XX:CMSInitiatingOccupancyFranction:老年代触发CMS回收空间占比阈值。JDK6默认为92%。
- -XX:+UseCMSCompactAtFullCollection:强制FullGC完成后要对老年代内存碎片压缩合并。
- -XX:CMSFullGCsBeforeCompaction:用于设置执行多少次不压缩的FullGC后,JVM再进行老年代的空间碎片整理。
(6)G1(Garbage-First Garbage Collector)收集器
Humongous是特殊的Old类型,专门放置大对象。G1将空间分为多个区域,优先回收垃圾最多的区域。G1的一大优势在于可预测的停顿时间,能够尽可能快地在指定时间内完成垃圾回收任务。
G1的特点:
- 并发与并行:G1充分利用多CPU、多核环境下的优势,使用多个CPU来缩短STW听段时间。G1收集器可以通过并发的方式让Java程序执行。
- 分代收集:G1总体上是基于“标记-整理”算法,但每个region之间是“复制”实现的,这意味两种算法都不会产生碎片。
- 可停顿预测,G1能够建立可预测的停顿时间模型,能够让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。
在G1中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,JVM是使用Remembered Set避免全堆扫描的。
G1中每个region都有与之对应的remembered set,当虚拟机发现程序堆Reference类型的数据进行写操作时,会产生一个Write Barrier中断写操作,检查Reference引用的对象是否处于不同的两个region中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的region的remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。
G1收集器的运作步骤分为以下几个步骤:
- 初始标记:标记GC Roots直接关联的对象。会引起STW。
- 并发标记:从GC Root开始对堆中对象进行可达性分析,找出存活对象,可以与用户线程并发执行。不会引起STW。
- 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,会引起STW,与CMS类似
- 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。