2024-01-25  阅读(4)
原文作者:hashcon 原文地址: https://zhanghaoxin.blog.csdn.net/article/details/113625645

9. OpenJDK HotSpot TLAB 相关源代码分析

如果这里看的比较吃力,可以直接看第 10 章,热门 Q&A,里面有很多大家常问的问题

9.1. TLAB 类构成

线程初始化的时候,如果 JVM 启用了 TLAB(默认是启用的, 可以通过 -XX:-UseTLAB 关闭),则会初始化 TLAB。

TLAB 包括如下几个 field (HeapWord* 可以理解为堆中的内存地址):
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    //静态全局变量
    static size_t   _max_size;                          // 所有 TLAB 的最大大小
      static int      _reserve_for_allocation_prefetch;   // CPU 缓存优化 Allocation Prefetch 的保留空间,这里先不用关心
      static unsigned _target_refills;                    //每个 GC 周期内期望的重填次数
    
    //以下是 TLAB 的主要构成 field
    HeapWord* _start;                              // TLAB 起始地址,表示堆内存地址都用 HeapWord* 
    HeapWord* _top;                                // 上次分配的内存地址
    HeapWord* _end;                                // TLAB 结束地址
    size_t    _desired_size;                       // TLAB 大小 包括保留空间,表示内存大小都需要通过 size_t 类型,也就是实际字节数除以 HeapWordSize 的值
    size_t    _refill_waste_limit;                 // TLAB最大浪费空间,剩余空间不足分配浪费空间限制。在TLAB剩余空间不足的时候,根据这个值决定分配策略,如果浪费空间大于这个值则直接在 Eden 区分配,如果小于这个值则将当前 TLAB 放回 Eden 区管理并从 Eden 申请新的 TLAB 进行分配。 
    AdaptiveWeightedAverage _allocation_fraction;  // 当前 TLAB 分配比例 EMA
    
    //以下是我们这里不用太关心的 field
    HeapWord* _allocation_end;                    // TLAB 真正可以用来分配内存的结束地址,这个是 _end 结束地址排除保留空间(预留给 dummy object 的对象头空间)
    HeapWord* _pf_top;                            // Allocation Prefetch CPU 缓存优化机制相关需要的参数,这里先不用考虑
    size_t    _allocated_before_last_gc;          // 这个用于计算 图10 中的线程本轮 GC 分配空间的大小,记录上次 GC 时,线程分配的空间大小
    unsigned  _number_of_refills;                 // 线程分配内存数据采集相关,TLAB 剩余空间不足分配次数
    unsigned  _fast_refill_waste;                 // 线程分配内存数据采集相关,TLAB 快速分配浪费,快速分配就是直接在 TLAB 分配,这个在现在 JVM 中已经用不到了
    unsigned  _slow_refill_waste;                 // 线程分配内存数据采集相关,TLAB 慢速分配浪费,慢速分配就是重填一个 TLAB 分配
    unsigned  _gc_waste;                          // 线程分配内存数据采集相关,gc浪费
    unsigned  _slow_allocations;                  // 线程分配内存数据采集相关,TLAB 慢速分配计数 
    size_t    _allocated_size;                    // 分配的内存大小
    size_t    _bytes_since_last_sample_point;     // JVM TI 采集指标相关 field,这里不用关心

9.2. TLAB 初始化

