dream

一个菜鸟程序员的成长历程

0%

算法导论

jvm核心

基础知识

jvm基础知识

java字节码

Java bytecode由单字节的指令组成,类似汇编,java使用了200多个操作码

  • 栈操作指令
  • 程序流程控制指令
  • 对象操作指令
  • 算术运算以及类型转换指令

javap可以看到字节码文件

1
javap -c -verbose ...

astore_1 代表store 本地变量表的slot 1,a代表引用类型
istore代表int store

算数操作

类型转换

方法调用指令

  • invokestatic: 调用静态方法
  • invokespecial: 用来调用构造函数,或同一个类中的private方法,以及超类方法
  • invokevirtual: 调用公共,受保护和package级的私有方法类似c的虚函数
  • invokeinterface: 调用接口方法
  • invokedynamic: lambda实现基础
  • new 创建对象
  • getfield: 获取对象的实例字段
  • putfield: 设置对象的实例字段

栈操作指令

  • pop:将栈顶的一个数值弹出(丢弃)。
  • dup:复制栈顶的一个数值。
  • swap:交换栈顶的两个数值。

条件跳转指令

  • if_icmpeq:如果栈顶两个int值相等,则跳转。
  • if_icmpne:如果栈顶两个int值不相等,则跳转。
  • goto:无条件跳转到指定位置。
  • tableswitch:根据值选择跳转位置,用于switch语句。

同步指令

  • monitorenter:进入同步代码块。
  • monitorexit:退出同步代码块。

JVM类加载器

引导类加载器

  • 引导类加载器是最顶层的类加载器,负责加载 Java 核心库中的类(例如 java.lang.* 包中的类)。这些类一般位于 JRE 目录下的 rt.jar 文件中。
  • 它是由 C++ 编写的,直接由操作系统加载,因此没有 Java 类的实现。
  • 它默认从 JRE/lib 路径下加载类。

扩展类加载器

  • 扩展类加载器负责加载 Java 扩展库中的类。这些类通常位于 JRE/lib/ext 目录下,或者由 java.ext.dirs 系统属性指定的其他路径。
  • 它是由 Java 编写的,属于标准的类加载器。
  • 它默认从 JRE/lib/ext 目录和 java.ext.dirs 属性指定的路径加载类

系统类加载器

  • 系统类加载器负责加载应用程序的类,也就是由用户编写的类。它通常从类路径(classpath)中加载类。
  • 它是由 Java 编写的,通常是 ClassLoader 类的一个实例。用户可以指定 classpath 来告诉系统类加载器从哪里加载类。
  • 它根据环境变量 CLASSPATH 或 -cp 命令行参数指定的路径加载类。

自定义类加载器

  • Java 还允许程序员创建自定义类加载器,来自定义类加载的行为。自定义类加载器可以扩展 ClassLoader 类并重写其 findClass() 方法,来实现从不同的数据源加载类,比如从数据库、网络等加载类。
  • 自定义类加载器在很多场景中有用,比如在应用服务器中动态加载插件,或者从不常规的源(如数据库、网络等)加载类。

类的生命周期

  • 加载: 找class文件,类加载器通过 findClass() 方法从指定的位置查找 .class 文件。加载器会将 .class 文件转化为二进制字节流。
  • 验证: 验证格式,依赖。验证阶段确保加载的 .class 文件格式正确,并且不包含任何有可能危害虚拟机安全的代码。
  • 准备: 静态字段,方法表。准备阶段分配内存并为类变量(静态字段)设置默认值(如 0、null 等)。
  • 解析: 符号解析为引用。解析阶段将类中的符号引用(例如字段、方法、类的名字等)解析为实际的内存地址或方法引用。
  • 初始化: 构造器,静态变量赋值,静态代码块。初始化阶段是类加载过程的最后一步,它会执行类的静态初始化块(static 块)和静态字段的初始化。
  • 使用
  • 卸载

图片

JVM 中的类加载器采用了父子委托机制(Parent Delegation Model)。当一个类加载器收到加载某个类的请求时,它不会直接去加载类,而是首先将请求委托给父类加载器。如果父类加载器无法加载,则再由当前加载器进行加载。这种机制的好处是可以保证 Java 核心类库不会被用户定义的类覆盖。

JVM内存模型

