ArrayList、线程池与 JVM:一次 Java 底层复盘
ArrayList 源码:容量 10 是”懒”出来的
先看三个字段:
private static final int DEFAULT_CAPACITY = 10; // 默认容量 10private static final Object[] EMPTY_ELEMENTDATA = {}; // 指定容量为 0 时用private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 无参构造用transient Object[] elementData; // 真正装数据的数组两个空数组长得一模一样,作用是区分「调用方显式给了 0」和「压根没给容量」,第一次 add 时好判断要不要按 10 来扩。
构造函数的两种情况:
// 无参构造:默认创建空集合,一个数组都没分配public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}
// 指定初始容量:直接 new 一个 Object[]public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); }}所以「初始容量 10」不是构造时就有的:无参 new 出来 size 是 0,elementData 指向共享空数组,第一次 add 才触发扩容,把数组撑到 10。
扩容的核心在 grow():
private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 原容量 + 右移一位(÷2) = 1.5 倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 1.5 倍不够就用最小需求值 // ... elementData = Arrays.copyOf(elementData, newCapacity); // 数组拷贝}右移一位就是除以 2,所以是 1.5 倍而不是 2 倍。扩容靠 Arrays.copyOf 把旧数组整个拷一遍,数据量大时开销不小——能预估规模就 new ArrayList<>(size),一步到位。
asList 和 toArray:一对正好相反的坑
Arrays.asList 只是包装,底层没有拷贝,两边指向同一块内存:
Integer[] arr = {1, 2, 3};List<Integer> list = Arrays.asList(arr);
arr[0] = 99;System.out.println(list.get(0)); // 99 ← 改数组,list 跟着变
list.set(1, 100);System.out.println(arr[1]); // 100 ← 改 list,数组也变
list.add(4); // UnsupportedOperationException反过来,list.toArray() 是真拷贝,互不影响:
List<String> list = new ArrayList<>();list.add("a");Object[] array = list.toArray();
array[0] = "b";System.out.println(list.get(0)); // a ← 拷贝出来的新数组,改了不影响原 list一个共享引用,一个数组拷贝,方向别记反。顺带一个经典坑:Arrays.asList(new int[]{1, 2, 3}) 的 size 是 1——int[] 本身被当成了一个对象装进去,想按元素转得用 Integer[]。
线程池核心线程数怎么定
| 业务特征 | 线程数 | 直觉 |
|---|---|---|
| 计算密集型 | CPU 核数 + 1 | CPU 一直在转,线程再多只会互相切换;+1 是为了偶发暂停时有人顶上 |
| 高并发、任务执行时间短 | CPU 核数 + 1 | 任务切换本来就频繁,线程多了上下文切换的开销反而吃掉吞吐 |
| IO 密集型 | CPU 核数 × 2 + 1 | 大部分时间在等 IO,CPU 闲着,多线程正好填补等待空隙 |
| 并发不高、任务执行时间长 | 按 IO / CPU 归到上面两条 | 并发低,线程再多也跑不满,按任务类型定就够 |
| 并发高、业务执行时间长 | 不在线程池 | 瓶颈在架构:缓存、异步、MQ、拆服务 |
最后一条最容易被忽略:线程池参数调不出架构的账。
JVM 运行时数据区
线程私有三件套:程序计数器、虚拟机栈、本地方法栈,随线程生、随线程灭。线程共享两块:堆和方法区。
堆里分年轻代(Eden + 两个 Survivor)和老年代。方法区在 1.7 由永久代实现(放在堆里),1.8 起换成了元空间——搬到本地内存,不再挤占堆。方法区存的是类的信息、运行时常量池这类元数据,虚拟机启动时创建,关闭时释放。
类加载器与双亲委派
类加载器负责装载字节码文件,把字节码加载进 JVM。加载一个类时的委派流程:
loadClass("com.xxx.Foo") → 先问上级:parent.loadClass() → 上级还有上级,继续向上(直到 BootstrapClassLoader) → 一路都没加载过,才轮到自己 findClass()这个机制叫双亲委派模型,两个作用:
- 唯一性:同一个类只会被加载一次,不会出现两份 java.lang.String;
- 安全:核心类库永远由 Bootstrap 加载,自己写一个同名 String 也不会被用上,类库 API 没法被顶替。
什么算垃圾
引用计数法:对象被引用一次计数 +1,引用失效 -1,归零就回收。实现简单,但循环引用直接让它失效——a 引用 b、b 引用 a,外部引用全断,两边计数也永远是 1,永远回收不掉。
JVM 实际用的是可达性分析:从 GC Roots 出发,能到达的对象就是活的,到不了的全是垃圾。
GC Roots 通常是这几类:
- 虚拟机栈中引用的对象(栈帧里的局部变量);
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI 引用的对象。
三大回收算法
| 算法 | 思路 | 优点 | 缺点 | 典型去处 |
|---|---|---|---|---|
| 标记-清除 | 可达性分析标记存活对象,直接清掉未标记的 | 快 | 碎片化严重,内存不连续 | 老年代(CMS) |
| 标记-整理 | 标记后把存活对象往一端挪,清掉边界外的 | 无碎片 | 移动对象成本高 | 老年代(Parallel Old) |
| 复制 | 内存分两块,存活对象复制到另一块,原块整体清空 | 无碎片、垃圾越多效率越高 | 牺牲一块空间,存活对象多时不划算 | 新生代 |
新生代用复制算法不是没道理:大部分对象朝生夕死,真正活着要搬家的没几个,配上 Eden : s0 : s1 = 8 : 1 : 1 的比例,一次只浪费十分之一的内存。
JVM 调优
Tomcat 部署改 bin/catalina.sh 里的 JAVA_OPTS;jar 包部署直接写在启动命令里:
java -Xms4g -Xmx4g -Xss1m -XX:SurvivorRatio=8 -XX:+UseParallelGC -XX:+UseParallelOldGC -jar app.jar常用参数:
| 参数 | 作用 | 默认值 |
|---|---|---|
| -Xms | 初始堆大小 | 物理内存的 1/64 |
| -Xmx | 最大堆大小 | 物理内存的 1/4 |
| -Xss | 单个线程的虚拟机栈大小 | 一般 1M |
| -XX | Eden 与两个 Survivor 区的比例 | 8(即 8:1<1>1>) |
| -XX:+UseParallelGC | 新生代并行收集器 | — |
| -XX:+UseParallelOldGC | 老年代并行收集器 | — |
实践里常见把 -Xms 和 -Xmx 设成一样,堆不用反复伸缩,省掉动态扩容的抖动。
排查工具一行一个:
jps -l # 列出 Java 进程,拿到 pidjstack <pid> # 打线程栈,查死锁、CPU 飙高jmap -heap <pid> # 看堆的使用概况jmap -dump:format=b,file=heap.hprof <pid> # 导出堆快照jhat 也能分析快照,但 JDK 9 之后就被移除了,现在多用 VisualVM 或 MAT。可视化工具:jconsole、VisualVM。
重点速记
- ArrayList 容量 10 是懒加载:无参构造只是空数组,第一次 add 才扩到 10
- 扩容 1.5 倍:oldCapacity + (oldCapacity >> 1),靠 Arrays.copyOf 拷贝
- asList 是包装、共享原数组(set 互通、add 抛异常);toArray 是拷贝,互不影响
- CPU 密集 / 高并发短任务:核数 + 1;IO 密集:核数 × 2 + 1;高并发长业务看架构,别指望线程池
- 线程私有:程序计数器、虚拟机栈、本地方法栈;线程共享:堆、方法区
- 1.7 永久代在堆里,1.8 换成元空间(本地内存)
- 双亲委派:先向上委托,上级不加载才自己来;保唯一、保安全
- 引用计数死于循环引用,JVM 用可达性分析 + GC Roots
- 新生代复制算法(8:1<1>1>),老年代标记-清除 / 标记-整理
- 调优参数:-Xms/-Xmx(堆)、-Xss(栈)、-XX
(Eden 与 S 区比例)
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





