首先是 JVM 启动的时候,全局 TLAB 需要初始化:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    void ThreadLocalAllocBuffer::startup_initialization() {
      //初始化,也就是归零统计数据
      ThreadLocalAllocStats::initialize();
    
      // 假设平均下来,GC 扫描的时候,每个线程当前的 TLAB 都有一半的内存被浪费,这个每个线程使用内存的浪费的百分比率(也就是 TLABWasteTargetPercent),也就是等于(注意,仅最新的那个 TLAB 有浪费,之前 refill 退回的假设是没有浪费的):1/2 * (每个 epoch 内每个线程期望 refill 次数) * 100
      //那么每个 epoch 内每个线程 refill 次数配置就等于 50 / TLABWasteTargetPercent, 默认也就是 50 次。
      _target_refills = 100 / (2 * TLABWasteTargetPercent);
      // 但是初始的 _target_refills 需要设置最多不超过 2 次来减少 VM 初始化时候 GC 的可能性
      _target_refills = MAX2(_target_refills, 2U);
    
    //如果 C2 JIT 编译存在并启用,则保留 CPU 缓存优化 Allocation Prefetch 空间,这个这里先不用关心,会在别的章节讲述
    #ifdef COMPILER2
      if (is_server_compilation_mode_vm()) {
        int lines =  MAX2(AllocatePrefetchLines, AllocateInstancePrefetchLines) + 2;
        _reserve_for_allocation_prefetch = (AllocatePrefetchDistance + AllocatePrefetchStepSize * lines) /
                                           (int)HeapWordSize;
      }
    #endif
    
      // 初始化 main 线程的 TLAB
      guarantee(Thread::current()->is_Java_thread(), "tlab initialization thread not Java thread");
      Thread::current()->tlab().initialize();
      log_develop_trace(gc, tlab)("TLAB min: " SIZE_FORMAT " initial: " SIZE_FORMAT " max: " SIZE_FORMAT,
                                   min_size(), Thread::current()->tlab().initial_desired_size(), max_size());
    }

每个线程维护自己的 TLAB,同时每个线程的 TLAB 大小不一。TLAB 的大小主要由 Eden 的大小,线程数量,还有线程的对象分配速率决定。
在 Java 线程开始运行时,会先分配 TLAB:
src/hotspot/share/runtime/thread.cpp

    void JavaThread::run() {
      // initialize thread-local alloc buffer related fields
      this->initialize_tlab();
      //剩余代码忽略
    }

分配 TLAB 其实就是调用 ThreadLocalAllocBuffer 的 initialize 方法。
src/hotspot/share/runtime/thread.hpp

    void initialize_tlab() {
        //如果没有通过 -XX:-UseTLAB 禁用 TLAB,则初始化TLAB
        if (UseTLAB) {
          tlab().initialize();
        }
    }
    
    // Thread-Local Allocation Buffer (TLAB) support
    ThreadLocalAllocBuffer& tlab()                 {
      return _tlab; 
    }
    
    ThreadLocalAllocBuffer _tlab;

ThreadLocalAllocBuffer 的 initialize 方法初始化 TLAB 的上面提到的我们要关心的各种 field:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    void ThreadLocalAllocBuffer::initialize() {
      //设置初始指针,由于还没有从 Eden 分配内存,所以这里都设置为 NULL
      initialize(NULL,                    // start
                 NULL,                    // top
                 NULL);                   // end
      //计算初始期望大小,并设置
      set_desired_size(initial_desired_size());
      //所有 TLAB 总大小,不同的 GC 实现有不同的 TLAB 容量, 一般是 Eden 区大小
      //例如 G1 GC,就是等于 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes,可以理解为年轻代减去Survivor区,也就是Eden区
      size_t capacity = Universe::heap()->tlab_capacity(thread()) / HeapWordSize;
      //计算这个线程的 TLAB 期望占用所有 TLAB 总体大小比例
      //TLAB 期望占用大小也就是这个 TLAB 大小乘以期望 refill 的次数
      float alloc_frac = desired_size() * target_refills() / (float) capacity;
      //记录下来,用于计算 EMA
      _allocation_fraction.sample(alloc_frac);
      //计算初始 refill 最大浪费空间,并设置
      //如前面原理部分所述,初始大小就是 TLAB 的大小(_desired_size) / TLABRefillWasteFraction
      set_refill_waste_limit(initial_refill_waste_limit());
      //重置统计
      reset_statistics();
    }

9.2.1. 初始期望大小是如何计算的呢?