java栈

  • 线程私有:存储线程的本地变量信息等
  • 每个线程在执行方法时,都会在栈中创建一个栈帧(stack frame)。栈帧包含方法的局部变量、操作数栈、动态链接、方法返回地址等信息。栈的大小通常由 -Xss 参数进行控制。
  • 局部变量的作用范围仅限于当前方法,存储在栈帧中。

本地方法栈

  • 用于执行本地方法(即通过 JNI 调用的 C/C++ 等语言编写的代码)。和 Java 栈类似,本地方法栈也包含栈帧,但栈帧里存储的是本地方法的相关信息。
  • 每个线程有自己的本地方法栈。

java堆

  • 共有
  • 存储动态信息,如对象数据,包括对象的成员变量,成员函数,静态变量。因此多线程访问堆中的数据时需要进行同步,以保证数据的一致性和可见性。
  • 堆是垃圾回收的主要区域。
  • 垃圾回收器负责堆内存的管理,通过标记清除、复制回收、分代回收等算法来回收无用对象的内存。

方法区

  • 方法区用于存储类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。在 Java 7 之前,方法区和永久代(PermGen)是一个概念,但在 Java 8 中,永久代被 Metaspace(元空间)替代,Metaspace 位于本地内存中,而非堆中。
  • 方法区是所有线程共享的,因此多线程访问时也需要注意同步。
  • 存储类的元数据(类的结构、常量池等)、静态变量、方法等。

运行时常量池

  • 运行时常量池是方法区的一部分,用于存储编译期间生成的字面量和符号引用(例如字符串常量和类、方法、字段的引用)。
  • 常量池是所有线程共享的,所有类共享相同的常量池。

直接内存

  • 直接内存并不是 JVM 内存模型的一部分,但它是通过 java.nio 包提供的 ByteBuffer 类实现的内存区域。直接内存可以绕过 JVM 的堆和垃圾回收机制,直接与操作系统的内存交互,用于提高 I/O 性能。
  • 直接内存可以由多个线程共享,但需要开发者手动管理。

图片

图片

堆内存图片

堆内存

  • 年轻代
    • 新生代
    • 存活区: 一般有两个
  • 老年代

非堆

  • 元数据:以前是永久代
  • CCS: 存放class信息,和元数据有交叉
  • code cache: 存放JIT编译后的本地机器代码

可见性

  • 当多个线程访问同一个共享变量时,一个线程对变量的修改可能对其他线程不可见。JMM 规定了变量的可见性,确保一个线程对共享变量的修改能够及时地被其他线程看到。

JVM启动参数

-开头是标准参数,如-server
-D设置系统属性,如-Dfile.encoding=UTF-8
-X开头是非标准参数,基本都传给JVM的,默认JVM实现这些参数的功能。可以jvm -X查看支持的参数。如-Xmx8g
-XX开头是非稳定参数,专门控制jvm的行为,根具体的jvm实现有关

  • -XX:+-Flags形式,+-是对布尔值进行开关。+代表开,-代表关。如-XX:_UseG1GC
  • -XX:key=value形式,指定某个选项的值.如-XX:MaxPermSize=256m

JDK内置工具

图片

jps/jinfo 查看java进程
jstat 查看jvm gc信息
jmap 查看heap或类占用空间统计
jstack 查看线程信息
jcmd 执行jvm相关分析命令
jrunscript/jjs 执行js命令

jstat -gc pid xxx

图片

jmap -heap 查看堆信息

jcmd可以查看所有信息

jconsole

jvisulVM

jMC

GC

分代假设:大部分新生对象很快无用,存活较长时间的对象,可能存活更长时间。

经历了15次GC还存在的就放在老年代

新生代80%,S0,S1各10%
新生代到存活区是复制,到老年代是移动
-XX: +MaxTenuringThreshold=15标记15次后放到老年代

可以做为GC ROOT的对象

  1. 当前正在执行的方法里的局部变量和输入参数
  2. 活动线程
  3. 所有类的静态字段
  4. JNI引用

需要记录夸代的依赖信息

Serial GC/ParNewGC

-XX: +UseSerialGC开启

对年轻代使用mark-copy算法,对老年代使用mark-sweep-compact算法

串行GC不能并行处理,所以触发全部暂停(STW)

ParNewGC可以配合CMSGC使用

适用场景:

  • 单线程应用或资源有限的环境(如嵌入式系统)。
  • 小型应用,不需要频繁的垃圾回收。

Parallel GC

目标是最大化应用程序运行时间(吞吐量),最小化 GC 时间。还是会短暂的暂停业务。需要业务能接受短暂的暂停。

