JVM对象探秘
HotSpot虚拟机对象探秘
介绍完Java虚拟机的运行时数据区之后,我们大致知道了虚拟机内存的概况,读者了解了内存中放了些什么后,也许就会想更进一步了解这些虚拟机内存中的数据的其他细节,譬如它们是如何创建、如何布局以及如何访问的。对于这样涉及细节的问题,必须把讨论范围限定在具体的虚拟机和集中在某一个内存区域上才有意义。基于实用优先的原则,笔者以常用的虚拟机HotSpot和常用的内存区域Java堆为例,深入探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
对象的创建
Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,而在虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个过程呢?
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,后续将探讨这部分内容的细节。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定(如何确定将在后续中介绍),为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。关于对象头的具体内容,稍后再做详细介绍。
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始——<init>方法还没有执行,所有的字段都还为零。所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
下面的代码清单是HotSpot虚拟机bytecodeInterpreter.cpp中的代码片段(这个解释器实现很少有机会实际使用,因为大部分平台上都使用模板解释器;当代码通过JIT编译器执行时差异就更大了。不过,这段代码用于了解HotSpot的运作过程是没有什么问题的)。
//确保常量池中存放的是已解释的类
if(!constants->tag_at(index).is_unresolved_klass()){
//断言确保是klassOop和instanceKlassOop(这部分下一节介绍)
oop entry=(klassOop)*constants->obj_at_addr(index);
assert(entry->is_klass(),"Should be resolved klass");
klassOop k_entry=(klassOop)entry;
assert(k_entry->klass_part()->oop_is_instance(),"Should be instanceKlass");
instanceKlass * ik=(instanceKlass*)k_entry->klass_part();
//确保对象所属类型已经经过初始化阶段
if(ik->is_initialized()&&ik->can_be_fastpath_allocated())
{
//取对象长度
size_t obj_size=ik->size_helper();
oop result=NULL;
//记录是否需要将对象所有字段置零值
bool need_zero=!ZeroTLAB;
//是否在TLAB中分配对象
if(UseTLAB){
result=(oop)THREAD->tlab().allocate(obj_size);
}
if(result==NULL){
need_zero=true;
//直接在eden中分配对象
retry:
HeapWord * compare_to=*Universe:heap()->top_addr();
HeapWord * new_top=compare_to+obj_size;
/*cmpxchg是x86中的CAS指令,这里是一个C++方法,通过CAS方式分配空间,如果并发失败,
转到retry中重试,直至成功分配为止*/
if(new_top<=*Universe:heap()->end_addr()){
if(Atomic:cmpxchg_ptr(new_top,Universe:heap()->top_addr(),compare_to)!=compare_to){
goto retry;
}
result=(oop)compare_to;
}
}
if(result!=NULL){
//如果需要,则为对象初始化零值
if(need_zero){
HeapWord * to_zero=(HeapWord*)result+sizeof(oopDesc)/oopSize;
obj_size-=sizeof(oopDesc)/oopSize;
if(obj_size>0){
memset(to_zero,0,obj_size * HeapWordSize);
}
}
//根据是否启用偏向锁来设置对象头信息
if(UseBiasedLocking){
result->set_mark(ik->prototype_header());
}else{
result->set_mark(markOopDesc:prototype());
}r
esult->set_klass_gap(0);
result->set_klass(k_entry);
//将对象引用入栈,继续执行下一条指令
SET_STACK_OBJECT(result,0);
UPDATE_PC_AND_TOS_AND_CONTINUE(3,1);
}
}
}
请简单阐述一下对象的创建过程?
public static void main(String[] args) {
Object object=new Object();
String string=new String();
}
main方法中创建了两个对象执行过程在右边字节码中展示完全一致new、dup、invokespecial、astore四个步骤
1、new,虚拟机指令为对象分配内存并在栈顶压入了指向这段内存的地址供后续操作来调用
2、dup,其实就是一个复制操作,其作用是把栈顶的内容复制一份再压入栈。jvm为什么要这么做呢
这完全是jvm自己编译优化的做法,再后续操作之前虚拟机自己会调用一次。我们都知道对象都有一个this的关键字指向对象本身,this是什么时候赋值的呢,就是这个时候
至于另一个引用当然是赋值给方法中的变量了
3、invokespecial,该过程是对实例对象进行初始化,第一步分配内存后对象内的实例变量都是初始值,在该步骤才会初始化对象内的实例变量
4、astore,方法内的变量指向内存中的对象
对象的内存布局
对象内存构成
Java 中通过 new 关键字创建一个类的实例对象,对象存于内存的堆中并给其分配一个内存地址,那么是否想过如下这些问题:
- 这个实例对象是以怎样的形态存在内存中的?
- 一个Object对象在内存中占用多大?
- 对象中的属性是如何在内存中分配的?
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:
- 对象头(object header):包括了关于堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息。Java对象和vm内部对象都有一个共同的对象头格式。
- 实例数据(Instance Data):主要是存放类的数据信息,父类的信息,对象字段属性信息。
- 对齐填充(Padding):为了字节对齐,填充的数据,不是必须的。
对象头
参考网址:http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
object header
Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.
我们可以在Hotspot官方文档中找到它的描述。从中可以发现,它是Java对象和虚拟机内部对象都有的共同格式,由两个字(计算机术语)组成。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。
它里面提到了对象头由两个字组成,这两个字是什么呢?我们还是在上面的那个Hotspot官方文档中往上看,可以发现还有另外两个名词的定义解释,分别是 mark word 和 klass pointer。
klass pointer
The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the "klass" contains a C++ style "vtable".
mark word
The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.
Mark Word
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32位、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
我们打开openjdk的源码包,对应路径/openjdk/hotspot/src/share/vm/oops
,Mark Word对应到C++的代码markOop.hpp
,可以从注释中看到它们的组成。代码为HotSpot虚拟机markOop.cpp中的代码(注释)片段,它描述了32bit下MarkWord的存储状态。
//Bit-format of an object header(most significant first,big endian layout below):
//32 bits:
//--------
//hash:25------------>|age:4 biased_lock:1 lock:2(normal object)
//JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2(biased object)
//size:32------------------------------------------>|(CMS free block)
//PromotedObject*:29---------->|promo_bits:3----->|(CMS promoted object)
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的
在64位JVM中是这么存的:
虽然它们在不同位数的JVM中长度不一样,但是基本组成内容是一致的。
- 锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
- biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
- 分代年龄(age):表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
- 对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。
- 偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
- epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。
Klass Pointer
即类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身
实例数据
如果对象有属性字段,则这里会有数据信息。如果对象无属性字段,则这里就不会有数据。根据字段类型的不同占不同的字节,例如boolean类型占1个字节,int类型占4个字节等等;实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变量的空隙之中。
对齐数据
对象可以有对齐数据也可以没有。默认情况下,Java虚拟机堆中对象的起始地址需要对齐至8的倍数。如果一个对象用不到8N个字节则需要对其填充,以此来补齐对象头和实例数据占用内存之后剩余的空间大小。如果对象头和实例数据已经占满了JVM所分配的内存空间,那么就不用再进行对齐填充了。
所有的对象分配的字节总SIZE需要是8的倍数,如果前面的对象头和实例数据占用的总SIZE不满足要求,则通过对齐数据来填满。
为什么要对齐数据?字段内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址。
分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象内存实战
概念的东西是抽象的,你说它是这样组成的,就真的是吗?学习是需要持怀疑的态度的,任何理论和概念只有自己证实和实践之后才能接受它。还好 openjdk 给我们提供了一个工具包,可以用来获取对象的信息和虚拟机的信息,我们只需引入 jol-core 依赖,如下
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
jol-core 常用的三个方法
- ClassLayout.parseInstance(object).toPrintable():查看对象内部信息.
- GraphLayout.parseInstance(object).toPrintable():查看对象外部信息,包括引用的对象.
- GraphLayout.parseInstance(object).totalSize():查看对象总大小.
普通对象
为了简单化,我们不用复杂的对象,自己创建一个类 D,先看无属性字段的时候
public class D {
}
通过 jol-core 的 api,我们将对象的内部信息打印出来
public static void main(String[] args) {
D d = new D();
System.out.println(ClassLayout.parseInstance(d).toPrintable());
}
最后的打印结果为
com.demo.D object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) //Mark 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) //Kclass 05 c4 00 20 (00000101 11000100 00000000 00100000) (536921093)
12 4 (loss due to the next object alignment) //padding
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到有 OFFSET、SIZE、TYPE DESCRIPTION、VALUE 这几个名词头,它们的含义分别是
- OFFSET:偏移地址,单位字节;
- SIZE:占用的内存大小,单位为字节;
- TYPE DESCRIPTION:类型描述,其中object header为对象头;
- VALUE:对应内存中当前存储的值,二进制32位;
可以看到,d对象实例共占据16byte,对象头(object header)占据12byte(96bit),其中 mark word占8byte(64bit),klass pointe 占4byte,另外剩余4byte是填充对齐的。
网址:https://docs.oracle.com/javase/7/docs/technotes/guides/vm/performance-enhancements-7.html
这里由于默认开启了指针压缩 ,所以对象头占了12byte,具体的指针压缩的概念这里就不再阐述了,感兴趣的读者可以自己查阅下官方文档。jdk8版本是默认开启指针压缩的,可以通过配置vm参数开启关闭指针压缩,-XX:-UseCompressedOops
。
如果关闭指针压缩重新打印对象的内存布局,可以发现总SIZE变大了,从下图中可以看到,对象头所占用的内存大小变为16byte(128bit),其中 mark word占8byte,klass pointe 占8byte,无对齐填充。
开启指针压缩可以减少对象的内存使用。从两次打印的D对象布局信息来看,关闭指针压缩时,对象头的SIZE增加了4byte,这里由于D对象是无属性的,读者可以试试增加几个属性字段来看下,这样会明显的发现SIZE增长。因此开启指针压缩,理论上来讲,大约能节省百分之五十的内存。jdk8及以后版本已经默认开启指针压缩,无需配置。
数组对象
上面使用的是普通对象,我们来看下数组对象的内存布局,比较下有什么异同
public static void main(String[] args) {
int[] a = {1};
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
打印的内存布局信息,如下
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) //Mark 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 20 (01101101 00000001 00000000 00100000) (536871277) //Kclass
12 4 (object header) //array length 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 4 int [I.<elements> //实例数据 N/A
20 4 (loss due to the next object alignment) //对齐填充
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到这时总SIZE为共24byte,对象头占16byte,其中Mark Work占8byte,Klass Point 占4byte,array length 占4byte,因为里面只有一个int 类型的1,所以数组对象的实例数据占据4byte,剩余对齐填充占据4byte。
结尾
经过以上的内容我们了解了对象在内存中的布局,了解对象的内存布局和对象头的概念,特别是对象头的Mark Word的内容,在我们后续分析 synchronize 锁优化 和 JVM 垃圾回收年龄代的时候会有很大作用。
JVM中大家是否还记得对象在Suvivor中每熬过一次MinorGC,年龄就增加1,当它的年龄增加到一定程度后就会被晋升到老年代中,这个次数默认是15岁,有想过为什么是15吗?在Mark Word中可以发现标记对象分代年龄的分配的空间是4bit,而4bit能表示的最大数就是2^4-1 = 15。
对象的访问定位
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图。
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,如图所示。
这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。就本书讨论的主要虚拟机Sun HotSpot而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
图解对象分配过程
为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。
new 的对象先放伊甸园区。此区有大小限制。 当伊甸园的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区 然后将伊甸园中的剩余对象移动到幸存者 0 区。 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区的,如果没有回收,就会放到幸存者 1 区。 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区。 啥时候能去养老区呢?可以设置次数。默认是 15 次。 可以设置参数:-Xx:MaxTenuringThreshold= N进行设置 在养老区,相对悠闲。当养老区内存不足时,再次触发 GC:Major GC,进行养老区的内存清理 若养老区执行了 Major GC 之后,发现依然无法进行对象的保存,就会产生 OOM 异常。
java.lang.OutofMemoryError: Java
总结
- 针对幸存者 s0,s1 区的总结:复制之后有交换,谁空谁是 to
- 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集
Minor GC、MajorGC和Full GC对比
JVM 在进行 GC 时,并非每次都对上面三个内存区域(新生区,老年区,方法区)一起回收的,大部分时候回收的都是指新生代。
针对 Hotspot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)
部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为: 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集 老年代收集(Major GC / Old GC):只是老年代的圾收集。 目前,只有 CMSGC 会有单独收集老年代的行为。 注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。 目前,只有 G1 GC 会有这种行为 整堆收集(Full GC):收集整个 java 堆和方法区(程序执行过程中需要加载的类)的垃圾收集。
年轻代 GC(Minor GC)触发机制
当年轻代空间不足时,就会触发 MinorGC。这里的年轻代满指的是 Eden 代满==,Survivor 满不会引发 GC==。(每次 Minor GC 会清理年轻代的内存。) 因为Java 对象大多都具备朝生夕灭的特性.,所以 Minor GC 非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。 Minor GC 会引发 STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行 备注:当Survivor满了,超出部分直接放在老年区,其他部分保持不变.等下次Eden满了,触发Minor GC,再对Survivor进行垃圾清理. 即 Survivor区的垃圾回收是被动的
老年代 GC(Major GC / Full GC)触发机制
指发生在老年代的 GC,对象从老年代消失时,我们说 “Major GC” 或 “Full GC” 发生了 出现了 Major Gc,经常会伴随至少一次的 Minor GC(但非绝对的,在 Paralle1 Scavenge 收集器的收集策略里就有直接进行 MajorGC 的策略选择过程) 也就是在老年代空间不足时,会先尝试触发 Minor Gc。如果之后空间还不足,则触发 Major GC(??) Major GC 的速度一般会比 Minor GC 慢 10 倍以上,STW 的时间更长(所以后序调优时,要尽量减少其出现次数.) 如果 Major GC 后,内存还不足,就报 OOM 了
Full GC 触发机制
触发 Full GC 执行的情况有如下五种:
调用 System.gc()时,系统建议执行 Full GC,但是不必然执行 老年代空间不足 方法区空间不足 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存 由 Eden 区、survivor space0(From Space)区向 survivor space1(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小,则会触发。 说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些
创建一个新对象 内存分配流程
绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发 YoungGarbageCollection,即 YGC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区。Survivor区分为so和s1两块内存空间。每次 YGC的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果 YGC要移送的对象大于Survivor区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,就像人到了18岁就会成年一样,在JVM中 -XX:MaxTenuringThreshold参数就是来配置一个对象从新生代晋升到老年代的阈值。默认值是 15 , 可以在Survivor区交换14次之后,晋升至老年代。