源码分析:Java堆的创建

 

源码分析:Java堆的创建

虚拟机在内存中申请一片区域,由虚拟机自动管理,用来满足应用程序对象分配的空间需求,即堆空间。

 

由于程序运行的局部特性,程序创建的大多数对象都具有非常短的生命周期,而程序也会创建一些生命周期特别长的对象。简单的复制收集器无论对象的 生命周期是长是短,都会进行复制操作。而生命周期较长的对象在多次垃圾回收期间内并不会被回收,这就使得这些对象被来回复制而使得算法性能大大下降。

 

分代收集把堆分为多个子堆,分别用来存放不同寿命的对象。新生对象空间的将经历最频繁的垃圾回收,而对于经历了若干次垃圾收集后仍然存活的对象,将成长为成熟对象,并移动到成熟对象的子堆中,而对老生代子堆的垃圾回收就不会像新生对象子堆那么频繁。

 

HotSpot的堆空间分为新生代(YoungGen)和老年代(OldGen,此外还有位于非堆空间的永久代,但在Java8中将移除永久代),新生代又分为Eden区和2个Survivor区(From/To)用以进行复制收集垃圾对象。

对Java堆和对象的分析将从Java堆的创建开始,然后分析Java对象的分配与垃圾回收。

 

一、堆的实现方式

 

在虚拟机的创建初始化过程中,通过调用Universe的成员函数initialize_heap()将完成Java堆的初始化。在Universe模块下的初始化将根据虚拟机选项来选择堆的具体实现方式:

1.若虚拟机配置UseParallelGC,则Java堆的堆类型为ParallelScavengeHeap(并行收集堆)

 

 1 import java.io.IOException;
 2 
 3 import javax.servlet.ServletException;
 4 import javax.servlet.http.HttpServlet;
 5 import javax.servlet.http.HttpServletRequest;
 6 import javax.servlet.http.HttpServletResponse;
 7 
 8 public class ExampleServlet extends HttpServlet {
 9 
10     private Object thisIsNOTThreadSafe;
11 
12     protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
13 
14         Object thisIsThreadSafe;
15 
16         thisIsNOTThreadSafe = request.getParameter("foo"); // BAD!! Shared among
17                                                            // all requests!
18 
19         thisIsThreadSafe = request.getParameter("foo"); // OK, this is thread
20                                                         // safe.
21 
22     }
23 
24 }

 

 

2.若虚拟机配置UseG1GC,那么将选择堆类型为G1CollectedHeap,垃圾收集策略将使用专用的G1CollectorPolicy(垃圾优先收集)策略

 

 1  else if (UseG1GC) {
 2 
 3     #ifndef SERIALGC
 4 
 5     G1CollectorPolicy* g1p = new G1CollectorPolicy_BestRegionsFirst();
 6 
 7     G1CollectedHeap* g1h = new G1CollectedHeap(g1p);
 8 
 9     Universe::_collectedHeap = g1h;
10 
11     #else // SERIALGC
12 
13     fatal("UseG1GC not supported in java kernel vm.");
14 
15     #endif // SERIALGC
16   }

 

 

3.否则,虚拟机将使用GenCollectedHeap(分代收集堆)

 

Universe::_collectedHeap = new GenCollectedHeap(gc_policy);

 

各个堆实现类的类关系如下:

 

《源码分析:Java堆的创建》

 

对于默认情况下的堆实现,还要根据配置选择垃圾回收策略gc_policy来构造一个GenCollectedHeap,这里根据虚拟机配置选择不同的GC策略:

(1).若虚拟机配置UseSerialGC,那么将使用MarkSweepPolicy(标记-清除)策略

 

GenCollectorPolicy *gc_policy;

if (UseSerialGC) {

    gc_policy = new MarkSweepPolicy();

 }

 

 

(2).若虚拟机配置UseConcMarkSweepGC和UseAdaptiveSizePolicy,那么将使用 ASConcurrentMarkSweepPolicy(自适应并发标记-清除)策略,若没有指定UseAdaptiveSizePolicy,虚拟机 将默认使用ConcurrentMarkSweepPolicy(并发标记-清除)策略

 

 1 else if (UseConcMarkSweepGC) {
 2 
 3     #ifndef SERIALGC
 4 
 5     if (UseAdaptiveSizePolicy) {
 6 
 7     gc_policy = new ASConcurrentMarkSweepPolicy();
 8 
 9 } else {
10 
11     gc_policy = new ConcurrentMarkSweepPolicy();
12 
13 }

 

 

(3).若没有进行配置,虚拟机将默认使用MarkSweepPolicy策略

 

 else { // default old generation

    gc_policy = new MarkSweepPolicy();

 }

 

 

如下表所示:

 

《源码分析:Java堆的创建》

 

其中垃圾回收策略类的关系如下图:

 

《源码分析:Java堆的创建》

 

4.接下来是相应实现的堆的初始化

 

jint status = Universe::heap()->initialize();

if (status != JNI_OK) {

return status;

}

 

5.堆空间初始化完成后,是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)时,直接使用32位的压缩对象指针< narrow_oop >就可以找到该对象

 