src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    //计算初始大小
    size_t ThreadLocalAllocBuffer::initial_desired_size() {
      size_t init_sz = 0;
      //如果通过 -XX:TLABSize 设置了 TLAB 大小,则用这个值作为初始期望大小
      //表示堆内存占用大小都需要用占用几个 HeapWord 表示,所以用TLABSize / HeapWordSize
      if (TLABSize > 0) {
        init_sz = TLABSize / HeapWordSize;
      } else {
        //获取当前epoch内线程数量期望,这个如之前所述通过 EMA 预测
        unsigned int nof_threads = ThreadLocalAllocStats::allocating_threads_avg();
        //不同的 GC 实现有不同的 TLAB 容量,Universe::heap()->tlab_capacity(thread()) 一般是 Eden 区大小
        //例如 G1 GC,就是等于 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes,可以理解为年轻代减去Survivor区,也就是Eden区
        //整体大小等于 Eden区大小/(当前 epcoh 内会分配对象期望线程个数 * 每个 epoch 内每个线程 refill 次数配置)
        //target_refills已经在 JVM 初始化所有 TLAB 全局配置的时候初始化好了
        init_sz  = (Universe::heap()->tlab_capacity(thread()) / HeapWordSize) /
                          (nof_threads * target_refills());
        //考虑对象对齐,得出最后的大小
        init_sz = align_object_size(init_sz);
      }
      //保持大小在  min_size() 还有 max_size() 之间
      //min_size主要由 MinTLABSize 决定
      init_sz = MIN2(MAX2(init_sz, min_size()), max_size());
      return init_sz;
    }
    
    //最小大小由 MinTLABSize 决定,需要表示为 HeapWordSize,并且考虑对象对齐,最后的 alignment_reserve 是 dummy object 填充的对象头大小(这里先不考虑 JVM 的 CPU 缓存 prematch,我们会在其他章节详细分析)。
    static size_t min_size()                       { 
        return align_object_size(MinTLABSize / HeapWordSize) + alignment_reserve(); 
    }

9.2.2. TLAB 最大大小是怎样决定的呢?

不同的 GC 方式,有不同的方式:

G1 GC 中为大对象(humongous object)大小 ,也就是 G1 region 大小的一半:src/hotspot/share/gc/g1/g1CollectedHeap.cpp

    // For G1 TLABs should not contain humongous objects, so the maximum TLAB size
    // must be equal to the humongous object limit.
    size_t G1CollectedHeap::max_tlab_size() const {
      return align_down(_humongous_object_threshold_in_words, MinObjAlignment);
    }

ZGC 中为页大小的 8 分之一 ,类似的在大部分情况下 S henandoah GC 也是每个 Region 大小的 8 分之一 。他们都是期望至少有 8 分之 7 的区域是不用退回的减少选择 Cset 的时候的扫描复杂度:
src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp

    MaxTLABSizeWords = MIN2(ShenandoahElasticTLAB ? RegionSizeWords : (RegionSizeWords / 8), HumongousThresholdWords);

src/hotspot/share/gc/z/zHeap.cpp

    const size_t      ZObjectSizeLimitSmall         = ZPageSizeSmall / 8;

对于 其他的 GC,则是 int 数组的最大大小 ,这个和为了填充 dummy object 表示 TLAB 的空区域有关。这个原因之前已经说明了。

9.3. TLAB 分配内存

当 new 一个对象时,需要调用instanceOop InstanceKlass::allocate_instance(TRAPS)
src/hotspot/share/oops/instanceKlass.cpp

    instanceOop InstanceKlass::allocate_instance(TRAPS) {
      bool has_finalizer_flag = has_finalizer(); // Query before possible GC
      int size = size_helper();  // Query before forming handle.
    
      instanceOop i;
    
      i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL);
      if (has_finalizer_flag && !RegisterFinalizersAtInit) {
        i = register_finalizer(i, CHECK_NULL);
      }
      return i;
    }

其核心就是heap()->obj_allocate(this, size, CHECK_NULL)从堆上面分配内存:
src/hotspot/share/gc/shared/collectedHeap.inline.hpp

    inline oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) {
      ObjAllocator allocator(klass, size, THREAD);
      return allocator.allocate();
    }

使用全局的 ObjAllocator 实现进行对象内存分配:
src/hotspot/share/gc/shared/memAllocator.cpp

    oop MemAllocator::allocate() const {
      oop obj = NULL;
      {
        Allocation allocation(*this, &obj);
        //分配堆内存,继续看下面一个方法
        HeapWord* mem = mem_allocate(allocation);
        if (mem != NULL) {
          obj = initialize(mem);
        } else {
          // The unhandled oop detector will poison local variable obj,
          // so reset it to NULL if mem is NULL.
          obj = NULL;
        }
      }
      return obj;
    }
    HeapWord* MemAllocator::mem_allocate(Allocation& allocation) const {
      //如果使用了 TLAB,则从 TLAB 分配,分配代码继续看下面一个方法
      if (UseTLAB) {
        HeapWord* result = allocate_inside_tlab(allocation);
        if (result != NULL) {
          return result;
        }
      }
      //否则直接从 tlab 外分配
      return allocate_outside_tlab(allocation);
    }
    HeapWord* MemAllocator::allocate_inside_tlab(Allocation& allocation) const {
      assert(UseTLAB, "should use UseTLAB");
    
      //从当前线程的 TLAB 分配内存,TLAB 快分配
      HeapWord* mem = _thread->tlab().allocate(_word_size);
      //如果没有分配失败则返回
      if (mem != NULL) {
        return mem;
      }
    
      //如果分配失败则走 TLAB 慢分配,需要 refill 或者直接从 Eden 分配
      return allocate_inside_tlab_slow(allocation);
    }

9.3.1. TLAB 快分配

src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp

    inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) {
      //验证各个内存指针有效,也就是 _top 在 _start 和 _end 范围内
      invariants();
      HeapWord* obj = top();
      //如果空间足够,则分配内存
      if (pointer_delta(end(), obj) >= size) {
        set_top(obj + size);
        invariants();
        return obj;
      }
      return NULL;
    }

9.3.2. TLAB 慢分配

src/hotspot/share/gc/shared/memAllocator.cpp

    HeapWord* MemAllocator::allocate_inside_tlab_slow(Allocation& allocation) const {
      HeapWord* mem = NULL;
      ThreadLocalAllocBuffer& tlab = _thread->tlab();
    
      // 如果 TLAB 剩余空间大于 最大浪费空间,则记录并让最大浪费空间递增
      if (tlab.free() > tlab.refill_waste_limit()) {
        tlab.record_slow_allocation(_word_size);
        return NULL;
      }
    
      //重新计算 TLAB 大小
      size_t new_tlab_size = tlab.compute_size(_word_size);
      //TLAB 放回 Eden 区
      tlab.retire_before_allocation();
      
      if (new_tlab_size == 0) {
        return NULL;
      }
    
      // 计算最小大小
      size_t min_tlab_size = ThreadLocalAllocBuffer::compute_min_size(_word_size);
      //分配新的 TLAB 空间,并在里面分配对象
      mem = Universe::heap()->allocate_new_tlab(min_tlab_size, new_tlab_size, &allocation._allocated_tlab_size);
      if (mem == NULL) {
        assert(allocation._allocated_tlab_size == 0,
               "Allocation failed, but actual size was updated. min: " SIZE_FORMAT
               ", desired: " SIZE_FORMAT ", actual: " SIZE_FORMAT,
               min_tlab_size, new_tlab_size, allocation._allocated_tlab_size);
        return NULL;
      }
      assert(allocation._allocated_tlab_size != 0, "Allocation succeeded but actual size not updated. mem at: "
             PTR_FORMAT " min: " SIZE_FORMAT ", desired: " SIZE_FORMAT,
             p2i(mem), min_tlab_size, new_tlab_size);
      //如果启用了 ZeroTLAB 这个 JVM 参数,则将对象所有字段置零值
      if (ZeroTLAB) {
        // ..and clear it.
        Copy::zero_to_words(mem, allocation._allocated_tlab_size);
      } else {
        // ...and zap just allocated object.
      }
    
      //设置新的 TLAB 空间为当前线程的 TLAB
      tlab.fill(mem, mem + _word_size, allocation._allocated_tlab_size);
      //返回分配的对象内存地址
      return mem;
    }
