为了降低启动代码的复杂性,进入linux内核的时候,MMU使关闭的。如果关闭MMU,意味着不能利用高速缓存的性能,那么内核使如何要打开MMU并使能数据高速缓存呢?
-
在关闭MMU的情况下,处理器访问的地址都是物理地址。当MMU打开后,处理器访问地址就变成虚拟地址
-
目前的处理器都是多级流水行架构,处理器会提前预取多条指令到流水线中。当打开MMU后,处理器之前预取的指令就会以虚拟地址来访问,到MMU查找对应的物理地址。
因此,为了保证处理器在开启MMU后,能完成从物理地址到虚拟地址的平滑过渡,首先会创建VA和PA的相等映射,也就是恒等映射(identity mapping)。 建立恒等映射是小范围的,占用的空间通常是内核映像的大小,也就是几兆字节。
注:文章代码分析基于linux-4.9,架构基于aarch64(ARM64)。涉及页表代码分析部分,假设页表映射层级是4,即配置CONFIG_ARM64_PGTABLE_LEVELS=4。地址宽度是48,即配置CONFIG_ARM64_VA_BITS=48。
/*
* Setup the initial page tables. We only setup the barest amount which is
* required to get the kernel running. The following sections are required:
* - identity mapping to enable the MMU (low address, TTBR0)
* - first few MB of the kernel linear mapping to jump to once the MMU has
* been enabled
*/
__create_page_tables:
mov x28, lr #把LR的值存放到X28
这里会建立两种section,分别完成identity mapping和kernel image mapping
/*
* Invalidate the idmap and swapper page tables to avoid potential
* dirty cache lines being evicted.
*/
adrp x0, idmap_pg_dir #加载idmap_pg_dir到x0
adrp x1, swapper_pg_dir + SWAPPER_DIR_SIZE #加载swapper_pg_dir到x1
bl __inval_cache_range #使页表对应的高速缓存无效,为后面建立内核空间页表映射
这个主要是将identity mapping和kernel image mapping空间的页表对应的高速缓存无效掉,其主要的实现在arch/arm64/kernel/vmlinux.lds.S链接文件中
#arch/arm64/kernel/vmlinux.lds.S
. = ALIGN(PAGE_SIZE);
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
swapper_pg_dir = .;
. += SWAPPER_DIR_SIZE;
调用__inval_cache_range函数来使idmap_pg_dir和swapper_pg_dir + SWAPPER_DIR_SIZE区间内页表i对应的高速缓存无效i,也就使先清空idmap_pg_dir对应大小为IDMAP_DIR_SIZE和swapper_pg_dir对应大小为SWAPPER_DIR_SIZE页表的高速缓存。
- idmap_pg_dir是identity mapping使用的页表,也就是物理地址和虚拟地址是相等的,主要是解决打开MMU后,从物理地址转换成虚拟地址,防止MMU开启后,无法获取页表
- swapper_pg_dir是kernel image mapping初始阶段使用的页表,
swapper_pg_dir
Linux内核编译后,kernel image是需要进行映射的,包括text,data
等各种。请注意,这里的内存是一段连续内存。也就是说页表(PGD/PUD/PMD)都是连在一起的,地址相差PAGE_SIZE(4k)
/*
* Clear the idmap and swapper page tables.
*/
adrp x0, idmap_pg_dir
adrp x6, swapper_pg_dir + SWAPPER_DIR_SIZE
1: stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
cmp x0, x6
b.lo 1b
这段话的含义很明白了,将idmap_pg_dir赋值给x0,将swapper_pg_dir + SWAPPER_DIR_SIZE赋值给x6,然后将这段空间的页表的内容设置为0。
mov x7, SWAPPER_MM_MMUFLAGS
SWAPPER_MM_MMUFLAGS宏描述了段映射的属性,它实现在arch/arm64/include/asm/kernel-pgtable.h头文件中
#arch/arm64/include/asm/kernel-pgtable.h
#define SWAPPER_MM_MMUFLAGS (PMD_ATTRINDX(MT_NORMAL) | SWAPPER_PMD_FLAGS)
#define SWAPPER_PMD_FLAGS (PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S)
其中定义了内存属性为普通内存MT_NORMAL,其属性含义如下:
-
PMD_TYPE_SECT表示一个块映射,
-
PMD_SECT_AF设置为块映射的访问权限,该Bit用来表示该页表是否第一次是使用,当程序访问对应的page或者section的时候,就会使用该entry,如果从来没有被访问过,那么其值等于0,否者等于1)。该bit主要被操作系统用来跟踪一个page是否被使用过(最近是否被访问),当该page首次被创建的时候,AF等于0,当代码第一次访问该page的时候,会产生MMU fault,这时候,异常处理函数应该设定AF等于1,从而阻止下一次访问该page的时候产生MMU Fault。
-
PMD_SECT_S表示块映射的共享属性
| SH[1:0]|Normalmemory| | :-----: | :-----: | | 00 | Non-shareable | | 01 | 无效 | | 10 | outershareable | | 11 | innershareble |
/*
* Create the identity mapping.
*/
adrp x0, idmap_pg_dir -----(1)
adrp x3, __idmap_text_start -----(2)
#ifndef CONFIG_ARM64_VA_BITS_48 -----(3)
#define EXTRA_SHIFT (PGDIR_SHIFT + PAGE_SHIFT - 3) -----(4)
#define EXTRA_PTRS (1 << (48 - EXTRA_SHIFT)) -----(5)
/*
* If VA_BITS < 48, it may be too small to allow for an ID mapping to be
* created that covers system RAM if that is located sufficiently high
* in the physical address space. So for the ID map, use an extended
* virtual range in that case, by configuring an additional translation
* level.
* First, we have to verify our assumption that the current value of
* VA_BITS was chosen such that all translation levels are fully
* utilised, and that lowering T0SZ will always result in an additional
* translation level to be configured.
*/
#if VA_BITS != EXTRA_SHIFT -----(6)
#error "Mismatch between VA_BITS and page size/number of translation levels"
#endif
/*
* Calculate the maximum allowed value for TCR_EL1.T0SZ so that the
* entire ID map region can be mapped. As T0SZ == (64 - #bits used),
* this number conveniently equals the number of leading zeroes in
* the physical address of __idmap_text_end.
*/
adrp x5, __idmap_text_end -----(7)
clz x5, x5
cmp x5, TCR_T0SZ(VA_BITS) // default T0SZ small enough? -----(8)
b.ge 1f // .. then skip additional level
adr_l x6, idmap_t0sz -----(9)
str x5, [x6]
dmb sy
dc ivac, x6 // Invalidate potentially stale cache line
create_table_entry x0, x3, EXTRA_SHIFT, EXTRA_PTRS, x5, x6 -----(10)
1:
#endif
create_pgd_entry x0, x3, x5, x6 -----(11)
mov x5, x3 // __pa(__idmap_text_start)
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
create_block_map x0, x7, x3, x5, x6 -----(12)
-
- 将加载idmap_pg_dir的物理地址到x0寄存器,idmap_pg_dir使恒等映射的页表,其定义在vmlinux.lds.S链接文件中定义了
. = ALIGN(PAGE_SIZE);
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
swapper_pg_dir = .;
这里分配给idmap_pg_dir的u页面大小为IDMAP_DIR_SIZE,而IDMAP_DIR_SIZE实现在arch/arm64/include/asm/kernel-pgtable.h头文件中,通常大小使3个连续的4K页面。
#define IDMAP_DIR_SIZE (IDMAP_PGTABLE_LEVELS * PAGE_SIZE)
#define IDMAP_PGTABLE_LEVELS (ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT) - 1)
#define PHYS_MASK_SHIFT (CONFIG_ARM64_PA_BITS)
这里的CONFIG_ARM64_PA_BITS配置的是48. 这里的含义是,计算采用section mapping的话,需要几个页来存放table。ARM64_HW_PGTABLE_LEVELS,很关键,根据配置的物理地址线的宽度计算需要的页面数,
#define ARM64_HW_PGTABLE_LEVELS(va_bits) (((va_bits) - 4) / (PAGE_SHIFT - 3))
完成的公司转换成为
((((va_bits) - PAGE_SHIFT) + (PAGE_SHIFT - 3) - 1) / (PAGE_SHIFT - 3))
结合vmlinux.lds,上面的公式就是: ((48-12)+(12-3)-1) / (12-3) = (36+9-1)/9 = 44/9 = 4,最终IDMAP_DIR_SIZE为3个页面,即PGD/PUD/PMD页表,每一级页表占据一个页面。
-
- 主要是将__idmap_text_start放到x3中,idmap是kernel image中的一个段,其位于 arch/arm64/kernel/vmlinux.lds.S中,定义如下,即定义了一个以__idmap_text_start开始,__idmap_text_end结束的段,该段会被放在vmlinux的代码段中。
. = ALIGN(SZ_4K); \
VMLINUX_SYMBOL(__idmap_text_start) = .; \
*(.idmap.text) \
VMLINUX_SYMBOL(__idmap_text_end) = .;
除了开机启动时,打开MMU外,内核里还有很对场景需要恒等映射的,如唤醒处理其的函数cpu_do_resume
-
- 如果没有定义CONFIG_ARM64_VA_BITS_48,表示虚拟地址的宽度配置项,就需要使用create_table_entry。主要是解决要标识映射的物理地址超出VA_BITS覆盖范围的问题。此时基本上创建identity mapping是没有什么大问题的,但是,如果物理内存的地址位于非常高的位置,那么在进行identity mapping就有问题了,因为有可能你配置的VA_BITS不够大,超出了虚拟地址的范围。这时候,就需要扩展virtual address range了。当然,如果配置了48bits的VA_BITS就不存在这样的问题了,因为ARMv8最大支持的VA BITS就是48个,根本不可能扩展了。
-
- 在虚拟地址地址不是48 bit,而系统内存的物理地址又放到了非常非常高的位置,这时候,为了完成identity mapping,我们必须要扩展虚拟地址,那么扩展多少呢?扩展到48个bit。扩展之后,增加了一个EXTRA的level,地址映射关系是EXTRA—>PGD—>……,其中EXTRA_SHIFT等于(PGDIR_SHIFT + PAGE_SHIFT - 3)。
-
- 扩展之后,地址映射多个一个level,我们称之EXTRA level,该level的Translation table中有多少个entry呢?EXTRA_PTRS给出了答案。
-
- linux kernel中,对地址映射是有要求的,即要求PGD是满的。例如:48 bit的虚拟地址,4k的page size,对应的映射关系是PGD(9-bit)+PUD(9-bit)+PMD(9-bit)+PTE(9-bit)+page offset(12-bit),对于42bit的虚拟地址,64k的page size,对应的映射关系是PGD(13-bit)+ PTE(13-bit)+ page offset(16-bit)。这两种例子有一个共同的特点就是PGD中的entry数目都是满的,也就是说索引到PGD的bit数目都是PAGE_SIZE-3。如果不满足这个关系,linux kernel会认为你的配置是有问题的。注意:这是内核的要求,实际上ARM64的硬件没有这么要求。
正因为正确的配置下,PGD都是满的,因此扩展之后EXTRA_SHIFT一定是等于VA_BITS的,否则一定是你的配置有问题。我们延续上一个实例来说明如何扩展虚拟地址的bit数目。对于42bit的虚拟地址,64k的page size,扩展之后,虚拟地址是48个bit,地址映射关系是EXTRA(6-bit)+ PGD(13-bit)+ PTE(13-bit)+ page offset(16-bit)。
-
- x5保存了__idmap_text_end的物理地址,之所以这么做是因为需要确定identity mapping的最高的物理地址,计算该物理地址的前导0有多少个,从而可以判断该地址是否是位于物理地址空间中比较高的位置。零计数指令CLZ,指令用于计算最高符号位与第一个1之间的0的个数
-
- 宏定义TCR_T0SZ可以计算给定虚拟地址数目下,前导0的个数。如果虚拟地址是48的话,那么前导0是16个。如果当前物理地址的前导0的个数(x5的值)还有小于当前配置虚拟地址的前导0的个数,那么就需要扩展。比较__idmap_text_end是否超过了VM_BITS所能达到的地址范围,如果没有,则跳转到标签1出,即11处,如果有,则跳转到9
-
- 创建extra translation table的entry。具体传递的参数如下
- x0:页表地址idmap_pg_dir
- x3:准备映射的虚拟地址(虽然x3保存的是物理地址,但是identity mapping嘛,VA和PA都是一样的)
- EXTRA_SHIFT:正常建立最高level mapping的时候, shift是PGDIR_SHIFT,但是,由于物理地址位置太高,需要额外的映射,因此这里需要再加上一个level的mapping,因此shift需要PGDIR_SHIFT + (PAGE_SHIFT - 3)。
- EXTRA_PTRS:增加了一个level的Translation table,我们需要确定这个增加level的Translation table中包含的描述符的数目,EXTRA_PTRS给出了这个参数。
-
- create_pgd_entry,建立各个中间level的table描述符,后面详细介绍,此外还将__idmap_text_start放到x5寄存器中,__idmap_text_end放到x6寄存器中
-
- 创建最后一个level translation table的entry。该entry可能是page descriptor,也可能是block descriptor,具体传递的参数如下
- x0:指向最后一个level的translation table
- x7:要创建映射的memory attribute
- x3:物理地址
- x5:虚拟地址的起始地址(其实和x3一样)
- x6:虚拟地址的结束地址
1. create_table_entry
这个宏定义主要是用来创建一个中间level的translation table中的描述符。如果用linux的术语,就是创建PGD、PUD或者PMD的描述符。如果用ARM64术语,就是创建L0、L1或者L2的描述符。具体创建哪一个level的Translation table descriptor是由tbl参数指定的,tbl指向了该translation table的内存。
.macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
lsr \tmp1, \virt, #\shift
and \tmp1, \tmp1, #\ptrs - 1 // table index
add \tmp2, \tbl, #PAGE_SIZE
orr \tmp2, \tmp2, #PMD_TYPE_TABLE // address of next table and entry type
str \tmp2, [\tbl, \tmp1, lsl #3]
add \tbl, \tbl, #PAGE_SIZE // next level table page
.endm
create_table_entry宏自带6个参数,部分参数说明如下
- tbl:指向了该translation table的内存,决定了具体创建哪一个level的Translation table descriptor
- virt:要创建地址映射的那个虚拟地址
- shift表示这一级页表的在虚拟地址中的偏移
- ptr表示这一级页表是几位的
- tmp1和tmp2是临时变量
其处理流程如下:
-
tmp1中保存virt地址对应在Translation table中的entry index
-
初始阶段的页表(PGD/PUD/PMD/PTE)都是排列在一起的,每一个占用一个page。也就是说,如果create_table_entry当前操作的是PGD,那么tmp2这时候保存了下一个level的页表,也就是PUD了。
-
光有下一级translation table的地址不行,还要告知该描述符是否有效(bit 0),该描述符的类型是哪一种类型(bit 1)。对于中间level的页表,该描述符不可能是block entry,只能是table type的描述符,因此该描述符的最低两位是0b11。
#define PMD_TYPE_TABLE (_AT(pmdval_t, 3) << 0)
-
把页表项内容放到指定的页表项当中,之所以有“lsl #3”操作,是因为一个描述符占据8个Byte
-
结束的时候tbl会加上一个PAGE_SIZE,也就是tbl变成了下一级页表的地址
2. create_pgd_entry
这个宏的作用并不仅仅是创建pgd,实际上该函数不仅仅创建PGD中的描述符,如果需要下一级的translation table,例如PUD、PMD,也需要同时建立,最终的要求是能够完成所有中间level的translation table的建立(其实每个table中都是只建立了一个描述符),仅仅留下PTE,由其他代码来完成。
.macro create_pgd_entry, tbl, virt, tmp1, tmp2
create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2
#if SWAPPER_PGTABLE_LEVELS > 3
create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2
#endif
#if SWAPPER_PGTABLE_LEVELS > 2
create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
#endif
.endm
create_pgd_entry宏自带4个参数,部分参数说明如下
- tbl:tbl是pgd translation table的地址
- virt: 具体要创建哪一个地址的描述符由virt指定
- tmp1和tmp2是临时变量
其处理流程如下:
- 这里通过create_table_entry调用该函数在PGD中为虚拟地址virt创建一个table type的描述符。
- SWAPPER_PGTABLE_LEVELS其实定义了swapper进程地址空间的页表的级数,可能3,也可能是2,具体中间的Translation table有多少个level是和配置相关的,如果是section mapping,那么中间level包括PGD和PUD就OK了,PMD是最后一个level。如果是page mapping,那么需要PGD、PUD和PMD这三个中间level,PTE是最后一个level。当然,如果整个page level是3或者2的时候,也有可能不存在PUD或者PMD这个level。
- 当SWAPPER_PGTABLE_LEVELS > 3的时候,需要创建PUD这一级的Translation table。
- 当SWAPPER_PGTABLE_LEVELS > 2的时候,需要创建PMD这一级的Translation table。
- 当虚拟地址是48个bit,4k page size,这时候page level等于4,映射关系是PGD(L0)—>PUD(L1)—>PMD(L2)—>Page table(L3)—>page
但是如果采用了section mapping,映射关系是PGD(L0)—>PUD(L1)—>PMD(L2)—>section。在create_pgd_entry函数中将创建PGD和PUD这两个中间level
3. create_block_map
该函数就是在tbl指定的Translation table中建立block descriptor以便完成address mapping,以2M为大小进行映射。具体mapping的内容是将start 到 end这一段VA mapping到phys开始的PA上去,代码如下:
.macro create_block_map, tbl, flags, phys, start, end
lsr \phys, \phys, #SWAPPER_BLOCK_SHIFT
lsr \start, \start, #SWAPPER_BLOCK_SHIFT
and \start, \start, #PTRS_PER_PTE - 1 // table index
orr \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT // table entry
lsr \end, \end, #SWAPPER_BLOCK_SHIFT
and \end, \end, #PTRS_PER_PTE - 1 // table end index
9999: str \phys, [\tbl, \start, lsl #3] // store the entry
add \start, \start, #1 // next entry
add \phys, \phys, #SWAPPER_BLOCK_SIZE // next block
cmp \start, \end
b.ls 9999b
.endm
create_pgd_entry宏自带5个参数,部分参数说明如下
- tbl:tbl是pgd translation table的地址
- flags:表示当前页表项指示的是block还是page
- phys: 要映射的物理地址的起始地址
- start和end分表表示物理地址要映射到的虚拟地址的开始和结束
其处理流程如下:
- 前6行当中,phys和flag计算得到页表项的内容,通过start得到页表的index开始,通过end得到页表的计数。
- 9999循环映射从phys开始的地址映射到start—end的区域
4. 如何创建页表
负责创建映射关系的函数是create_page_tables。create_page_tables函数负责identity mapping和kernel image mapping。前文提到identity mapping主要是打开MMU的过度阶段,因此对于identity mapping不需要映射整个kernel,只需要映射操作MMU代码相关的部分。首先我们在回头看看create_page_tables的流程
- 对identity mapping和kernel mapping对应的区域invalid cache操作
- identity和swapper的页表区域清零操作
- 对identity区域进行映射,映射的过程就是调用create_pgd_entry和create_block_map的过程
- 对kernel区域进行映射,映射的过程就是调用create_pgd_entry和create_block_map的过程
4.1 identity区域进行映射
create_page_tables创建了一个恒等映射,把idmap.text段的虚拟地址映射到相同的物理地址上,这个映射的页表在idmap_pg_dir页表中,映射的起始地址为idmap_text_start,结束地址为idmap_text_end.
4.2 创建内核映像的页表
/*
* Map the kernel image (starting with PHYS_OFFSET).
*/
adrp x0, swapper_pg_dir
mov_q x5, KIMAGE_VADDR + TEXT_OFFSET // compile time __va(_text)
add x5, x5, x23 // add KASLR displacement
create_pgd_entry x0, x5, x3, x6
adrp x6, _end // runtime __pa(_end)
adrp x3, _text // runtime __pa(_text)
sub x6, x6, x3 // _end - _text
add x6, x6, x5 // runtime __va(_end)
create_block_map x0, x7, x3, x5, x6
/*
* Since the page tables have been populated with non-cacheable
* accesses (MMU disabled), invalidate the idmap and swapper page
* tables again to remove any speculatively loaded cache lines.
*/
adrp x0, idmap_pg_dir
adrp x1, swapper_pg_dir + SWAPPER_DIR_SIZE
dmb sy
bl __inval_cache_range
-
加载swapper_pg_dir页表的基地址到x0寄存器
-
加载内核映像即将要映射的虚拟地址为KIMAGE_VADDR + TEXT_OFFSET,即__va(_text)
-
内核映像的虚拟地址要加上KASLR,x23寄存器存放了_PHYS_OFFSET的值
-
内核映像的起始地址为_text,结束地址为_end,计算内环境映像借宿的虚拟地址
-
调用create_block_map建立页表映射
- x0:tbl是pgd translation table的地址
- x7:表示当前页表项指示的是block还是page
- x3: 要映射的物理地址的起始地址
- x5和x6分表表示物理地址要映射到的虚拟地址的开始和结束
-
使恒等映射页表idmap_pg_dir和内核态页表swapper_pg_dir对应的高速缓存无效,虽然目前还没有使能MMU,但是数据可能被提前取到高速缓存中,因此要清对应的高速缓存并使它无效
所以函数会创建两个映射:
-
一段是identity mapping,其实就是把地址等于物理地址的那些虚拟地址mapping到物理地址上去,打开MMU相关的代码需要这样的mapping
-
kernel image mapping,内核代码欢快的执行当然需要将kernel running需要的地址(kernel txt、rodata、data、bss等等)进行映射了。
具体的映射情况可以参考下图:
5 开启MMU
ENTRY(__enable_mmu)
mrs x1, ID_AA64MMFR0_EL1
ubfx x2, x1, #ID_AA64MMFR0_TGRAN_SHIFT, 4
cmp x2, #ID_AA64MMFR0_TGRAN_SUPPORTED
b.ne __no_granule_support
update_early_cpu_boot_status 0, x1, x2
adrp x1, idmap_pg_dir
adrp x2, swapper_pg_dir
msr ttbr0_el1, x1 // load TTBR0
msr ttbr1_el1, x2 // load TTBR1
isb
msr sctlr_el1, x0
isb
/*
* Invalidate the local I-cache so that any instructions fetched
* speculatively from the PoC are discarded, since they may have
* been dynamically patched at the PoU.
*/
ic iallu
dsb nsh
isb
ret
ENDPROC(__enable_mmu)
__enable_mmu函数传递一个参数
- x0:表示SCTLR_EL1的值
因为ARM64处理器有两个页表的地址寄存器,一个使TTBR0,另外一个使TTBR1,当虚拟地址的第63位为0时,选择TTBR0指向的页表;当虚拟地址的第63位为1时,,选择TTBR1指向的页表。
由于ARM64的Soc的处理器物理地址的起始地址是从0开始,物理内存大小不可能太大,所以在做恒等映射的时候,我们采用TTBR0来映射低256TB大小的地址空间,而把内核映像映射到内核空间时,采用TTBR1。该函数通过msr ttbr0_el1, x1和msr ttbr1_el1, x2来完成配置。
参考文档
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] ,回复【面试题】 即可免费领取。