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的对象
- 当前正在执行的方法里的局部变量和输入参数
- 活动线程
- 所有类的静态字段
- 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 停顿时间。
- 不对老年代进行整理,而是使用空闲列表来管理内存空间的回收
- 在mark-and-sweep的工作和业务线程并发执行。
默认并发线程数等于CPU核心数的1/4
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的时间和分布变成可预期和可配置的,可设置某项特定的性能指标,为了达成可预期的指标,有独特的实现。增量方式,每次处理一部分,称为回收集合,每次处理所有的年轻代和部分老年代。能看到哪个块的垃圾多,优先回收他们
处理步骤
- 年轻代模式转移暂停
- 并发标记
- 转移暂停:混合模式
G1GC可能退化成串行GC
- 并发模式失败:增加堆大小
- 晋升失败:
- 巨型对象分配失败:增加内存或增大Region大小
优点:
- 减少内存碎片化。
- 更好地控制 GC 停顿时间,可通过 -XX:MaxGCPauseMillis 调整。
- 自动调节年轻代和老年代的大小。
缺点:
- 实现复杂,配置选项多。
- 内存占用较高,CPU 开销大。
适用场景:
- 需要低延迟的中大型应用。
- 堆内存较大的环境(如 >4GB)。
- 替代 CMS 的推荐选择。
- 如果系统考虑吞吐优先,CPU资源用来处理业务,用Parallel GC
- 如果系统考虑低延迟优先,每次GC时间尽量短,用CMS GC
- 如果系统内存堆大,平均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时间更长。
一般来说过早提升的症状表现为以下形式:
- 短时间内频繁的执行full GC
- 每次full GC后老年代的使用率都很低,在10%-20%或以下。
- 提升速率接近分配速率
解决方案
- 增加年轻代大小。-Xmx1g -XX:NewSize=512M,FullGC的次数就会减少
- 减少每次批处理的数量
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算法
触发条件,达到下面的条件才会真正向网络发送数据
- 缓冲区满
- 达到超时
优化
- 不要阻塞EventLoop
- 系统参数优化
- 缓冲区优化
- 心跳周期优化
- 内存和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:服务编排,网关
- 暴露调用
- 增强和中介
- 统计和监控
缺点是中心节点,增加一层
分布式服务化:用配置和注册发现代替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开始事务