虚拟机在内存中申请一片区域,由虚拟机自动管理,用来满足应用程序对象分配的空间需求,即堆空间
。
由于程序运行的局部特性,程序创建的大多数对象都具有非常短的生命周期,而程序也会创建一些生命周期特别长的对象。简单的复制收集器无论对象的 生命周期是长是短,都会进行复制操作。而生命周期较长的对象在多次垃圾回收期间内并不会被回收,这就使得这些对象被来回复制而使得算法性能大大下降。
分代收集把堆分为多个子堆,分别用来存放不同寿命的对象。新生对象空间的将经历最频繁的垃圾回收,而对于经历了若干次垃圾收集后仍然存活的对象,将成长为成熟对象,并移动到成熟对象的子堆中,而对老生代子堆的垃圾回收就不会像新生对象子堆那么频繁。
HotSpot
的堆空间分为新生代(YoungGen
)和老年代(OldGen
,此外还有位于非堆空间的永久代,但在Java8
中将移除永久代),新生代又分为Eden
区和2个Survivor
区(From
/To
)用以进行复制收集垃圾对象。
对Java
堆和对象的分析将从Java
堆的创建开始,然后分析Java
对象的分配与垃圾回收。
在虚拟机的创建初始化过程中,通过调用Universe
的成员函数initialize_heap()
将完成Java
堆的初始化。在Universe
模块下的初始化将根据虚拟机选项来选择堆的具体实现方式:
UseParallelGC
,则Java
堆的堆类型为ParallelScavengeHeap
(并行收集堆)//定义在/hotspot/src/share/vm/memory/universe.cpp中 if (UseParallelGC) { #ifndef SERIALGC Universe::_collectedHeap = new ParallelScavengeHeap(); #else // SERIALGC fatal("UseParallelGC not supported in java kernel vm."); #endif // SERIALGC
UseG1GC
,那么将选择堆类型为G1CollectedHeap
,垃圾收集策略将使用专用的G1CollectorPolicy
(垃圾优先收集)策略else if (UseG1GC) { #ifndef SERIALGC G1CollectorPolicy* g1p = new G1CollectorPolicy_BestRegionsFirst(); G1CollectedHeap* g1h = new G1CollectedHeap(g1p); Universe::_collectedHeap = g1h; #else // SERIALGC fatal("UseG1GC not supported in java kernel vm."); #endif // SERIALGC }
GenCollectedHeap
(分代收集堆)Universe::_collectedHeap = new GenCollectedHeap(gc_policy);
gc_policy
来构造一个GenCollectedHeap
,这里根据虚拟机配置选择不同的GC策略:UseSerialGC
,那么将使用MarkSweepPolicy
(标记-清除)策略GenCollectorPolicy *gc_policy; if (UseSerialGC) { gc_policy = new MarkSweepPolicy(); }
(2) 若虚拟机配置UseConcMarkSweepGC
和UseAdaptiveSizePolicy
,那么将使用 ASConcurrentMarkSweepPolicy
(自适应并发标记-清除)策略,若没有指定UseAdaptiveSizePolicy
,虚拟机 将默认使用ConcurrentMarkSweepPolicy
(并发标记-清除)策略
else if (UseConcMarkSweepGC) { #ifndef SERIALGC if (UseAdaptiveSizePolicy) { gc_policy = new ASConcurrentMarkSweepPolicy(); } else { gc_policy = new ConcurrentMarkSweepPolicy(); }
(3).若没有进行配置,虚拟机将默认使用MarkSweepPolicy
策略
else { // default old generation gc_policy = new MarkSweepPolicy(); }
如下表所示:
其中垃圾回收策略类的关系如下图:
4. 接下来是相应实现的堆的初始化
jint status = Universe::heap()->initialize(); if (status != JNI_OK) { return status; }
LP64
平台上的指针压缩以及TLAB
的相关内容 。64
位JVM
消耗的内存会比32
位的大1.5
倍,这是因为在64
位环境下,对象将使用64
位指针,这就增加了一倍的指针占用内存开销。从JDK 1.6 update14
开始,64 bit JVM
正式支持了-XX:+UseCompressedOops
选项来压缩指针,以节省内存空间。addr = <narrow_oop_base> + <narrow_oop> << 3 + <field_offset>
4GB
(
2
32
2^{32}
232)时,直接使用32
位的压缩对象指针< narrow_oop >
就可以找到该对象4GB
(
2
32
2^{32}
232)但小于32GB
时,就必须借助偏移来获得真正的地址(对象是8字节
对齐的)。32GB
时,就需要借助堆的基址来完成寻址了,< narrow_oop_base >
为堆的基址,< field_offset >
为一页的大小。heap
的地址空间的最大地址大于OopEncodingHeapMax(32GB)
,则设置基础地址为当前堆的起始地址-页大小,设置偏移为LogMinObjAlignmentInBytes(3)
,即使用普通的对象指针压缩技术if ((uint64_t)Universe::heap()->reserved_region().end() > OopEncodingHeapMax) { // Can't reserve heap below 32Gb. Universe::set_narrow_oop_base(Universe::heap()->base() - os::vm_page_size()); Universe::set_narrow_oop_shift(LogMinObjAlignmentInBytes); }
(2) 否则设置基础地址为0
else { Universe::set_narrow_oop_base(0); //... }
若heap
的地址空间的最大地址大于NarrowOopHeapMax
(4GB,小于32GB),则设置偏移为LogMinObjAlignmentInBytes
(默认为3),即使用零基压缩技术,否则设置偏移为0,即直接使用压缩对象指针进行寻址
if((uint64_t)Universe::heap()->reserved_region().end() > NarrowOopHeapMax) { // Can't reserve heap below 4Gb. Universe::set_narrow_oop_shift(LogMinObjAlignmentInBytes); } else { Universe::set_narrow_oop_shift(0);
接下来分析特定堆的初始化过程,这里以GenCollectedHeap
和MarkSweepPolicy
为例:
GenCollectedHeap
的构造函数中使用传入的策略作为_gen_policy
(代策略)。以MarkSweepPolicy
为例,看看其构造函数:
//定义在/hotspot/src/share/vm/memory/collectorPolicy.cpp中 MarkSweepPolicy::MarkSweepPolicy() { initialize_all(); }
MarkSweepPolicy
的构造函数调用了initialize_all()
来完成策略的初始化,initialize_all()
是父 类GenCollectorPolicy()
的虚函数,它调用了三个子初始化虚函数,这三个子初始化过程由GenCollectorPolicy
的子类实 现。其中initialize_flags()
初始化了永久代的一些大小配置参数,initialize_size_info()
设置了Java
堆大小的相关参数,initialize_generations()
根据用户参数,配置各内存代的管理器。
//hotspot/src/share/vm/memory/collectorPolicy.hpp中 virtual void initialize_all() { initialize_flags(); initialize_size_info(); initialize_generations(); }
下面通过initialize_generations()
来看看各代有哪些实现方式:
UseParNewGC
,并且并行GC
线程数大于1
,那么新生代就会使用ParNew
实现//永久代初始化 _generations = new GenerationSpecPtr[number_of_generations()]; //... if (UseParNewGC && ParallelGCThreads > 0) { _generations[0] = new GenerationSpec(Generation::ParNew, _initial_gen0_size, _max_gen0_size); }
DefNew
实现else { _generations[0] = new GenerationSpec(Generation::DefNew, _initial_gen0_size, _max_gen0_size); }
MarkSweepCompact
实现_generations[1] = new GenerationSpec(Generation::MarkSweepCompact, _initial_gen1_size, _max_gen1_size);
DefNew、ParNew、MarkSweepCompact
等均为Generation
的枚举集合Name
的成员,描述了可能实现的各种代实现类型)MarkSweepPolicy、ConcurrentMarkSweepPolicy、ASConcurrentMarkSweepPolicy
对各代的实现综合如下表所示:分析完了构造函数,回到Universe
模块中堆的initialize()
以GenCollectedHeap
为例:
gc_policy
(分代策略)来初始化分代数//定义在/hotspot/src/share/vm/memory/genCollectedHeap.cpp中 jint GenCollectedHeap::initialize() { //... _n_gens = gen_policy()->number_of_generations();
根据GenCollectedHeap
的定义可以看到,GenCollectedHeap
最多支持10
个分代
enum SomeConstants { max_gens = 10 }; //... private: int _n_gens; Generation* _gens[max_gens];
其实并不需要这么多分代,MarkSweepPolicy
、ConcurrentMarkSweepPolicy
、 ASConcurrentMarkSweepPolicy
(ConcurrentMarkSweepPolicy
的子类)均有着共同的祖先类TwoGenerationCollectorPolicy
,其分代只有2代,即新生代和老年代。
2. 每代的大小是基于GenGrain
大小对齐的
// The heap must be at least as aligned as generations. size_t alignment = Generation::GenGrain;
GenGrain
定义在/hotspot/src/share/vm/memory/generation.h
中,在非ARM
平台中是2^16
字节,即64KB
大小
3. 获取各分代的管理器指针数组和永久代的管理器指针,并对齐各代的大小到64KB
PermanentGenerationSpec *perm_gen_spec = collector_policy()->permanent_generation(); // Make sure the sizes are all aligned. for (i = 0; i < _n_gens; i++) { _gen_specs[i]->align(alignment); } perm_gen_spec->align(alignment);
GenerationSpec
的align()
定义在/hotspot/src/share/vm/memory/generationSpec.h
,使初始和最大大小值向上对齐至64KB的倍数
// Alignment void align(size_t alignment) { set_init_size(align_size_up(init_size(), alignment)); set_max_size(align_size_up(max_size(), alignment)); }
allocate()
为堆分配空间,其起始地址为heap_address
char* heap_address; size_t total_reserved = 0; int n_covered_regions = 0; ReservedSpace heap_rs(0); heap_address = allocate(alignment, perm_gen_spec, &total_reserved, &n_covered_regions, &heap_rs);
_reserved
(CollectedHeap
的MemRegion
成员)中_reserved = MemRegion((HeapWord*)heap_rs.base(), (HeapWord*)(heap_rs.base() + heap_rs.size()));
misc_data
和misc_code
的空间,并创建一个覆盖整个空间的数组,数组每个字节对应于堆的512
字节,用于遍历新生代和老年代空间_reserved.set_word_size(0); _reserved.set_start((HeapWord*)heap_rs.base()); size_t actual_heap_size = heap_rs.size() - perm_gen_spec->misc_data_size() - perm_gen_spec->misc_code_size(); _reserved.set_end((HeapWord*)(heap_rs.base() + actual_heap_size)); _rem_set = collector_policy()->create_rem_set(_reserved, n_covered_regions); set_barrier_set(rem_set()->bs());
_gch = this; for (i = 0; i < _n_gens; i++) { ReservedSpace this_rs = heap_rs.first_part(_gen_specs[i]->max_size(),UseSharedSpaces, UseSharedSpaces); _gens[i] = _gen_specs[i]->init(this_rs, i, rem_set()); heap_rs = heap_rs.last_part(_gen_specs[i]->max_size()); } _perm_gen = perm_gen_spec->init(heap_rs, PermSize, rem_set());
那么GenCollectedHeap
是如何向系统申请内存空间的呢?
答案就在allocate()
函数中
UseLargePages
而不同,large_page_size
在不同平台上表现不同,x86
使用 2/4M
(物理地址扩展模式)的页大小,AMD64
使用2M
,否则,Linux默认内存页大小只有4KB,接下来会以各代所配置的最大大小进行计算,若最 大值设置为负数,那么jvm将报错退出,默认的新生代和老年代的分块数为1,而永久代的分块数为2char* GenCollectedHeap::allocate(size_t alignment, PermanentGenerationSpec* perm_gen_spec, size_t* _total_reserved, int* _n_covered_regions, ReservedSpace* heap_rs){ //... // Now figure out the total size. size_t total_reserved = 0; int n_covered_regions = 0; const size_t pageSize = UseLargePages ? os::large_page_size() : os::vm_page_size(); for (int i = 0; i < _n_gens; i++) { total_reserved += _gen_specs[i]->max_size(); if (total_reserved < _gen_specs[i]->max_size()) { vm_exit_during_initialization(overflow_msg); } n_covered_regions += _gen_specs[i]->n_covered_regions(); }
加上永久代空间的大小和块数
total_reserved += perm_gen_spec->max_size(); if (total_reserved < perm_gen_spec->max_size()) { vm_exit_during_initialization(overflow_msg); } n_covered_regions += perm_gen_spec->n_covered_regions();
(2) 加上永久代的misc_data
和misc_code
的空间大小(数据区和代码区),但其实并不是堆的一部分
size_t s = perm_gen_spec->misc_data_size() + perm_gen_spec->misc_code_size(); total_reserved += s;
(3) 如果配置了UseLargePages
,那么将向上将申请的内存空间大小对齐至页
if (UseLargePages) { assert(total_reserved != 0, "total_reserved cannot be 0"); total_reserved = round_to(total_reserved, os::large_page_size()); if (total_reserved < os::large_page_size()) { vm_exit_during_initialization(overflow_msg); } }
(4) 对象地址压缩的内容
根据UnscaledNarrowOop
(直接使用压缩指针)选取合适的堆起始地址,并尝试在该地址上分配内存
if (UseCompressedOops) { heap_address = Universe::preferred_heap_base(total_reserved, Universe::UnscaledNarrowOop); *_total_reserved = total_reserved; *_n_covered_regions = n_covered_regions; *heap_rs = ReservedHeapSpace(total_reserved, alignment, UseLargePages, heap_address);
若不能再该地址进行分配内存,则尝试使用ZereBasedNarrowOop
(零基压缩)尝试在更高的地址空间上进行分配
if (heap_address != NULL && !heap_rs->is_reserved()) { // Failed to reserve at specified address - the requested memory // region is taken already, for example, by 'java' launcher. // Try again to reserver heap higher. heap_address = Universe::preferred_heap_base(total_reserved, Universe::ZeroBasedNarrowOop); *heap_rs = ReservedHeapSpace(total_reserved, alignment, UseLargePages, heap_address);
若仍然失败,则使用普通的指针压缩技术在其他地址上进行分配
if (heap_address != NULL && !heap_rs->is_reserved()) { // Failed to reserve at specified address again - give up. heap_address = Universe::preferred_heap_base(total_reserved, Universe::HeapBasedNarrowOop); assert(heap_address == NULL, ""); *heap_rs = ReservedHeapSpace(total_reserved, alignment, UseLargePages, heap_address); } }
调用ReservedHeapSpace
的构造函数进行内存空间的申请
*_total_reserved = total_reserved; *_n_covered_regions = n_covered_regions; *heap_rs = ReservedHeapSpace(total_reserved, alignment, UseLargePages, heap_address); return heap_address;
在构造函数中并没有发现对内存空间进行申请,那么继续看父类ReservedSpace
的构造函数
ReservedSpace::ReservedSpace(size_t size, size_t alignment, bool large, char* requested_address, const size_t noaccess_prefix) { initialize(size+noaccess_prefix, alignment, large, requested_address, noaccess_prefix, false); }
initialize()
的实现如下:
(1) 如果目标操作系统不支持large_page_memory
,那么将进行特殊处理,此外,对指针压缩处理还需要对请求分配的内存空间大小进行调整
if (requested_address != 0) { requested_address -= noaccess_prefix; // adjust requested address assert(requested_address != NULL, "huge noaccess prefix?"); }
(2).对于上述特殊情况,会调用reserve_memory_special()
进行内存空间的申请,并若申请成功会进行空间大小的对齐验证
if (special) { //向操作系统申请指定大小的内存,并映射到用户指定的内存空间中 base = os::reserve_memory_special(size, requested_address, executable); if (base != NULL) { if (failed_to_reserve_as_requested(base, requested_address, size, true)) { // OS ignored requested address. Try different address. return; } // Check alignment constraints assert((uintptr_t) base % alignment == 0, "Large pages returned a non-aligned address"); _special = true;
(3) 若配置了UseSharedSpace
或UseCompressedOops
,那么堆将在指定地址进行申请,就会调用attempt_reserve_memory_at()
进行申请,否则,调用reserve_memory()
进行申请
if (requested_address != 0) { base = os::attempt_reserve_memory_at(size, requested_address); if (failed_to_reserve_as_requested(base, requested_address, size, false)) { // OS ignored requested address. Try different address. base = NULL; } } else { base = os::reserve_memory(size, NULL, alignment);
(4) 若分配成功,还需要对分配的起始地址进行对齐验证。若没有对齐,则会进行手工调整。调 整的方法为尝试申请一块size+alignment
大小的空间,若成功则向上对齐所得的内存空间的起始地址(失败则无法对齐,直接返回),并以此为起始 地址重新申请一块size大小的空间,这块size大小的空间必然包含于size+alignment
大小的空间内,以此达到对齐地址的目的。
// Check alignment constraints if ((((size_t)base + noaccess_prefix) & (alignment - 1)) != 0) { // Base not aligned, retry if (!os::release_memory(base, size)) fatal("os::release_memory failed"); // Reserve size large enough to do manual alignment and // increase size to a multiple of the desired alignment size = align_size_up(size, alignment); size_t extra_size = size + alignment; do { char* extra_base = os::reserve_memory(extra_size, NULL, alignment); if (extra_base == NULL) return; // Do manual alignement base = (char*) align_size_up((uintptr_t) extra_base, alignment); assert(base >= extra_base, "just checking"); // Re-reserve the region at the aligned base address. os::release_memory(extra_base, extra_size); base = os::reserve_memory(size, base); } while (base == NULL);
最后,在地址空间均已分配完毕,GenCollectedHeap
的initialize()
中为各代划分了各自的内存空间范围,就会调用各代的GenerationSpec
的init()函数完成各代的初始化。
switch (name()) { case PermGen::MarkSweepCompact: return new CompactingPermGen(perm_rs, shared_rs, init_size, remset, this); #ifndef SERIALGC case PermGen::MarkSweep: guarantee(false, "NYI"); return NULL; case PermGen::ConcurrentMarkSweep: { assert(UseConcMarkSweepGC, "UseConcMarkSweepGC should be set"); CardTableRS* ctrs = remset->as_CardTableRS(); if (ctrs == NULL) { vm_exit_during_initialization("RemSet/generation incompatibility."); } // XXXPERM return new CMSPermGen(perm_rs, init_size, ctrs, (FreeBlockDictionary::DictionaryChoice)CMSDictionaryChoice); } #endif // SERIALGC default: guarantee(false, "unrecognized GenerationName"); return NULL; }
各分代实现类的类关系如下:
归纳堆初始化的流程图如下: