Java八股文核心串联:JVM、集合与类加载机制

发布时间:2026/11/2 8:10:00
Java八股文核心串联:JVM、集合与类加载机制 简介面向Java后端开发与面试复习场景的核心知识整理PDF适合希望系统梳理JVM、集合、多线程、设计模式等常考点的初中级开发者与求职者用于查漏补缺和面试冲刺。资源包为1个PDF文件大小10.71MB内容按目录组织从JVM架构、类加载器、执行引擎与运行时数据区讲起细分程序计数器、虚拟机栈、本地方法区、堆、方法区并延伸到新生代、老年代、永久代及MinorGC复制—清空—互换流程便于按知识模块检索和对照复习。现有650人学习下载可作为面试前的框架梳理材料也可配合项目排查理解内存区域与垃圾回收相关概念。PDF单文件便于跨设备阅读但内容侧重知识提纲与要点整理适合已有Java基础、需要快速回顾核心八股与查漏补缺的读者。1. 什么是 Java 八股文背了会忘、讲出来才记得住「八股文」这三个字很多 Java 开发者第一次听到是在准备面试的时候心里多半犯嘀咕不就是一堆要背的题吗可真坐到面试桌对面被追问一句「新生代为什么用复制算法而不是标记清除」脑子里往往只剩一句结论再往下问一层就卡住了。Java 核心知识之所以被整理成八股文不是因为它只能拿来应付考试而是因为它把 JVM、集合、类加载这些本来连成一片的东西裁成了一问一答的碎片。碎片背起来快忘得也快真正能救命的是你知道每个答案背后在解决什么问题。这份资料把散落的考点重新串成线接下来就按「是什么、为什么、怎么区分」的顺序一块一块拆开看。2. JVM 内存与垃圾回收对象住在哪谁来决定它该走2.1 线程私有的三块区域程序计数器、虚拟机栈、本地方法栈先把 JVM 想象成一栋写字楼每个线程就是楼里一间独立办公室办公室里发生的事情别的房间看不见。线程私有的三块内存就是这间办公室里的三样东西桌上的一张书签、墙上的一沓便签、抽屉里的一本外文词典。程序计数器就是那张书签它记录「当前线程执行到第几条字节码指令」。CPU 的时间片在多个线程之间来回切换切走之前把书签夹好切回来才能接着往下读。它是 JVM 内存里唯一不会抛出内存溢出错误的区域因为这块空间在 JVM 规范里小到几乎可以忽略。线程执行 native 方法时计数器的值是空的因为 native 方法不走字节码那一套。虚拟机栈是墙上那沓便签每调用一个 Java 方法就贴一张上去方法返回就撕掉一张这张便签叫栈帧。一个栈帧里装着局部变量表方法参数和方法内定义的局部变量、操作数栈计算过程中的临时中转站、动态链接指向常量池里本方法的引用和方法出口返回地址。方法一层层调用便签就一层层往上叠递归太深叠到天花板就会抛出 StackOverflowError。栈也可以动态扩展扩不动时抛 OutOfMemoryError。本地方法栈的职责和虚拟机栈一样只是服务对象换成了 native 方法。所谓 native 方法是指用 C 或 C 写、通过 JNI 接口被 Java 调用的那部分代码。很多讲 JVM 内存的资料会把它和虚拟机栈合并成一块来讲实际在 HotSpot 里两者确实是合一的但规范上它们是两个独立概念面试时被问到「JVM 内存分几块」答五块比答四块更稳。这三块区域有个共同点随线程生随线程死。线程结束它们占用的内存自然释放不需要垃圾回收操心。所以排查内存问题时如果堆的占用很平稳、线程数却在疯涨方向就该往栈上想而不是盯着堆看。2.2 线程共享的两块堆与方法区Java 8 之后方法区去哪了堆是整栋楼共用的仓库所有 new 出来的对象实例和数组都堆在这里它是垃圾回收的主战场。堆的唯一使命就是存对象所以它的结构设计全都围绕「怎么让回收更高效」展开这就引出了新生代和老年代的分代。方法区是另一块共享区域存的是类被加载后的元信息类的结构、常量池、静态变量、即时编译器编译后的代码。注意它存的是「类的描述」不是「类的实例」实例在堆里。方法区在 JVM 规范里只是一个逻辑概念具体怎么落地由各个虚拟机自己决定。这里藏着 Java 八股文里最经典的一个演变。Java 8 之前HotSpot 用「永久代」来实现方法区永久代这块内存位于堆内大小受堆参数限制类多了、常量池膨胀了就容易撞上内存溢出。Java 8 之后永久代被移除换成「元空间」元空间搬到了本地内存不再受堆大小约束能用的上限变成了机器物理内存。同时字符串常量池从方法区挪进了堆里。这次改动解决的是一个很实际的问题永久代的大小太难估。调小了稍微多加载一些类就溢出调大了又白白占着堆空间不给对象用。元空间把这块压力从堆里挪出去堆就能专心干「存对象」这一件事。代价是元空间也可能把机器内存吃光所以线上仍然需要给它设一个上限。用一句话记住这条演变链方法区是规范永久代是 Java 8 之前的实现元空间是 Java 8 之后的实现。三者不是并列关系而是「概念—旧实现—新实现」的关系。2.3 新生代与老年代Minor GC 的复制三步走堆被分成新生代和老年代比例默认是 1:2。新生代再细分成三块Eden、SurvivorFrom、SurvivorTo默认比例 8:1:1。为什么这么分因为绝大多数对象活不过第一轮回收这是分代收集理论的基本假设。新对象优先在 Eden 区出生。Eden 满了触发一次 Minor GC也就是只回收新生代的垃圾。回收过程可以概括成三步把 Eden 和 SurvivorFrom 里还活着的对象全部复制到 SurvivorTo同时把每个对象的「年龄」加 1。清空 Eden 和 SurvivorFrom这两块区域重新变成空闲状态。SurvivorTo 和 SurvivorFrom 互换角色刚才接收对象的那块变成下一次的 From。这个「复制、清空、互换」的循环就是资料里写的 Minor GC 全过程。对象每熬过一次 Minor GC年龄就长一岁长到默认 15 岁这个阈值可以通过参数调整就会被晋升到老年代。除了年龄达标还有两种提前晋升的情况一是对象太大新生代放不下直接进老年代二是 Survivor 区里相同年龄的所有对象大小加起来超过 Survivor 空间的一半那么这批对象里年龄大于等于该年龄的可以直接晋升这叫动态年龄判定。分代的意义在于「因地制宜」。新生代对象朝生夕死存活率极低用复制算法只需要复制少量存活对象成本很低而且 8:1:1 的划分让每次只有 10% 的空间被闲置浪费。老年代对象存活率高复制算法就不划算了得换标记清除或者标记整理。同一个堆里跑两套算法这就是分代收集的核心思路。2.4 怎么判定垃圾引用计数为什么不够用回收之前得先知道谁是垃圾。有两种主流判定方式引用计数法和可达性分析法。引用计数法的思路很朴素给每个对象配一个计数器被引用一次加 1引用失效减 1减到 0 就说明没人用了可以回收。它实现简单、判定快但有一个致命缺陷处理不了循环引用。看下面这段代码。// 演示引用计数法处理不了的循环引用 public class CycleDemo { Object ref; public static void main(String[] args) { CycleDemo a new CycleDemo(); CycleDemo b new CycleDemo(); a.ref b; // a 持有 bb 的计数变成 1 b.ref a; // b 持有 aa 的计数变成 1 a null; // 断开 main 栈帧对 a 的引用 b null; // 断开 main 栈帧对 b 的引用 // 此时 a、b 互相持有计数都是 1但外部再也访问不到它们 } }这段代码里两个对象互相牵着对方的手计数器都不会归零可 main 方法里已经没有任何变量能指向它们了程序永远拿不到这两个对象。引用计数法会误判它们「还在使用」内存就这样漏掉了。这也是主流 Java 虚拟机不采用引用计数作为唯一判定手段的原因。可达性分析法换了个思路不再问「谁引用了我」而是问「从根上能不能走到我」。它从一组叫 GC Roots 的对象出发沿着引用链向下遍历能走到的对象是活的走不到的就是垃圾。GC Roots 主要包括虚拟机栈中局部变量表引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象以及活跃线程、系统类加载器等。上面那段循环引用的例子用可达性分析一跑就露馅了a 和 b 都从 GC Roots 走不到直接判定为垃圾该回收回收。可达性分析也不是没有代价它需要真正去遍历一遍对象图所以要暂停用户线程这个暂停就是所谓的 Stop The World。后面讲的收集器绝大多数优化都在想办法把这个暂停压短。2.5 三种基础回收算法与分代收集判定出垃圾之后怎么把它们清走有三种基础算法各自的取舍完全不同。标记清除算法最简单先标记出所有需要回收的对象标记完成后统一清除。它的缺点是两个一是效率不稳定对象越多标记和清除越慢二是会产生大量不连续的内存碎片碎片多了以后要分配一个大对象时可能找不到足够大的连续空间只能提前触发下一次回收。复制算法把内存分成大小相等的两块每次只用其中一块这块用完了就把还活着的对象复制到另一块然后一次性清空当前这块。它没有碎片问题分配内存时按顺序挪指针就行速度很快。代价是内存利用率直接砍半而且如果存活对象多复制的开销会变得很大。所以复制算法适合存活率低的场景也就是新生代。标记整理算法是标记清除的改良版标记完之后不直接清除而是把所有存活对象往内存的一端移动然后清理掉边界以外的内存。它解决了碎片问题也不需要牺牲一半空间代价是移动对象本身成本不低而且移动过程中必须暂停用户线程。所以它适合存活率高、又在意碎片的场景也就是老年代。分代收集不是第四种算法而是把上面三种按区域组合使用新生代用复制算法老年代用标记清除或标记整理。资料里还提到另一种思路叫分区收集它把整个堆划分成多个大小相等的独立区域每个区域单独回收G1 就是这种思路的代表。分代是按对象的年龄分区分区是按内存地址切块两者解决的问题不一样分代是为了让不同存活率的对象用上合适的算法分区是为了让每次回收只处理一小块、把停顿控制住。2.6 四种引用与七种收集器什么时候用哪个Java 把引用分成了四个强度等级强度从高到低是强、软、弱、虚对应的是「垃圾回收器对这个对象有多客气」。强引用就是普通 new 出来的对象引用只要引用链还在垃圾回收器宁可抛内存溢出也不回收它。软引用描述的是「还有用但非必需」的对象系统内存充足时不动它内存快不够了才回收天生适合做缓存。弱引用比软引用更弱只要发生垃圾回收不管内存够不够它指向的对象都会被回收ThreadLocal 的 Entry 和 WeakHashMap 用的就是它。虚引用最特殊它完全不影响对象生命周期唯一的作用是对象被回收时收到一条通知用来做一些清理和跟踪工作必须配合引用队列使用。理解这四种引用的关键是别把它们当成「四种不同的对象」它们描述的是「引用关系本身的强度」。同一个对象可以被强引用指着也可以同时被软引用指着只要强引用还在软引用说什么都没用。垃圾收集器是回收算法的具体实现资料里列了七种各自服务不同的年代和场景。收集器工作区域线程模型核心算法主要目标Serial新生代单线程复制简单、客户端场景ParNew新生代多线程复制配合 CMS 使用Parallel Scavenge新生代多线程复制高吞吐量Serial Old老年代单线程标记整理CMS 的后备方案Parallel Old老年代多线程标记整理配合 Parallel ScavengeCMS老年代多线程标记清除低停顿G1整堆多线程分区 复制/整理可预测停顿这里最值得展开的是 CMS 和 G1 这两个。CMS 追求的是最短停顿时间它的回收过程分四步初始标记、并发标记、重新标记、并发清除。初始标记只标记 GC Roots 能直接关联到的对象速度很快但需要暂停并发标记从这些对象出发遍历整个对象图这一步耗时最长但可以和用户线程同时跑重新标记是为了修正并发标记期间因为用户线程继续运行而产生变动的那些对象需要暂停但比初始标记长不了太多并发清除最后清掉垃圾同样可以和用户线程并发。四步里只有两步需要停停的时间还都很短这就是 CMS 低停顿的来源。CMS 的毛病也很明显它用的是标记清除算法会产生碎片并发阶段用户线程还在跑会不断产生新垃圾这部分叫浮动垃圾只能留到下一次回收它对 CPU 资源敏感并发阶段会抢走一部分算力。所以在堆特别大、对停顿又极其敏感的场景里CMS 就显得吃力了。G1 把堆切成一个个大小相等的 Region每个 Region 可以是 Eden、Survivor 或者老年代逻辑上还是分代的物理上不再连续。它维护一个优先列表每次根据允许的收集时间优先回收垃圾最多的那些 Region所以叫 Garbage First。这样做的结果是停顿时间变得可预测你可以设一个期望的最大停顿时间G1 尽量在这个时间内挑最划算的区域回收。G1 整体上采用标记整理局部上采用复制所以不会像 CMS 那样攒出一堆碎片。选收集器的逻辑其实就一句话先确定你更在意吞吐量还是停顿时间。后台批处理这类任务在意单位时间干完多少活Parallel 系列更合适在线服务在意每次卡顿多久CMS 和 G1 更合适堆特别大、又想要可控停顿G1 是更稳的选择。3. 集合框架ArrayList、HashSet、HashMap 到底怎么选3.1 List 三兄弟数组和链表的取舍List 是有序、可重复的集合三个常用实现分别是 ArrayList、Vector、LinkedList它们的差别本质上就是「底层用什么存数据」。ArrayList 底层是一个数组。数组的特点是内存连续所以按下标随机访问极快时间复杂度 O(1)但往中间插入或删除元素时后面的元素要整体挪位复杂度 O(n)。它有个默认容量 10扩容时按旧容量的 1.5 倍增长具体算法是旧容量加上旧容量右移一位。扩容这个动作本身要申请新数组、复制旧数据所以如果你的场景能预估元素数量构造时直接指定初始容量能省掉好几次扩容。Vector 底层也是数组和 ArrayList 最大的区别是它的方法基本都加了 synchronized所以是线程安全的。代价是每次操作都要抢锁单线程场景下比 ArrayList 慢不少。它扩容时按 2 倍增长比 ArrayList 更激进。这个类如今基本属于历史遗留需要线程安全的 List 时通常用并发包里的 CopyOnWriteArrayList 或者自己加锁而不是选 Vector。LinkedList 底层是双向链表每个节点除了存数据还要存前后两个指针。它的随机访问必须从头或从尾一个个走复杂度 O(n)所以别用 get(i) 去遍历它但它的头尾插入删除是 O(1)而且它同时实现了 Deque 接口可以当队列和栈用。选型上有个流传很广的说法叫「查多用 ArrayList增删多用 LinkedList」这话只对了一半。LinkedList 的增删快指的是「已经拿到那个节点」的前提下如果还要先遍历找到位置那 O(n) 的查找成本已经把优势吃掉了。而且数组内存连续对 CPU 缓存友好实际跑起来 ArrayList 在大多数场景都不慢。真正该选 LinkedList 的场合是频繁在头部插入、或者需要当队列用的时候。3.2 Set 三兄弟去重方式决定了顺序Set 是不允许重复元素的集合三个常用实现的差别在于「怎么判断重复」和「元素按什么顺序摆」。HashSet 底层就是一个 HashMap元素作为 key 存进去value 统一放一个固定的 Object 占位。判断重复靠的是 hashCode 和 equals先算哈希值找到桶再用 equals 确认是不是同一个元素。所以放进 HashSet 的对象这两个方法必须正确重写否则去重会失效。HashSet 不保证顺序迭代出来的顺序和插入顺序没有关系它还允许一个 null 元素。TreeSet 底层是 TreeMap也就是红黑树。它不靠哈希去重而是靠比较元素要么实现 Comparable 接口要么构造时传一个 Comparator。因为红黑树是有序的所以 TreeSet 里的元素天然按大小排好找某个范围的元素很方便代价是插入、删除、查找都是 O(log n)比 HashSet 慢一截。另外TreeSet 判断重复用的是比较结果是否为 0而不是 equals这一点经常被忽略。LinkedHashSet 继承自 HashSet内部用 LinkedHashMap 维护了一条双向链表把插入顺序记了下来。所以它既能去重迭代时又能按插入顺序输出。如果你既要去重又在意顺序它是首选代价是多了一点维护链表的开销。三者选型的判断很简单只要去重、不在乎顺序用 HashSet要排序用 TreeSet要去重还要记住谁先谁后用 LinkedHashSet。3.3 HashMap 的 Java 7 与 Java 8为什么要长成红黑树HashMap 是集合里被问得最多的一个因为它在 Java 7 和 Java 8 里的实现差别很大而这两个版本的差异恰好能说明「一个设计是怎么被实际问题推着走的」。Java 7 的 HashMap 是「数组 链表」结构。数组的每个位置叫一个桶key 的哈希值经过扰动后对数组长度取模决定它落在哪个桶。如果多个 key 落到同一个桶就用链表串起来这叫哈希冲突。插入新元素时用的是头插法新元素放在链表头部因为头插不需要遍历到尾部速度快。头插法在单线程下没问题但在多线程同时扩容时会出大事两个线程同时触发扩容各自复制链表节点指针指向在并发下被打乱可能让链表首尾相连形成一个环。之后再有线程去查这个桶就会在环里无限循环CPU 直接打满。这就是 Java 7 HashMap 在并发场景下最经典的问题。此外就算没成环头插法在扩容后也会把链表顺序颠倒。Java 8 把结构改成了「数组 链表 红黑树」插入方式从头插改成了尾插。尾插保证了扩容时节点顺序不变从根上消除了成环的可能。红黑树的引入则是为了对付另一种情况如果哈希函数设计得不好或者攻击者刻意构造大量冲突的 key链表会变得很长查询退化成 O(n)。转成红黑树之后最坏情况也能保持在 O(log n)。看下面这组常量它们决定了什么时候树化、什么时候退化。// HashMap 中与树化相关的关键常量Java 8 static final int DEFAULT_INITIAL_CAPACITY 1 4; // 默认容量 16 static final int MAXIMUM_CAPACITY 1 30; // 容量上限 2 的 30 次方 static final float DEFAULT_LOAD_FACTOR 0.75f; // 默认负载因子 static final int TREEIFY_THRESHOLD 8; // 链表长度达到 8 时考虑树化 static final int UNTREEIFY_THRESHOLD 6; // 红黑树节点降到 6 时退回链表 static final int MIN_TREEIFY_CAPACITY 64; // 树化要求的最小表容量逐条解释。默认容量 16 是数组的初始长度负载因子 0.75 是「元素数量 / 数组长度」的阈值超过这个比例就扩容所以默认情况下元素超过 12 个就扩到 32。树化阈值 8 的意思是链表长度达到 8 才考虑转红黑树但还有一道附加条件数组容量必须达到 64。如果容量还不满 64HashMap 会选择先扩容而不是树化因为容量小的时候扩容能更有效地打散冲突比维护一棵树划算。退化阈值 6 比 8 小中间留了一段缓冲避免元素数量在 7、8 之间反复横跳导致频繁转换。那为什么偏偏是 8因为链表长度服从泊松分布在哈希函数正常的情况下长度达到 8 的概率极低大约是千万分之几。也就是说红黑树是一个为极端情况准备的兜底方案日常根本用不上但不能没有。扩容时还有一个细节值得记Java 8 扩容是翻倍元素的新位置只有两种可能要么留在原来的下标要么移动到「原下标 旧容量」。判断依据是哈希值和旧容量做一次与运算结果为 0 就留不为 0 就走。这个设计让扩容时不需要重新计算每个 key 的哈希效率高了不少。3.4 ConcurrentHashMap从分段锁到 CASHashMap 线程不安全Hashtable 线程安全但把所有方法都上了同一把锁等于让所有线程排队进门并发度为零。ConcurrentHashMap 要解决的就是「既要安全又要并发」。Java 7 的方案是分段锁。整个表被分成若干个 Segment默认 16 个每个 Segment 本质上是一个小的 HashMap并且继承 ReentrantLock。写操作只锁住 key 所在的那一个 Segment其他 Segment 不受影响所以理论上最多支持 16 个线程同时写。这个 16 就叫并行度构造时可以指定一旦初始化就不能再改。读操作大多不加锁靠 volatile 保证可见性。Java 8 把分段锁整个拆掉了改成「数组 链表 红黑树」锁的粒度细到了单个桶。具体做法是如果目标桶是空的直接用 CAS 把新节点放进去不加锁如果桶里已经有节点就用 synchronized 锁住这个桶的头节点再往后操作。因为锁的只是这一个桶冲突概率比锁一个 Segment 低得多并发度自然更高。size 的统计也不再靠加锁而是用 baseCount 加一个 CounterCell 数组分散计数最后汇总避免了所有线程争同一个计数器。从分段锁到 CAS synchronized变化的不是「要不要保证安全」而是「锁的范围能压到多小」。粒度越细能同时干活的线程越多这就是 ConcurrentHashMap 在高并发场景下碾压 Hashtable 的根本原因。4. 类加载与 IO/NIOJVM 的进与出4.1 类加载五个阶段加载、验证、准备、解析、初始化一个 .class 文件躺在磁盘上只是一堆字节。要让它变成一个能 new 出对象的类得经过类加载机制。整个过程分五步顺序是加载、验证、准备、解析、初始化。加载是第一步做三件事通过类的全限定名拿到它的二进制字节流从磁盘、从 jar 包、从网络都行这也是为什么类加载器能做那么多花样把这个字节流代表的静态存储结构转成方法区的运行时数据结构在堆里生成一个代表这个类的 Class 对象作为方法区数据的访问入口。验证是第二步目的只有一个保证这堆字节流不会把虚拟机搞崩。它要检查文件格式对不对、元数据是否符合规范、字节码指令会不会跳到非法位置、符号引用能不能解析到。这一步在生产环境通常开着因为关掉它省下的那点时间远不够赔一次线上事故。准备是第三步为类的静态变量分配内存并设零值。注意是零值不是代码里写的初始值一个static int value 10;在经过准备阶段后value 是 0不是 10。如果是static final修饰的常量那准备阶段就会直接赋成 10因为常量在编译期已经确定了。解析是第四步把常量池里的符号引用替换成直接引用。符号引用只是一串字符串比如「com.example.Demo」直接引用则是内存里的真实地址或者偏移量。解析这个动作不一定在初始化之前完成它可以推迟到第一次真正用到某个符号时再做。初始化是最后一步真正执行类中定义的 Java 代码也就是执行类构造器clinit方法。这个方法由编译器自动收集类中所有静态变量的赋值动作和静态代码块合并生成。它有几个必须记住的规则父类的clinit先于子类执行clinit不是必需的类里没有静态变量和静态代码块就不会生成接口的clinit不需要先执行父接口的只有用到父接口里的变量时才会初始化父接口。触发初始化的时机叫「主动引用」包括new 一个对象、读取或设置类的静态字段被 final 修饰、已在编译期放入常量池的除外、调用类的静态方法、用反射调用、初始化子类时先初始化父类、虚拟机启动时指定的主类。除此之外的引用都叫被动引用不会触发初始化比如通过子类引用父类的静态字段只会初始化父类。4.2 双亲委派为什么自己写的 String 不生效类加载器负责执行上面这套流程。Java 里有三层主要的类加载器启动类加载器负责加载 Java 核心类库扩展类加载器负责加载扩展目录下的类应用程序类加载器负责加载 classpath 上的类。除此之外用户还可以自定义类加载器。双亲委派模型规定的是它们之间的协作方式一个类加载器收到加载请求先不自己动手而是把请求往上抛给父加载器父加载器再往上抛一直到启动类加载器只有当父加载器反馈「我加载不了」子加载器才尝试自己加载。整个过程从上往下试像一条链。这套机制带来两个好处。第一个是安全所有对java.lang.String的加载请求最终都会走到启动类加载器加载的是 JDK 自带的那一份。你自己写一个同名类放进 classpath永远不会被加载也就没办法伪造一个核心类混进运行时。第二个是避免重复加载同一个类被不同加载器重复加载会变成两个不同的类双亲委派保证了同一个类在同一个加载器体系里只有一份。双亲委派不是铁律它可以被打破。最常见的一种打破方式是 SPI 机制核心类库里定义接口实现类由第三方提供。核心类库由启动类加载器加载它看不到 classpath 上的实现类于是只能让线程上下文类加载器去加载。另一种打破方式就是接下来要讲的 OSGI它干脆放弃了树状的委派结构改成网状。4.3 OSGI打破双亲委派换来热插拔OSGI 是一套模块化规范它把程序拆成一个个叫 Bundle 的模块每个 Bundle 有自己的类加载器也声明了自己要导出哪些包、依赖哪些包。加载一个类时它不按双亲委派那条链走而是先在本地找本地找不到再按依赖关系去别的 Bundle 里找。这种网状结构换来两个很强能力。一是动态改变构造Bundle 可以在运行期安装、启动、停止、更新、卸载不需要重启整个应用。二是模块化编程与热插拔模块之间的依赖在配置里写清楚谁依赖谁一目了然替换某个模块的实现时其他模块可以继续跑。代价也很直接。类加载从一棵树变成一张网排查「这个类到底从哪加载来的」会变得非常困难模块的卸载要考虑其他模块还持有它的引用一不小心就内存泄漏线程模型和类加载器交织在一起复杂度陡增。所以 OSGI 更适合那些确实需要运行期装卸模块的场景比如桌面应用插件体系、某些中间件平台。普通业务系统用 Maven 或 Gradle 做模块划分就够了上 OSGI 属于拿大炮打蚊子还得自己承担后坐力。4.4 五种 IO 模型与 NIO 三件套IO 模型讲的是「数据从内核缓冲区搬到用户程序」这件事由谁发起、谁等待、谁完成。五种模型的区别就在这三个问题上。阻塞 IO 最直白程序调用读操作线程就挂起一直等到数据准备好、并且从内核拷贝到用户空间才返回。这本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询