9.3.2.1 TLAB最大浪费空间

TLAB最大浪费空间 _refill_waste_limit 初始值为 TLAB 大小除以 TLABRefillWasteFraction:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.hpp

    size_t initial_refill_waste_limit()            { return desired_size() / TLABRefillWasteFraction; }

每次慢分配,调用record_slow_allocation(size_t obj_size)记录慢分配的同时,增加 TLAB 最大浪费空间的大小:

src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    void ThreadLocalAllocBuffer::record_slow_allocation(size_t obj_size) {
      //每次慢分配,_refill_waste_limit 增加 refill_waste_limit_increment,也就是 TLABWasteIncrement
      set_refill_waste_limit(refill_waste_limit() + refill_waste_limit_increment());
      _slow_allocations++;
      log_develop_trace(gc, tlab)("TLAB: %s thread: " INTPTR_FORMAT " [id: %2d]"
                                  " obj: " SIZE_FORMAT
                                  " free: " SIZE_FORMAT
                                  " waste: " SIZE_FORMAT,
                                  "slow", p2i(thread()), thread()->osthread()->thread_id(),
                                  obj_size, free(), refill_waste_limit());
    }
    //refill_waste_limit_increment 就是 JVM 参数 TLABWasteIncrement
    static size_t refill_waste_limit_increment()   { return TLABWasteIncrement; }
9.3.2.2. 重新计算 TLAB 大小

重新计算会取 当前堆剩余给 TLAB 可分配的空间 和 TLAB 期望大小 + 当前需要分配的空间大小 中的小的那个:

src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp

    inline size_t ThreadLocalAllocBuffer::compute_size(size_t obj_size) {
      //获取当前堆剩余给 TLAB 可分配的空间
      const size_t available_size = Universe::heap()->unsafe_max_tlab_alloc(thread()) / HeapWordSize;
      //取 TLAB 可分配的空间 和 TLAB 期望大小 + 当前需要分配的空间大小 以及 TLAB 最大大小中的小的那个
      size_t new_tlab_size = MIN3(available_size, desired_size() + align_object_size(obj_size), max_size());
    
      // 确保大小大于 dummy obj 对象头
      if (new_tlab_size < compute_min_size(obj_size)) {
        log_trace(gc, tlab)("ThreadLocalAllocBuffer::compute_size(" SIZE_FORMAT ") returns failure",
                            obj_size);
        return 0;
      }
      log_trace(gc, tlab)("ThreadLocalAllocBuffer::compute_size(" SIZE_FORMAT ") returns " SIZE_FORMAT,
                          obj_size, new_tlab_size);
      return new_tlab_size;
    }
9.3.2.3. 当前 TLAB 放回堆

src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    //在TLAB慢分配被调用,当前 TLAB 放回堆
    void ThreadLocalAllocBuffer::retire_before_allocation() {
      //将当前 TLAB 剩余空间大小加入慢分配浪费空间大小
      _slow_refill_waste += (unsigned int)remaining();
      //执行 TLAB 退还给堆,这个在后面 GC 的时候还会被调用用于将所有的线程的 TLAB 退回堆
      retire();
    }
    
    //对于 TLAB 慢分配,stats 为空
    //对于 GC 的时候调用,stats 用于记录每个线程的数据
    void ThreadLocalAllocBuffer::retire(ThreadLocalAllocStats* stats) {
      
      if (stats != NULL) {
        accumulate_and_reset_statistics(stats);
      }
      //如果当前 TLAB 有效
      if (end() != NULL) {
        invariants();
        //将用了的空间记录如线程分配对象大小记录
        thread()->incr_allocated_bytes(used_bytes());
        //填充dummy object
        insert_filler();
        //清空当前 TLAB 指针
        initialize(NULL, NULL, NULL);
      }
    }

9.4. GC 相关 TLAB 操作

9.4.1. GC 前