-XX: +UseParallelGC
-XX: +UseParallelOldGC

年轻代和老年代GC都会触发STW事件。

对年轻代使用mark-copy算法,对老年代使用mark-sweep-compact算法

-XX: ParallelGCThreads=N 来指定GC线程数,默认值为CPU核心数。

优点:

  • 多线程回收显著提高了回收效率,适合多核环境。
  • 停顿时间较 Serial GC 短。
  • 两次GC之间不消耗系统资源。

缺点:

  • GC 停顿仍然是全暂停(STW)。
  • 不适合对延迟要求苛刻的场景。

适用场景:

  • 需要高吞吐量的大型后台任务(如批处理、数据分析)。
  • 多核 CPU 环境。

CMS GC

-XX: +UseConcMarkSweepGC

对年轻代使用STW的mark-copy算法,对老年代主要使用并发的mark-sweep算法

设计目标:专为老年代设计,目标是最小化 GC 停顿时间。

  1. 不对老年代进行整理,而是使用空闲列表来管理内存空间的回收
  2. 在mark-and-sweep的工作和业务线程并发执行。

默认并发线程数等于CPU核心数的1/4

6个阶段

  1. 初始标记
  2. 并发标记
  3. 并发预清理
  4. 最终标记
  5. 并发清楚
  6. 并发重制

MaxHeapSize是系统的1/4内存
MaxNewSize是MaxHeapSize的1/3
NewSize是系统的1/64

优点:

  • 只有yongGC暂停业务。GC 停顿时间短,适合延迟敏感的应用。
  • 并发回收利用多核资源减少 STW 时间。

缺点:

  • 内存碎片化:CMS 不会整理内存,可能导致分配大对象失败(触发 Full GC)。
  • CPU 开销较高:并发阶段可能与用户线程争抢资源。
  • 容易产生 “Concurrent Mode Failure”:若老年代空间不足,回退到 Serial GC。

适用场景:

  • 延迟敏感的应用(如 Web 服务、在线交易系统)。
  • 多核环境下的中大型应用。

G1 GC

分区堆内存:将堆划分为若干独立的固定大小的区域(Region),每个 Region 可充当年轻代、老年代或其他用途。混合回收:通过优先回收包含最多垃圾的 Region(Garbage-First)。并行和并发回收:减少 STW 时间。内置碎片整理机制,避免了 CMS 的碎片化问题。

-XX: +UseG1GC -XX: MaxGCPauseMillis=50

将STW的时间和分布变成可预期和可配置的,可设置某项特定的性能指标,为了达成可预期的指标,有独特的实现。增量方式,每次处理一部分,称为回收集合,每次处理所有的年轻代和部分老年代。能看到哪个块的垃圾多,优先回收他们

处理步骤

  1. 年轻代模式转移暂停
  2. 并发标记
  3. 转移暂停:混合模式

G1GC可能退化成串行GC

  1. 并发模式失败:增加堆大小
  2. 晋升失败:
  3. 巨型对象分配失败:增加内存或增大Region大小

优点:

  • 减少内存碎片化。
  • 更好地控制 GC 停顿时间,可通过 -XX:MaxGCPauseMillis 调整。
  • 自动调节年轻代和老年代的大小。

缺点:

  • 实现复杂,配置选项多。
  • 内存占用较高,CPU 开销大。

适用场景:

  • 需要低延迟的中大型应用。
  • 堆内存较大的环境(如 >4GB)。
  • 替代 CMS 的推荐选择。
  1. 如果系统考虑吞吐优先,CPU资源用来处理业务,用Parallel GC
  2. 如果系统考虑低延迟优先,每次GC时间尽量短,用CMS GC
  3. 如果系统内存堆大,平均GC时间可控,使用G1 GC

ZGC

专注于低延迟,目标是将 GC 停顿时间控制在 10ms 以下。支持非常大的堆内存(TB 级)。使用多线程并发回收,避免长时间的 STW。基于标记-整理算法,避免内存碎片化。使用指针染色(Pointer Coloring)来实现并发标记和引用更新。

-XX: +UnlockExpermentalVMOptions
-XX: +UseZGC
-XX: -Xmx16g

优点:

  • 极低的 GC 停顿时间。
  • 支持超大堆,扩展性好。
  • 减少内存碎片。
  • 与G1相比,应用吞吐量下降不超过15%

