首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
JavaSE进阶知识梳理:集合源码、并发机制与JVM核心
📅 2026/9/9 21:59:09
✍️ 爱科研究院
👁 阅读 3,247
1. 为什么还要系统复习JavaSE进阶先说个我自己的情况。前阵子准备带一个新人给他讲项目里的一些底层封装结果发现很多我以为他应该知道的Java基础其实他并不是很熟。比如HashMap在并发下的问题、synchronized和ReentrantLock的区别、泛型擦除带来的限制这些平时写代码不一定天天碰但一旦碰到就是大坑。后面我干脆把JavaSE进阶知识重新梳理了一遍一边整理一边又踩了几个坑所以想把这份复习笔记分享出来。这篇内容适合谁如果你已经写过一段时间Java代码能熟练用List、Map、能写多线程但遇到为什么这么设计这类问题会卡壳或者你准备跳槽面试需要把JavaSE核心知识串一遍又或者你工作中经常遇到ClassCastException、ConcurrentModificationException、内存溢出这类问题但知其然不知其所以然——那这篇文章就是给你准备的。本文不做入门普及聚焦在进阶知识点上接口与抽象类、异常体系、集合源码、泛型、反射与注解、IO与NIO、并发编程、JVM基础。我尽量用实际能跑通的代码片段和踩坑记录来讲不只是罗列概念。毕竟复习JavaSE进阶知识目标只有一个让这些底层机制真正成为你写代码时的底气而不是背过的八股。2. 面向对象进阶接口、抽象类与内部类的设计权衡2.1 接口和抽象类的选择远不止语法差异在JavaSE的进阶复习里接口和抽象类是最容易被低估的一对。很多新手只知道抽象类用extends接口用implements一个类只能继承一个父类但可以实现多个接口但真正到设计层面两者的定位是完全不同的。抽象类更适合用来表达is-a关系并且强调模板化流程复用。比如你做一个数据导出功能流程是固定的查询数据、格式化、写入文件。不同数据源可能查询方式不同但流程骨架不变。这时候用抽象类定义模板方法就很自然public abstract class DataExporter { // 模板方法定义流程骨架 public final void export() { ListString data queryData(); String content formatData(data); writeToFile(content); } protected abstract ListString queryData(); protected abstract String formatData(ListString data); private void writeToFile(String content) { // 通用实现 System.out.println(写入文件 content); } }子类只需实现queryData和formatData流程本身不会被破坏。这就是抽象类最大的价值控制流程开放扩展点。而接口承载的是can-do能力契约。比如Comparable、Runnable、AutoCloseable它们定义的是一个能力不关心实现类是什么身份。接口在Java 8之后增加了默认方法和静态方法很多人以为接口越来越像抽象类其实不然。默认方法的引入是为了集合API平滑演化不是为了让接口替代抽象类。你在设计自己的代码时如果拿不准可以用一个简单标准判断多个类是否只是做这件事的方式不同如果是用接口如果多个类共享一个流程骨架且缺了某个步骤就报错用抽象类。2.2 内部类与Lambda的演进对代码结构的影响内部类在JavaSE学习里经常被一带而过但在进阶复习时值得重新审视。内部类分为成员内部类、静态内部类、局部内部类和匿名内部类。其中静态内部类和成员内部类有个关键区别成员内部类隐式持有外部类引用这在内存敏感场景下是个隐患。如果你在一个长期存活的对象里创建了成员内部类的实例而这个内部类又没被释放外部类也不会被GC回收容易出现内存泄漏。匿名内部类在Java 8之后逐渐被Lambda替代但两者有一个重要差别匿名内部类会生成独立的class文件而Lambda走的是invokedynamic运行时才决定具体调用性能表现更好。而且Lambda中的this指向的是外围对象匿名内部类的this指向自身。如果你在写代码时发现Lambda里访问不到期望的字段先确认一下是不是这个原因。一个实战场景我在写一个事件监听器时用Lambda简化了代码但调试时发现Lambda内部调用了一个实例方法而那个实例方法依赖外部类的状态。因为Lambda的this指向外部类所以没有问题。但如果当时用的是匿名内部类反而会多一层容易混淆的this语义。代码写多了你会发现内部类语法简单但使用场景里的内存模型和语义指向才是关键。3. 异常体系深入从try-catch到防御式编程3.1 异常分类与处理机制的核心逻辑JavaSE中的异常体系Throwable下分Error和Exception。复习时重点不是背出几个子类而是建立一套处理心智模型。Error比如OutOfMemoryError、StackOverflowError属于JVM级别的严重问题应用层代码基本无法处理catch了也没用Exception分为受检异常和运行时异常。受检异常Checked Exception是Java的一大特色编译器强制你处理比如IOException、SQLException。设计者本意是好的让程序员不忽略那些可能出错的环节。但实际开发中很多人为了通过编译写出一堆空的catch块这比不处理更糟糕——异常被静默吞掉问题在三个月后以诡异形式爆发。我见过一个线上问题就是某个文件解析逻辑catch到ParseException后只打了e.printStackTrace()日志文件被刷爆但业务完全没感知直到数据对不上才查出来。处理异常的核心原则就两条要么抛出要么处理绝不能只打印后继续往下走。捕获异常时要精确匹配类型不要一股脑catchException。3.2 try-with-resources与自定义异常的坑Java 7引入的try-with-resources是处理资源释放的最佳实践。它的底层原理是自动调用AutoCloseable.close()并且支持多个资源同时管理try (FileInputStream fis new FileInputStream(a.txt); FileOutputStream fos new FileOutputStream(b.txt)) { byte[] buffer new byte[1024]; int len; while ((len fis.read(buffer)) ! -1) { fos.write(buffer, 0, len); } }这段代码不用在finally里手动关闭异常时资源也会关闭。需要注意的一个细节是如果try块本身抛出了异常同时close()方法也抛出异常那么原始异常会被抑制但Throwable.getSuppressed()里能拿到被抑制的异常。排查问题时如果不看suppressed信息很可能会误判。自定义异常时很多人只写一个空构造函数这是错的。一个合格的自定义异常应该至少包含无参构造、带消息构造、带消息和原因的构造。我通常还会加上一个带Throwable cause的构造这样在业务层捕获底层异常后可以包装成业务异常并保留原始堆栈方便日志追踪。我在实际项目中遇到过一个坑自定义了BizException但忘了设置serialVersionUID后来改了异常类结构线上反序列化老报错。虽然异常对象一般不跨JVM传输但在分布式链路中把异常信息序列化到消息队列时还是会踩到。所以自定义异常类一定记得加serialVersionUID这属于典型的JavaSE复习时容易忽略但生产环境很疼的细节。4. 集合框架源码视角HashMap、ArrayList与并发集合4.1 HashMap的底层结构从数组链表到红黑树JavaSE进阶复习绕不开HashMap。面试喜欢问写代码的也经常用但真正理解它底层机制的人不多。HashMap底层是数组加链表JDK 8之后链表过长会转红黑树。当你put一个键值对时HashMap会先计算key.hashCode()然后将hash值做扰动处理再通过(n - 1) hashn为数组长度得到桶的索引。为什么用而不是取模因为当数组长度是2的幂次方时(n - 1) hash等价于hash % n但位运算更快。这也解释了为什么HashMap扩容时总是扩大到原来的两倍。关于负载因子默认是0.75。这是一个在时间和空间之间折中的经验值——太小了浪费空间太大了哈希碰撞概率升高。当你put的元素个数超过数组长度 * 负载因子时触发扩容。扩容要重新计算所有元素的位置这是个O(n)的操作如果一开始能预估数据量最好在构造时指定初始容量省掉扩容开销// 预估需要存1000个元素那么初始容量应设为1000/0.7511334向上取2的幂次就是2048 MapString, String map new HashMap(2048);还有个常见问题HashMap是线程不安全的。在多线程环境下put可能导致数据覆盖甚至JDK 7版本中并发扩容会形成循环链表造成CPU 100%。很多人说用ConcurrentHashMap替代但ConcurrentHashMap也有细节——它的size()方法是弱一致的可能不是实时精确值在高并发场景下不能用它做精确统计。4.2 ArrayList的扩容机制与内存效率分析ArrayList底层是Object[]数组初始化时如果不指定容量会是一个空数组第一次add时才扩容到10。每次扩容都是int newCapacity oldCapacity (oldCapacity 1)也就是1.5倍。为什么是1.5倍而不是2倍如果用2倍扩容后数组大小会快速增长浪费内存如果太慢比如1.1倍扩容次数增多每次都涉及Arrays.copyOf的数组复制性能差。1.5倍是一个经验上平衡的选择。这里有个很容易踩的坑如果你频繁在ArrayList中间插入或删除元素比如list.add(0, element)每次操作都要将后面的元素整体后移一位时间复杂度是O(n)。在大数据量场景下这个操作会拖垮性能。正确做法是看具体场景如果添加和删除操作远多于随机读取用LinkedList更合适虽然LinkedList的节点占用内存更大而且存在指针操作开销不是绝对优势如果是尾部追加随机访问的稳定场景ArrayList是最佳选择。我在项目里封装过一个缓存队列原始实现用的ArrayList往头部插入频繁压测时吞吐量远低于预期。换成LinkedList后数据量小的时候反而更慢因为LinkedList的每个节点都是独立对象内存不连续CPU缓存命中率低。最后我的方案是用ArrayList预留容量批量追加偶尔头部插入时用ArrayDeque替代性能稳定了很多。这个经历让我体会到集合选型不能只看时间复杂度内存模型和实际使用模式都很重要。4.3 并发集合的适用场景与误用CopyOnWriteArrayList是读多写少场景的利器它的核心机制是写操作时复制一份底层数组在副本上修改然后替换引用。读操作不加锁因此并发读性能很高但每次写都复制全量数组代价巨大。如果你在写频繁的场景用了它内存和GC压力会直线上升可能比synchronized的同步集合还慢。ConcurrentHashMap的分段锁设计我就不展开说了JDK 8之后改用CAS synchronized锁单个桶节点锁粒度更细。用它的时候注意两点第一putIfAbsent和computeIfAbsent在复杂业务里更安全第二keySet、values这些视图是弱一致性的迭代过程中如果其他线程修改了集合迭代器不会抛ConcurrentModificationException但也看不到最新数据不能拿来做同步控制。复习集合这块我建议把每个接口的适用场景自己整理成一张表读多写少、写多读少、有序性、线程安全、允许null每个维度过一遍。只有把这些点串起来看到需求时才能条件反射式地选对集合。5. 泛型机制类型擦除与上下界限定5.1 类型擦除到底擦掉了什么Java的泛型是JDK 5引入的但它在运行时并不存在真正的泛型类型。编译器在编译阶段会执行类型擦除Type Erasure泛型类型参数会被替换为它的边界类型通常是Object并插入必要的类型转换。来看个实际例子ListString stringList new ArrayList(); ListInteger intList new ArrayList(); System.out.println(stringList.getClass() intList.getClass()); // true两个List的运行时Class完全相同因为泛型信息被擦除了。这就是为什么你不能通过getClass()判断一个List到底存的是什么类型。有一个例外泛型中的通配符可以通过反射拿到一部分信息但日常开发中不常用。类型擦除带来的第一个局限就是new T()这种操作会编译失败因为运行时不知道T是什么无法调用构造器。解决方案通常是传入ClassT类型作为参数然后用反射创建public T T createInstance(ClassT clazz) throws Exception { return clazz.getDeclaredConstructor().newInstance(); }另一个典型场景是泛型方法设计。比如你写一个通用的JSON解析工具需要把JSON字符串转为指定类型的对象public static T T parseJson(String json, ClassT clazz) { // 内部通过clazz获取类型信息 return objectMapper.readValue(json, clazz); }如果不传ClassT在运行时是没有办法知道具体转化类型的。很多JSON库提供的TypeReference就是为了突破类型擦除的限制它通过匿名内部类在父类中保存泛型信息。理解这一点你在使用TypeReference时就不会觉得它神秘了。5.2 上界和下界通配符的使用原则泛型通配符是JavaSE进阶里绕不开的难点。? extends T定义了上界表示集合中存放的是T或T的子类? super T定义了下界表示集合中存放的是T或T的父类。两者的读写特性完全不同业界有个著名的PECS原则全称是Producer Extends, Consumer Super。简单解释如果你要从集合中读取数据并把数据当作T来处理这个集合是生产者用? extends T如果你要向集合中写入数据集合是消费者用? super T。// 只能读取不能写入除了null public void printFruits(List? extends Fruit fruits) { for (Fruit fruit : fruits) { System.out.println(fruit.getName()); } } // 只能写入不能安全读取 public void addApple(List? super Apple apples) { apples.add(new Apple()); }为什么List? extends Fruit不能add假设这个List实际类型是ListApple你往里add一个Banana类型安全就会被破坏所以编译器直接禁止写入写null可以但没实际意义。理解这个约束后你会发现PECS原则不仅帮助记忆还能防止写出有类型安全隐患的代码。泛型复习时还有一个容易忽略的点泛型异常。Java不允许catch一个泛型类型的异常但可以throws一个泛型类型这在某些需要灵活抛出类型化异常的框架代码里很有用。不过日常业务开发中不建议过度设计泛型本身就够绕了再用泛型异常会让代码可读性直线下降。6. 反射与注解运行时信息的获取与自定义场景6.1 反射的三大核心获取方式与应用场景反射是JavaSE中最魔法的机制给了程序在运行时审视自己的能力。获取Class对象有三种方式实际使用时有区别// 方式一类名.class编译期就确定 ClassUser clazz1 User.class; // 方式二对象.getClass()运行时获取 User user new User(); Class? extends User clazz2 user.getClass(); // 方式三Class.forName()从全限定类名加载 Class? clazz3 Class.forName(com.example.User);方式三最灵活也是很多框架加载驱动时采用的方式。获取到Class后可以通过getDeclaredFields()获取全部字段getDeclaredMethods()获取全部方法getDeclaredConstructor()获取构造器。注意getDeclaredXxx和getXxx的区别前者能拿到私有的后者只能拿到public。反射最常见的应用场景是框架开发Spring的IOC、MyBatis的ORM映射动态代理JDK代理底层就是反射序列化/反序列化工具Jackson、Gson通过反射创建对象并赋值注解处理器运行时读取注解信息反射性能比直接调用慢因为类型安全检查、方法查找等环节都是运行时做的。如果某个反射调用在循环里执行建议加缓存——把反射获取到的Method或Field缓存起来避免每次重复查找。我在一个批量导出功能里就是用反射给实体类字段赋值第一次实现直接遍历字段再get、set百万级数据跑了很久。优化后缓存Field对象性能提升了近一倍。6.2 自定义注解从定义到使用的完整链路注解本身不做事它只是元数据。真正干活的是读取注解的处理器。自定义一个注解并运行完整链路是定义一个注解比如LogExecutionTarget({ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) public interface LogExecution { String value() default ; }Target指定这个注解能贴在哪个位置方法、字段、类、参数等Retention决定注解的生命周期。如果你要在运行时通过反射读取必须用RetentionPolicy.RUNTIME如果只需在编译期处理用CLASS或SOURCE就够了比如Override就是SOURCE编译完就丢弃。然后写一个注解处理器用反射读取注解并实现逻辑public class LogProcessor { public static void process(Object target) { Method[] methods target.getClass().getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(LogExecution.class)) { long start System.currentTimeMillis(); try { method.invoke(target); } catch (Exception e) { e.printStackTrace(); } long cost System.currentTimeMillis() - start; System.out.println(方法 method.getName() 耗时: cost ms); } } } }这是运行时注解的经典写法。如果你进一步用动态代理或AOP就能把注解拦截逻辑从业务代码中剥离出来形成清晰的横切关注点。有个坑我踩过自定义注解时忘了加Retention(RetentionPolicy.RUNTIME)结果运行时用isAnnotationPresent一直返回false。查了半天才发现注解的生命周期设置成了默认的CLASS编译期有运行期就被丢弃了。另外注意Inherited元注解它只能让注解对子类生效但作用范围有限——标注在类上的注解能被子类继承标注在方法上的不会。7. IO与NIO从流式读写到非阻塞模型7.1 字节流、字符流与桥接机制IO流在JavaSE中的地位有点尴尬写业务代码时天天用但大多数人只知道FileInputStream读文件、BufferedReader读行底层怎么流转的并不清楚。字节流和字符流的本质区别在于处理单位字节流按1字节处理字符流按1字符处理。但字符和字节之间的转换依赖字符集编码比如UTF-8中一个中文占3字节GBK占2字节。如果字节流直接按字符读很容易乱码。Java提供InputStreamReader做桥接把字节流转换为字符流同时指定字符集。BufferedReader reader new BufferedReader( new InputStreamReader(new FileInputStream(data.txt), StandardCharsets.UTF_8) );这行代码在很多老项目中频繁出现但有个性能问题层层包装没问题但别忘了最外层用BufferedReader。缓冲流的意义在于减少底层IO操作次数——每写一个字符就调一次系统调用开销巨大缓冲之后攒到一定量再一次性写入性能提升非常明显。7.2 NIO的三大核心Buffer、Channel、SelectorNIONew IO的复习难点不在API而在编程模型。核心三件套是Buffer、Channel和Selector。网络编程中旧IO的阻塞模型是一个线程处理一个连接连接多了线程就爆炸NIO基于Selector一个线程可以同时监听多个Channel的事件可读、可写、连接就绪这就是多路复用。写一个简化版的理解模型Selector selector Selector.open(); ServerSocketChannel serverChannel ServerSocketChannel.open(); serverChannel.configureBlocking(false); serverChannel.register(selector, SelectionKey.OP_ACCEPT); while (selector.select() 0) { IteratorSelectionKey keyIterator selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { SelectionKey key keyIterator.next(); if (key.isAcceptable()) { // 处理新连接 } else if (key.isReadable()) { // 处理可读事件 } keyIterator.remove(); } }但说实话如果不是中间件开发者日常业务中用NIO手写网络通信的场景不多。大多数人用到NIO都是通过Netty这类封装好的框架。所以复习NIO时我建议重点放在理解模型和定位上不要把时间花在手写轮子上知道BIO是阻塞的、NIO是非阻塞多路复用的、AIO是异步的但Java AIO实际应用不广以及为什么Netty底层基于NIO而非AIO就够了。我再提一个容易被忽略的坑Files.readAllLines()虽然方便但它会一次性把整个文件加载进内存。如果处理的是几GB的日志文件直接OutOfMemoryError教做人。大文件应该用BufferedReader逐行读取或者用Files.lines()配合Stream流式处理。8. 并发编程核心从线程池参数到锁机制8.1 线程创建方式与线程池的核心参数JavaSE进阶里并发是重头戏而线程池又是并发的核心工具。很多人会背ThreadPoolExecutor的七个参数但一到实际配置就懵。七个参数分别是核心线程数、最大线程数、空闲存活时间、存活时间单位、任务队列、线程工厂、拒绝策略。最核心的决策是核心线程数和任务队列怎么选。如果任务以CPU计算为主核心线程数建议设为CPU核数 1如果是IO密集型比如网络请求、磁盘读写线程数可以更大经验公式是CPU核数 * 2因为IO等待期间线程会释放CPU。这只是起步参考实际必须结合压测调整。任务队列的选择同样关键。LinkedBlockingQueue无界队列可能导致最大线程数形同虚设——任务永远进队列不触发额外线程创建积压任务多了内存先扛不住。ArrayBlockingQueue有界队列能控制积压上限但队列满了之后触发拒绝策略。实际业务中如果你要求快速响应队列容量要小一点允许任务更早走到增加线程的分支如果你更看重稳定积压队列可以大一些。拒绝策略默认是AbortPolicy也就是直接抛RejectedExecutionException。很多线上故障就是没配置拒绝策略高并发下任务被静默丢弃。我惯用的做法是自定义拒绝策略将无法执行的任务降级到告警日志并写入待重试队列宁可延迟处理不能无感知丢失。8.2 synchronized与ReentrantLock的深度对比synchronized和ReentrantLock都能实现线程同步但底层机制完全不同。synchronized是JVM层面的关键字JDK 6之后引入了偏向锁、轻量级锁、重量级锁的升级路径性能已经很好ReentrantLock是JDK层面的API需要手动加锁解锁但提供了更多功能可中断等待、可轮询锁、公平锁、多个条件变量。从实用的角度给个建议绝大多数场景优先用synchronized。因为它在代码块结束时由JVM自动释放锁不会出现死锁而且JVM内部的锁优化都是针对它的没有特殊需求不必为了性能更好去换成ReentrantLock。我见过有人嫌弃synchronized性能差全项目换ReentrantLock结果有一处忘了在finally中unlock()线上直接卡死排查了两天才定位到。需要用到ReentrantLock的场景通常有明确特征需要尝试获取锁tryLock()带超时时间需要可中断的锁获取需要公平锁按线程等待顺序分配锁需要多个Condition精确唤醒某一类等待线程对比一下synchronized的wait/notify只能唤醒一个或全部线程而Condition可以做到定向唤醒这是复杂并发协作的关键工具。关于锁和性能的关系我最后想强调一点锁竞争是性能杀手但消灭锁更危险的可能是数据错乱。复习并发知识时我建议把安全放在第一位性能优化排第二。不要一上来就上无锁数据结构或者ThreadLocal裸奔等压测暴露问题了再针对性优化这个顺序不要搞反。9. JVM基础内存区域、类加载与GC概述9.1 运行时数据区划分与常见内存溢出场景JVM基础知识不是JavaSE的核心语法但进阶复习必须带一笔因为很多疑难杂症最终都沉淀到内存上。运行时数据区按线程共享与否划分线程共享的有堆和方法区JDK 8之后方法区的实现叫元空间Metaspace线程私有的有虚拟机栈、本地方法栈、程序计数器。堆是GC的主战场几乎所有对象实例都在这里分配。如果堆内存不足且对象无法回收抛出OutOfMemoryError: Java heap space。而JDK 8后字符串常量池等从永久代移到了堆里永久代则由元空间替代元空间使用的是本地内存默认不受JVM堆大小限制这解决了很多PermGen溢出问题但如果加载的类无限多元空间同样会爆报错是OutOfMemoryError: Metaspace。排查时先看是哪个区域溢出再决定加堆内存还是查泄漏。9.2 类加载的双亲委派与JVM参数配置实战类加载机制里的双亲委派模型是一个类加载器收到加载请求后先让父类加载器尝试加载父类加载不了才自己加载。这保证了核心类库比如java.lang.String不会被随意覆盖。复习时你只要记住一个场景就够了为什么你写个包名java.lang的类不会生效因为引导类加载器会先去加载真正的JDK类你的自定义类根本没机会被加载。JVM参数配置算是个实用技能。启动一个Spring Boot应用常用参数组合是这样java -Xms512m -Xmx512m -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m -jar app.jar-Xms和-Xmx分别是堆初始大小和最大大小线上一般设为相同值减少运行时扩容带来的性能抖动。-Xmx不要盲目设置过大要考虑机器总内存和别的进程预留。如果你的应用有大对象、高并发建议加上-XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/path/to/gc.log记录GC日志排查性能问题时会很有用。GC算法里G1在JDK 9后成为默认收集器目标是可预测的停顿时间。它的核心思路是把堆划分为多个Region根据回收价值优先回收收益高的Region。会看G1日志是排查线上性能问题的基本功至少要知道to-space exhausted表示分配担保失败humongous allocation表示大对象分配频繁这两个信息能帮你快速定位问题方向。需要说明的是JVM这块水很深进阶复习不需要达到调优专家的水平但至少做到能看懂内存溢出日志的堆栈能说出常用参数的含义能判断是内存泄漏还是内存不足这就足够应对绝大多数开发场景了。如果碰到每次重启后运行几天内存就涨满的问题先想办法拿堆转储再用MAT或JProfiler分析对象引用链比猜要有效得多。10. 一套JavaSE进阶复习路线的个人建议复习到最后我想给你一套我自己实践过的路线按这个顺序走效果比乱翻书好很多。先复习语言基础部分包括接口与抽象类、内部类、异常体系目标是能说清楚Java设计者为什么这样设计。然后进入集合框架一边看源码一边画结构图重点是把ArrayList、HashMap、ConcurrentHashMap的底层实现吃透。接着是泛型和反射这两个知识经常一起考也是很多框架的基石建议写一个小工具实践一下比如用反射注解实现一个简易字段校验器。然后IO和NIO不用深挖Netty源码但要知道BIO/NIO/AIO的模型差异和适用场景。最后是并发和JVM这两块内容偏多优先掌握线程池参数、锁机制、内存区域划分和常用GC参数。再提一个很多人忽略的点复习JavaSE进阶知识一定动手改代码。看一遍源码觉得懂了和你自己动手重写一遍、故意制造一个并发bug、调一次JVM参数效果完全不一样。我在复习时就经常干一件事把集合类改成自己的实现跑一遍测试用例很多隐藏细节比如扩容的边界条件、HashMap的threshold计算就自然暴露出来了。除此之外我建议根据自己的薄弱项做笔记。做一个.md笔记文件每个主题拆成三栏核心概念、我的理解、踩过的坑。不要直接抄博客用自己的话写写不出来的地方就是你还没理解透的地方。这个习惯帮我保持技术积累持续有效远比存储一堆收藏夹里的文章有用得多。这份复习分享就是基于我个人的JavaSE进阶知识梳理写成的里面涉及到的代码、参数和排查思路都是实测或实际项目里验证过的。如果你也在复习JavaSE希望这份内容能帮你少走一些弯路。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/9/9 21:59:09
Docker 部署 Claude 浏览器自动化应用:browser-use-demo 容器化上手指南
2026/9/9 21:59:09
内容互动链路与查询聚合:每日统计表的架构设计与性能优化
2026/9/9 21:54:09
模具制造中的高精度3D扫描测量:从全型面检测到数字化交付
2026/9/9 22:39:13
airi 项目实战:用 @vueuse/rxjs 的 from 与 fromEvent 打通 Vue 响应式与 RxJS 流
2026/9/9 22:39:13
TensorFlow Lite 如何用 CMake 为 ARM 设备构建 tflite-runtime Python 包?
2026/9/9 22:39:13
页面卡顿怎么办?Web Workers 如何用 postMessage 把重计算移出主线程
2026/9/9 22:39:13
Orca 如何用 Agent Feed 统一查看所有 worktree 的 Agent 完成与阻塞事件
2026/9/9 22:39:13
magic editing AI修图实战:从单图处理到批量落地
2026/9/9 22:34:13
Vue3组件实例属性完全指南:从$refs到defineExpose的实战解析
2026/9/9 0:00:26
MHS模型硬件标准:让大模型像调用软件一样控制物理设备
2026/9/9 0:00:27
AI五大核心方向详解:从机器学习到大模型,零基础转行选哪条?
2026/9/9 0:00:27
从50行最小循环到生产级AI引擎:工程化改造全解析
2026/9/9 2:07:00
超人会飞不算本事:系统稳定依赖清晰规则与边界设计
2026/9/9 1:41:51
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
2026/9/9 5:25:52
基于CNN的调制信号识别:MATLAB实现时频图分类实战