不同的 GC 可能实现不一样,但是 TLAB 操作的时机是基本一样的,这里以 G1 GC 为例,在真正 GC 前:

src/hotspot/share/gc/g1/g1CollectedHeap.cpp

    void G1CollectedHeap::gc_prologue(bool full) {
      //省略其他代码
    
      // Fill TLAB's and such
      {
        Ticks start = Ticks::now();
        //确保堆内存是可以解析的
        ensure_parsability(true);
        Tickspan dt = Ticks::now() - start;
        phase_times()->record_prepare_tlab_time_ms(dt.seconds() * MILLIUNITS);
      }
      //省略其他代码
    }

为何要确保堆内存是可以解析的呢?这样有利于更快速的扫描堆上对象。确保内存可以解析里面做了什么呢?其实主要就是退还每个线程的 TLAB 以及填充 dummy object。

src/hotspot/share/gc/g1/g1CollectedHeap.cpp

    void CollectedHeap::ensure_parsability(bool retire_tlabs) {
      //真正的 GC 肯定发生在安全点上,这个在后面安全点章节会详细说明
      assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(),
             "Should only be called at a safepoint or at start-up");
    
      ThreadLocalAllocStats stats;
      for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next();) {
        BarrierSet::barrier_set()->make_parsable(thread);
        //如果全局启用了 TLAB
        if (UseTLAB) {
          //如果指定要回收,则回收 TLAB
          if (retire_tlabs) {
            //回收 TLAB,调用  9.3.2.3. 当前 TLAB 放回堆 提到的 retire 方法
            thread->tlab().retire(&stats);
          } else {
            //当前如果不回收,则将 TLAB 填充 Dummy Object 利于解析
            thread->tlab().make_parsable();
          }
        }
      }
    
      stats.publish();
    }

9.4.2. GC 后

不同的 GC 可能实现不一样,但是 TLAB 操作的时机是基本一样的,这里以 G1 GC 为例,在 GC 后:

src/hotspot/share/gc/g1/g1CollectedHeap.cpp
_desired_size是什么时候变得呢?怎么变得呢?

    void G1CollectedHeap::gc_epilogue(bool full) {
        //省略其他代码
        resize_all_tlabs();
    }

src/hotspot/share/gc/shared/collectedHeap.cpp

    void CollectedHeap::resize_all_tlabs() {
      //需要在安全点,GC 会处于安全点的
      assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(),
             "Should only resize tlabs at safepoint");
      //如果 UseTLAB 和 ResizeTLAB 都是打开的(默认就是打开的)
      if (UseTLAB && ResizeTLAB) {
        for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next(); ) {
          //重新计算每个线程 TLAB 期望大小
          thread->tlab().resize();
        }
      }
    }

重新计算每个线程 TLAB 期望大小:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    void ThreadLocalAllocBuffer::resize() {
      assert(ResizeTLAB, "Should not call this otherwise");
      //根据 _allocation_fraction 这个 EMA 采集得出平均数乘以Eden区大小,得出 TLAB 当前预测占用内存比例
      size_t alloc = (size_t)(_allocation_fraction.average() *
                              (Universe::heap()->tlab_capacity(thread()) / HeapWordSize));
      //除以目标 refill 次数就是新的 TLAB 大小,和初始化时候的计算方法差不多
      size_t new_size = alloc / _target_refills;
      //保证在 min_size 还有 max_size 之间
      new_size = clamp(new_size, min_size(), max_size());
    
      size_t aligned_new_size = align_object_size(new_size);
    
      log_trace(gc, tlab)("TLAB new size: thread: " INTPTR_FORMAT " [id: %2d]"
                          " refills %d  alloc: %8.6f desired_size: " SIZE_FORMAT " -> " SIZE_FORMAT,
                          p2i(thread()), thread()->osthread()->thread_id(),
                          _target_refills, _allocation_fraction.average(), desired_size(), aligned_new_size);
      //设置新的 TLAB 大小
      set_desired_size(aligned_new_size);
      //重置 TLAB 最大浪费空间
      set_refill_waste_limit(initial_refill_waste_limit());
    }

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文