缺点:

  • 内存占用较高:由于需要染色指针和写屏障。
  • 不适合资源紧张的环境。

-XX: +UseShenandoahGC

立项比ZGC早,暂停时间与堆大小无关

适用场景:

  • 延迟敏感的大型应用(如金融交易、高并发系统)。
  • 超大堆应用(TB 级别内存)。

Shenandoah GC

目标是极低延迟,尽可能减少 STW 时间。采用并发压缩技术(Concurrent Compaction),避免碎片化问题。大部分回收工作与应用线程并发执行。

优点:

  • GC 停顿时间低。
  • 内存整理避免碎片化。
  • 对堆内存的需求比 ZGC 更低。

缺点:

  • 较新的 GC,生态和调优支持可能不如 G1 和 ZGC。
  • CPU 开销高。

适用场景:

  • 延迟敏感的应用。
  • 中大型堆内存的场景。

GC日志分析

JVM线程堆栈数据分析

图片

JVM内部线程

  • VM线程:单例的VMThread对象,负责执行VM操作
  • 定时任务线程:单例的WatcherThread对象,模拟计时器中断。
  • GC线程:垃圾回收
  • 编译器线程:将字节码编译为本地机器代码
  • 信号分发线程:等待进程指示的信号,并将其分配给java级别的信号处理

安全点

  • 方法代码中植入检测入口
  • 线程处于安全点状态:线程暂停执行,这个时候线程栈不再发生改变
  • JVM的安全点状态:所有线程都处于安全点状态

fastthread

内存分析

Instrumentation.getObjectSize()

JOL可以看到内存布局

64位JVM中对象头占12byte,8字节对齐。Integer占16字节(8+4+对齐),int占4字节。Long占16字节,long占8字节。

String对象的空间随着内部字符数组的增长而增长。String类的对象有24个字节的额外开销。

jhat

JVM问题分析调优

分配速率:两次GC之间的年轻代使用量。也就是创建速度,流入速度。

  • 正常系统:分配速率较低 < GC速率: 健康
  • 内存溢出: 分配速率 > GC速率: OOM
  • 性能不好:分配速率较高 < GC速率: 亚健康,可能随时导致内存溢出

提升速率:从年轻代到老年代的数据量。

  • 有一些要被干掉的放到了老年代,老年代GC时间更长。

一般来说过早提升的症状表现为以下形式:

  1. 短时间内频繁的执行full GC
  2. 每次full GC后老年代的使用率都很低,在10%-20%或以下。
  3. 提升速率接近分配速率

解决方案

  1. 增加年轻代大小。-Xmx1g -XX:NewSize=512M,FullGC的次数就会减少
  2. 减少每次批处理的数量

Arthas工具

GC疑难情况

JVM常见面试题

NIO

java socket

java.net.ServerSocket类

server处理发生了什么

IO模型和相关概念

阻塞非阻塞

同步异步

netty

高性能

  • 高并发
  • 低延迟:延迟指服务端运行时间
  • 高吞吐量

netty如何实现高性能

  • 异步
  • 事件驱动
  • 基于NIO

粘包和拆包,主要通过解码器来解决,比如抄答案的时候两个多选题,答案写的abcd,你不知道是1.ab 2.cd还是1.a 2.bcd还是1.abc 2.d,所以中间需要来个标点符号区分。

  • Fixed:固定长度解码器
  • LineBased:行分隔符,\n or \r\n
  • Delimiter: 分隔符解码器,可以自定义分隔符
  • LengthField: 长度编码解码器,划分为包文头,包文体
  • Json: JSON解码器

网络拥堵和Nagle算法。TCP_NODELAY 可以关掉Nagle算法
触发条件,达到下面的条件才会真正向网络发送数据

  • 缓冲区满
  • 达到超时

优化

  1. 不要阻塞EventLoop
  2. 系统参数优化
  3. 缓冲区优化
  4. 心跳周期优化
  5. 内存和buffer优化

并发

start以后陷入JVM线程,然后OS线程,最后TLAB,准备好了运行以后开始运行状态,run状态。

重要属性

  • 线程名称
  • 是否后台执行
  • 任务
  • start 启动
  • join 等待执行完成
  • currentThread 静态方法,获取当前线程信息
  • sleep 静态方法,线程睡眠并让出时间片
  • wait 释放锁,等待
  • notify 唤醒一个
  • notifyAll 唤醒所有
  • yield 让出cpu资源