若堆寻址空间大于4GB(2^32)但小于32GB时,就必须借助偏移来获得真正的地址(对象是8字节对齐的)。

 

若堆寻址空间大于32GB时,就需要借助堆的基址来完成寻址了,< narrow_oop_base >为堆的基址,< field_offset >为一页的大小。

 

(1).若heap的地址空间的最大地址大于OopEncodingHeapMax(32GB),则设置基础地址为当前堆的起始地址-页大小,设置偏移为LogMinObjAlignmentInBytes(3),即使用普通的对象指针压缩技术

 

1     if ((uint64_t)Universe::heap()->reserved_region().end() > OopEncodingHeapMax) {
2 
3     // Can't reserve heap below 32Gb.
4 
5     Universe::set_narrow_oop_base(Universe::heap()->base() - os::vm_page_size());
6 
7     Universe::set_narrow_oop_shift(LogMinObjAlignmentInBytes);
8 
9     }

 

 

(2).否则设置基础地址为0

 

1     else {
2 
3     Universe::set_narrow_oop_base(0);
4 
5     //...
6 
7     }

 

 

若heap的地址空间的最大地址大于NarrowOopHeapMax(4GB,小于32GB),则设置偏移为LogMinObjAlignmentInBytes(默认为3),即使用零基压缩技术,否则设置偏移为0,即直接使用压缩对象指针进行寻址

 

1 if((uint64_t)Universe::heap()->reserved_region().end() > NarrowOopHeapMax) {
2 
3     // Can't reserve heap below 4Gb.
4 
5     Universe::set_narrow_oop_shift(LogMinObjAlignmentInBytes);
6 
7  } else {
8 
9     Universe::set_narrow_oop_shift(0);

 

 

二、堆的初始化:分代实现方式

 

接下来分析特定堆的初始化过程,这里以GenCollectedHeap和MarkSweepPolicy为例:

 

GenCollectedHeap的构造函数中使用传入的策略作为_gen_policy(代策略)。以MarkSweepPolicy为例,看看其构造函数:

 

1  //定义在/hotspot/src/share/vm/memory/collectorPolicy.cpp中
2 
3  MarkSweepPolicy::MarkSweepPolicy() {
4 
5  initialize_all();
6 
7  }

 

 

MarkSweepPolicy的构造函数调用了initialize_all()来完成策略的初始化,initialize_all()是父 类GenCollectorPolicy()的虚函数,它调用了三个子初始化虚函数,这三个子初始化过程由GenCollectorPolicy的子类实 现。其中initialize_flags()初始化了永久代的一些大小配置参数,initialize_size_info()设置了Java堆大小的相关参数,initialize_generations()根据用户参数,配置各内存代的管理器。

 

 1     //hotspot/src/share/vm/memory/collectorPolicy.hpp中
 2 
 3     virtual void initialize_all() {
 4 
 5     initialize_flags();
 6 
 7     initialize_size_info();
 8 
 9     initialize_generations();
10 
11     }

 

 

下面通过initialize_generations()来看看各代有哪些实现方式:

 

1.若配置了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);

    }

 

 

2.默认新生代使用DefNew实现

 

 else {

     _generations[0] = new GenerationSpec(Generation::DefNew, _initial_gen0_size, _max_gen0_size);

    }

 

 

3.老年代固定使用MarkSweepCompact实现

 

_generations[1] = new GenerationSpec(Generation::MarkSweepCompact, _initial_gen1_size, _max_gen1_size);

 

(其中DefNew、ParNew、MarkSweepCompact等均为Generation的枚举集合Name的成员,描述了可能实现的各种代实现类型)

 

MarkSweepPolicy、ConcurrentMarkSweepPolicy、ASConcurrentMarkSweepPolicy对各代的实现综合如下表所示:

 

《源码分析:Java堆的创建》

 

三、堆的初始化:堆内存空间分配

 

分析完了构造函数,回到Universe模块中堆的initialize()。

以GenCollectedHeap为例:

1.根据构造函数传入的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));

    }

 

 

4.调用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);

 

 

5.初始分配所得的空间将被封装在_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());

 

 

7.调用heap_rs的的first_part(),依次为新生代和老年代分配空间并调用各代管理器的init()将其初始化为Generation空间,最后为永久代分配空间和进行初始化

 

    _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()函数中

1.在申请之前,当然要对内存空间的大小和分块数进行计算

(1).内存页的大小将根据虚拟机是否配置UseLargePages而不同,large_page_size在不同平台上表现不同,x86使用 2/4M(物理地址扩展模式)的页大小,AMD64使用2M,否则,Linux默认内存页大小只有4KB,接下来会以各代所配置的最大大小进行计算,若最 大值设置为负数,那么jvm将报错退出,默认的新生代和老年代的分块数为1,而永久代的分块数为2

 

    char* 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);

    }

    }

 

 

2.调用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);

    }

 

 

3.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;

    }

 

 

各分代实现类的类关系如下:

 

《源码分析:Java堆的创建》

 

归纳堆初始化的流程图如下:

 

 

    原文作者:kangye1014
    原文地址: https://www.cnblogs.com/kangye1014/p/4993864.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