原子性
可见性
有序性

图片

线程池

  • 核心线程数
  • 最大线程数
  • 队列
  • 拒绝策略

为什么核心线程数满了以后先放到队列里面?因为要平衡CPU密集型应用和IO密集型应用

拒绝策略

  • 丢弃任务抛出异常
  • 丢弃任务不抛出异常
  • 丢弃队列最前面的任务,重新提交被拒绝的任务
  • 由调用线程处理该任务

线程池创建

  • Executors.newSingleThreadExecutor : 仅仅创建一个线程的线程池
  • Executors.newFixedThreadPool:固定大小的
  • Executors.newCachedThreadPool:可缓存的线程池,如果线程超过了执行任务所需的,60s不执行任务的会被回收,任务增加的时候可以智能增加线程数,线程大小依赖于操作系统
  • Executors.newScheduledThreadPool:支持定时以及周期性执行

核心线程数,如果CPU密集型,可以N+1或者N,如果IO密集型,可以2N或者2N+2

  • Sync

  • ReentrantLock

  • ReadWriteLock

  • Condition

  • 可重入锁

  • 公平锁

LockSupport 类似Executors

  • park
  • parkNanos
  • unpark

三个用锁的最佳实践

  • 永远只在更新对象的成员变量时加锁
  • 永远只在访问可变的成员变量时加锁
  • 永远不在调用其他对象的方法时加锁

原子类

通过volitale和CAS实现

LongAdder对AtomicLong的改进

JUC

AQS

Semaphore

准入数量 信号量

CountDownLatch

CyclicBarrier

arrayList

默认大小10,扩容1.5, new = old + (old >> 1)

线程不安全

写写冲突
读写冲突 iterator

linkedList

链表实现不需要扩容

线程不安全

CopyOnWriteArrayList

写时复制线程安全,写加锁

Vector

加锁的ArrayList

List.asList

只能set,不能添加删除元素

Collections.SyncList

加锁的ArrayList

Collections.noModifyList

不可插入删除set的list

HashMap

初始容量16,扩容*2,负载因子0.75, jDK8以后,链表长度8,数组长度64以后,改成红黑树

  • 写冲突
  • 读写问题
  • keys()无序问题

LinkedHashMap

继承HashMap, 增加了双向链表,保证有序

ConcurrentHashMap

线程安全 JDK7用的extendible hash 实现,分段

JDK8改成一个大叔组

ThreadLocal

线程本地变量
不改方法签名静默传参
及时进行清理

线程通信

static变量
Lock
Sync

协作

  • Thread#join();
  • Object#wait/notify/notifyall
  • Future/Callable
  • CountdownLatch
  • CyclicBarrier

进程间通信

分布式服务

服务治理

集群:多个相同服务,对等的关系
分布式:有角色关系,多个服务处理不一样的东西

服务汇聚到ESB:服务编排,网关

  1. 暴露调用
  2. 增强和中介
  3. 统计和监控

缺点是中心节点,增加一层

分布式服务化:用配置和注册发现代替ESB单节点,使请求直连

有状态的部分放到配置中心和注册中心,无状态的部分放到应用侧。比如RPC放到应用侧。

配置中心:全局非业务参数
注册中心:运行期临时状态
元数据中心:核心的业务模型

队列

两种消息模式:

  • 点对点
  • 发布订阅

消息处理的保障

  • At most once,至多一次,只发一次,消息丢失了也不重发
  • At least once, 至少一次,消息不回丢失,可能会重复
  • Exactly once, 精确一次,每条消息肯定会被传输一次且仅一次,不丢失不重复

消息处理的事务性

  • 通过确认机制实现事务性
  • 可以被事务管理器管理,甚至可以支持XA分布式事务

有序性

  • 同一个Topic或Queue的消息,保障顺序
  • 消息分区,批量预取等可能会无序

书籍企业集成模式

消息协议

  • AMQP:规定了序列化,网路传输等
  • MQTT:遥感常用
  • JMS:java规定的接口
  • XMPP: IM用的

kafka
ack = 0 ,不管成功没
ack = 1, 写到leader就成功
ack = -1/all 写到最小副本数才算成功

顺序保证
per.connetion = 1
send()
flush()

消息可靠性

  • 消息的事务,发送的时候要么全成功,要么全失败,ack = all,idem=true打开幂等,transaction.id tx0001设置事务ID,begin开始事务