Java知识体系
2024-06-07 20:13:52 2 举报
AI智能生成
Java知识体系思维导图
作者其他创作
大纲/内容
字节码
基础
局部变量表
操作数栈
指令
加载存储
控制跳转
运算
类型转换
案例
i++ 与 ++i 的底层
i++
++i
finally的底层
序列化
实现流程
分支主题
反序列化会破坏单例模式
原因
底层运用了反射的技术,通过反射调用
无参数的构造方法创建一个新的对象
解决方案
在类中增加私有方法readResolve
实现方式
实现Serializable接口
实现Externalizable接口
重写writeExternal和readExternal方法,它的效率比Serializable高一些,并且可以决定哪些属性需要序列化(即使是transient修饰的)但是对大量对象,或者重复对象,则效率低
疑问
序列化为什么需要实现Serializable接口
在ObjectOutputStream调用writeObject方法时,当要写入的对象是String、Array、Enum、Serializable类型的对象则可以正常序列化,否则会抛出NotSerializableException异常
String为啥就不用实现Serializable接口
String已经内部实现了Serializable,不用我们再显式实现
既然已经实现了Serializable接口,为什么还要显示指定serialVersionUID的值
因为序列化对象时,如果不显示的设置serialVersionUID,Java在序列化时会根据对象属性自动的生成一个serialVersionUID,再进行存储或用作网络传输
在反序列化时,会根据对象属性自动再生成一个新的serialVersionUID,和序列化时生成的serialVersionUID进行比对,两个serialVersionUID相同则反序列化成功,否则就会抛异常
JVM
参数
-XX:MetaspaceSize 默认21M
调优
根据请求量预估
1. 根据请求量预估,每台机器每秒钟会有多少请求,每个请求估计占用多少内存,以此得出每秒请求时新生代内存需要分配的内存,进而判断机器应该为JVM各个区域分配多少内存
使用jstat工具
使用命令 jstat -gc <pid> 1s 10 查看实际生产环境中新生代及老年代内存增长速率,YGC与FGC触发速率及耗时等指标来进行调优。尤其是请求高峰时段,日常也不能少
案例分析
1.【Full GC(Metadata GC Threshold)xxxxx, xxxxx】
分析
1. 若MetaspaceSize未设置则很有可能该区域内存过小,将该内存设置大点即可
2. 若MetaspaceSize已设置且内存充足,则出现该情况应该是因为不停的有新的类产生被加载到Metaspace区域里去,然后不停的把Metaspace区域占满,接着触发一次Full GC回收掉Metaspace区域中的部分类。
1. 增加参数 “-XX:TraceClassLoading -XX:TraceClassUnloading
用于查看是哪些类被不停的创建与卸载
2. 如在日志中找到 【Loaded sun.reflect.GeneratedSerializationConstructorAccessor from __JVM_Defined_Class】
-XX:SoftRefLRUPolicyMSPerMB=0 表示每一MB空闲内存空间可以允许SoftReference对象存活多久
2. 各个区域内存使用都正常,为什么会频繁FGC
代码显示调用 System.gc(),可使用参数关闭显式调用 -XX:+DisableExplicitGC
FGC频繁发生的可能性
1. 让Survivor空间足以容纳每次YGC或存活的
对象,尽量不要让存活的对象进入老年代
2. 若是使用CMS收集器,则让其尽量占满内存
空间后再回收,避免过早FGC
3. 主动设置永久代空间大小,防止使用反射时
动态加载的类过多引起FGC
4. 内存里驻留了大量的对象塞满了老年代,
导致稍微有一些对象进入老年代就会引发Full GC
5. 手动执行System.gc() 导致FGC
默认参数
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/app/oom
GC 日志优化
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m
运行时数据区
程序计数器
虚拟机栈
本地方法栈
堆
新生代
Eden
from Survivor
to Survivor
老年代
方法区
常量池
直接内存
JVM 执行模式
解释执行( -Xint )
混合模式 ( -Xmixed )
若函数为热点代码则会被编译执行, server 模式下函数被调用 10000 次被视为热代码,
而 client 为 1500, 可通过 -XX:CompilationThreshold 设置
编译执行 ( -Xcomp )
虚拟机常见实现方式
基于栈的指令集架构
案例
HotSpot JVM
.Net CLR
易于移植, 指令更短, 实现简单,但不能随机访问堆栈元素,完成相同功能所需的指令数一般比寄存器架构多,需要频繁出入栈,不利于代码优化
基于寄存器的指令集架构
案例
LuaVM
DalvikVM
速度快,可充分利用寄存器,有利于程序做运行速度优化,但操作数需要显式指定,指令较长
虚拟机规范
部分扩展
1. 两个线程轮流打印A,B
wait/notify
park/unpark
ReentrantLock/Condition
LinkedTransferQueue
并发编程
内存屏障
定义
编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的
1 . 确保特定操作的执行顺序
2. 影响数据在各CPU的可见性
硬件层
Intel
lfence,是一种Load Barrier 读屏障
sfence, 是一种Store Barrier 写屏障
mfence, 是一种全能型的屏障,具备ifence和sfence的能力
Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。
Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
软件层
LoadLoad屏障
禁止处理器把前面的volatile读与后面的普通读重排序
LoadStore屏障
禁止处理器把前面的volatile读与后面的普通写重排序
StoreStore屏障
确保前面所有普通写在volatile写之前刷新到主内存
StoreLoad屏障
确保前面写入的数据能够被后续指令访问前对其他处理器可见,避免volatile写与后面可能有的volatile写或读操作重排序
万能屏障,兼具其它三种内存屏障的功能,开销最大。因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。
编译器通常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障,为了保证能正确实现volatile语义,JMM采取了保守策略,即在每个volatile写后面或在每个volatile读前面插入一个StoreLoad屏障
应用
Synchronized
StoreStore
volatile
StoreLoad
Unsafe类方法
UNSAFE.putOrderedXXX
StoreStore
Unsafe.putVolatileXXX
StoreLoad屏障
happen-before原则
如果前一个操作的执行结果必须对后一个操作可见,那就不允许这两个操作进行重排序,且happen-befor具有传递性
对于两个操作 A 和 B,这两个操作可以在不同的线程中执行。如果 A Happens-Before B,那么可以保证,当 A 操作执行完后,A 操作的执行结果对 B 操作是可见的。
JMM内存模型
设计意图
1. 程序员希望基于一个强内存模型来编写代码
2. 编译器和处理器希望实现一个弱内存模型,这样可以尽可能的优化来提高性能
JMM对两种不同性质的重排序采取了不同策略
1. 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序
2. 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求
相关类
LongAdder
base变量
非竞态条件下,直接累加到该变量上
Cell数组初始化时,必须保证仅被初始化一次,
其他竞争失败的线程会将数值累加到base上
Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中
cellsBusy
锁标志,在Cells初始化或者扩容时会通过
cas操作将该标志位置为1,结束后置0
collide
扩容标志,当Cell数组长度大于CPU核心数后不再扩容,每次扩容两倍
DelayQueue
消费者线程的数量要够,处理任务的速度要快。否则,队列中的到期元素无法被及时取出并处理,造成任务延期、队列元素堆积等情况。
volatile语义增强
原因:在JSR-133旧的内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量重排序。即在旧的内存模型中,volatile的写-读没有锁的释放-获取所具有的内存语义
为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义
从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止
CAS同时具有volatile读和volatile写的内存语义
因为在多核处理器下执行 compareAndSwapInt会为 cmpxchg 指令加上lock前缀
指令加lock前缀的作用
1. 确保对内存的读-改-写操作原子执行
2. 禁止该指令与之前和之后的读和写指令重排序
3. 把写缓冲区中的所有数据刷新到主内存中
队列
阻塞队列
SynchronousQueue
LinkedListBlockingQueue
ArrayBlockingQueue
非阻塞队列
ConcurrentLinkedQueue
线程池
创建方式
Executors.newFixedThreadPool()
固定线程数量,用于执行长期任务
Executors.newSingleThreadPool()
适用于单个任务顺序执行的场景
Executors.newCacheThreadPool()
适用于执行短期异步负载较轻的服务器,线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收
重要参数
corePoolSize: 线程池中常驻核心线程数,
当线程数达到该值后,其余的任务就需要放入缓冲队列
maximumPoolSize: 线程池能够容纳同时执行的最大线程数,必须大于1
keepAliveTime:多余空闲线程的存活时间。当线程池数量超过corePoolSize时,空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到只剩下corePoolSize个线程
unit: keepAliveTime的单位
workQueue: 任务队列,被提交但尚未被执行的任务
threadFactory: 表示生成线程池中工作线程的工厂,用于创建线程一般默认即可
handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时所做出的拒绝策略
JDK内置的拒绝策略
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
CallerRunsPolicy:调用者运行,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
DiscardOldestPolicy: 抛弃队列中等待最久的任务,然后把当前任务放入队列中
DiscardPolicy: 直接丢弃任务,不予任何处理也不抛弃异常。如果允许任务丢弃,这是最好一种方案
原理
1. 创建线程池,等待任务提交
2. 调用execute方法添加一个请求任务
3. 若正在执行的线程数小于corePoolSize,则创建新线程执行该任务
4. 若正在运行的线程数量大于等于corePoolSize,那么将这个任务放入队列
5. 若队列已满且正在运行的线程数小于maximumPoolSize,
那么继续创建新的线程执行该任务
6. 若队列已满且正在运行的线程数大于或等于maximumPoolSize,则启动拒绝策略
7. 当一个线程完成任务时,它会从队列中取下一个任务来执行
8. 当一个线程空闲超过一定时间keepAliveTime时,线程池会判断如果当前运行的线程数大于corePoolSize则该线程被回收
注意
1. 线程池不允许使用Executors创建,而是通过ThreadPoolExecutor的方式创建,这样的处理方式可以更加明确线程池的运行规则,避免资源耗尽
1. FixedThreadPool及SingleThreadPool,其工作队列都是LinkedBlockingQueue,他们的长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2. CachedThreadPool及ScheduledThreadPool,其最大线程数都是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
如何合理配置线程池数量
IO密集型
1. CPU核心数 * 2
2. CPU核心数 / (1-阻塞系数),阻塞系数在0.8~0.9之间
CPU密集型
CPU核心数+1
Servlet
生命周期
1.构造器
init()
service()
destroy()
实现
HttpServlet
doGet
doPost
doHead
doDelete
doPut
doOptions
doTrace
集合
分支主题
分支主题
HashTable
key及value 均不能为null
key为null时,在获取哈希值(key.hashCode())时会抛出 NullPointerException
value 为null会抛出NullPointerException
ConcurrentHashMap
各版本实现差异
1.7
使用分段锁设计,使用的是segment数组,元素Segment继承于ReentransLock,内部属性与HashMap无异,有HashEntry数组,loadFactor, threshold, modCount, count
segment数组默认大小为16,并发度亦为16
获取segment锁的重试次数,多核CPU为64次,单核为1次
get方法不加锁,通过Unsafe.getObjectVolatile获取元素
size计算方式
1.将segment数组各元素中的计数count累加
2. 如果连续两次计算结果相同,则认为并发过程中计算的值正确,并返回。
3. 如果两次计算结果不相同,则segment全部加锁后重新计算
1.8
去除segment分段锁,改用数组 + 链表+ 红黑树
当table数量达到阈值则进行扩容,扩容过程中仍可以并发更新及插入
控制标识符 SizeCtl
-1 代表正在初始化
-N 表示有N-1个线程正在进行扩容操作
正数或0代表hash表还没有被初始化
HashMap
线程不安全
多线程故障: java.util.ConcurrentModificationException
线程安全解决方案
HashTable
ConcurrentHashMap
实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆
key及value 均可为null,当key为null时 hashcode为0
源码实现
初始容量16,负载因子0.75
各版本实现差异
1.7
扩容
桶中的链表使用头插法
多线程环境下,在transfer过程中容易造成循环链表,导致get的时候造成死循环
1.8
增加红黑树实现
链表转红黑数阈值为 8
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094 亿分之94
8: 0.00000006 亿分之6
————————————————
在负载因子 0.75的情况下,单个 hash 槽内元素个数为 8 的概率为亿分之6
红黑树转链表阈值为 6
链表转红黑树时数组容量最小阈值为 64
扩容阈值的两倍,新的容量与阈值相同
扩容
桶中的链表使用尾插法
hash值声明为final
头插法出现死循环
LinkedHashMap
继承于HashMap, 其Entry改写至HashMap, 增加了before及after指针,实现了双向链表。以此保证插入顺序与遍历顺序一致,也可以设置其按读取顺序
插入顺序(默认)
读取顺序
每读取一个元素就会将
该元素移动到链表的末尾
TreeMap
实现了SortedMap接口,默认排序是根据key值进行升序排序,若需自行实现排序方式可实现 comparator 方法
若key为字符串类型则默认使用的String中的compareTo方法作为比较方法,若需要指定key为其他类型则该类型需要实现Comparable接口
ArrayList
线程不安全,初始值为10
多线程故障: java.util.ConcurrentModificationException
线程安全解决方案
Vector , JDK 1.0版本
Collections.synchronizedList(new ArrayList())
CopyOnWriteArrayList
使用可重入锁实现
HashSet
线程不安全
多线程故障: java.util.ConcurrentModificationException
线程安全解决方案
Collections.synchronizedSet(new HashSet())
CopyOnWriteArraySet
使用可重入锁实现
基于HashMap实现,key是其变量,value是一个object常量
锁
可重入锁
synchronized
ReentrantLock
不可重入锁
死锁
原理
两个或两个以上的线程在执行过程中,因争夺资源二造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去
排查
1. 首先使用 jps -l 查出对应的进程
2. 使用 jstack 查看指定进程的死锁情况
逃逸分析
线程逃逸
方法逃逸
高效优化
栈上分配
同步消除
标量替换
ORM
Mybatis
Executor
SimpleExecutor
ReuseExecutor
BatchExecutor
延迟加载
插件运行原理
支持的四种接口
ParameterHandler
ResultSetHandler
StatementHandler
Executor
GC
判断对象是否存活的算法
引用计数法
可达性分析算法
垃圾收集算法
标记清除算法
效率问题,标记和清除两个过程的效率都不高
空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
标记整理算法
分代收集算法
垃圾收集器
serial
parnew (Serial收集器的多线程版本)
parallel scavenge
serial old
parallel old
CMS
参数
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=5
-XX:CMSInitiatingOccupancyFraction 默认92
-XX:+UseCMSInitiatingOccupancyOnly
仅使用设定的回收阈值,如92. 如果不指定,
JVM仅在第一次使用设定值,后续则自动调整.
-XX:+CMSParallelInitialMarkEnabled
初始标记阶段开启多线程,让系统停顿时间更短
-XX:+CMSScavengeBeforeRemark
在重新标记阶段,尽量先执行一次YGC。先执行一次Young GC,就会回收掉一些年轻代里没有人引用的对象,以此减少标记过程,提升重新标记性能
-XX:+CMSParallelRemarkEnabled
重新标记阶段开启多线程
缺陷
1. 因为存在并发标记,故对CPU敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低
2. 无法处理浮动垃圾,故需要预留部分内存用于存放浮动垃圾,也即当老年代空间使用达到一定比例时需要触发GC,即参数CMSInitiatingOccupancyFraction默认92%
3. 内存碎片过多,容易导致GC
-XX:+UseCMSCompactAtFullCollection 每次GC之后整理内存碎片
-XX:CMSFullGCsBeforeCompaction=5 在执行5次FullGC后再进行内存整理
流程
1. 初始标记
2. 并发标记
3. 重新标记
4. 并发清除
调优模板
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark
G1(jdk6引入实验版,jdk7商用,jdk9默认)
参数
-XX:UseG1GC
-XX:G1HeapRegionSize
-XX:G1NewSizePercent 默认5%
-XX:G1MaxNewSizePercent 默认60%
新生代占用堆内存的最大比例,超过此比例则进行新生代GC。按默认比例,此时老年代占比为40%
-XX:MaxGCPauseMills 默认200ms
-XX:InitiatingHeapOccupancyPercent 默认45%
如果老年代占据堆内存的45%的region时,
就会触发一个新生代和老年代一起回收
-XX:G1MixedGCCountTarget 默认8
意味着最后一个阶段先停止系统运行,混合回收一些region,再恢复系统运行,接着再次禁止系统运行,混合回收一些region,反复8次
-XX:G1HeapWastePercent 默认5%
混合回收时,对region回收都是基于复制算法进行的,都是把要回收的region里的存活对象复制到其他region,然后这个region的垃圾对象全
部被清理。当空出来的region数量达到堆内存的5%时,立即停止混合回收
-XX:G1MixedGCLiveThresholdPercent 默认85%
确定回收region时,必须是存活对象低于85%的region才可以回收
-XX:G1ReservePercent 默认10
老年代预留的内存,方便年轻代进入老年代时有足够空间
大对象进入单独的大对象region,不再进入老年代
回收失败时的FullGC
在进行混合回收时,无论是年轻代还是老年代都是基于复制算法进行回收,都要把各个region的存活对象拷贝到别的region里去。若拷贝过程中发现没有空闲的region可以承载存活的对象,则会触发一次失败。一旦失败,立马会切换停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出一批region,此过程会非常之慢。
对象何时进入老年代
1. 对象年龄超过 MaxTenuringThreshold
2. 动态年龄判定:若新生代GC后,存活对象超过survivor的50%,此时会判断某个年龄之前的的所有对象大小总和是否超过survivor的50%(如 年龄1+年龄2+年龄3 > survivor 的50%),则大龄对象全部进入老年代(即年龄3以上的对象)
流程
1. 初始标记
2. 并发标记
3. 最终标记
4. 筛选回收
jdk各版本默认收集器
java -XX:+PrintCommandLineFlags -version
jdk7: UseParallelGC
即 Parallel Scavenge + Serial Old
jdk8: UseParallelGC
即 Parallel Scavenge + Serial Old
jdk9: UseG1GC
ZGC(Zero Pause GC)
ZGC 收集器是一款基于 Region 内存布局的,(暂时)不设分代,使用了读屏障、染色指针和多重映射等技术实现可并发的标记 - 压缩算法、以低延迟为首要目标的一款垃圾收集器
JDK11支持4T内存,JDK13支持16T,收集时间不随内存变大而增大
特点
1. NUMA(Non uniform memory access)
CPU分配对象时,优先分配离得最近的内存
ZGC 默认支持 NUMA 架构,在创建对象时,根据当前线程在哪个 CPU 执行,优先在靠近这个 CPU 的内存进行分配,这样可以显著地提高性能,在 SPEC JBB 2005 基准测试里获得 40% 地提升
不分代
颜色指针
immediate memory reuse
OOM
案例分析
java.lang.OutOfMemoryError: PermGen space
可能的原因
MaxPermSize设置过小(默认为4M)
tomcat热部署时没有清理之前加载的环境
引用了大量三方jar包
改进
将相同的第三方jar文件移置到tomcat/shared/lib目录下,这样可以达到减少jar 文档重复占用内存的目的
java.lang.StackOverFlowError
原因
存在递归调用
存在循环依赖
改进
尽量将递归调用铺平,减少栈深度
如果你确认递归实现是正确的,为了允许大量的调用,你可以增加栈的大小。依赖于安装的 Java 虚拟机,默认的线程栈大小可能是 512KB 或者 1MB。你可以使用 -Xss 标识来增加线程栈的大小。这个标识即可以通过项目的配置也可以通过命令行来指定
java.lang.OutOfMemoryError: java heap space
java.lang.OutOfMemoryError: unable to create new native thread
高并发环境,一个进程创建多个线程,超过系统承载极限
查看当前用户可创建线程的上限
ulimit -u
在配置文件中设置:vim /etc/security/limits.d/20-nproc.conf
java.lang.OutOfMemoryError: GC overhead limit exceeded
GC回收时间过长,超过98%的时间用来做GC,并且回收了不到2%的堆内存。连续多次GC都只回收了不到2%的极端情况才会抛出,若没有抛出GC overhead limit 错误,那么GC清理完后很快又会被填满,迫使GC再次执行,如此形成恶性循环,CPU使用率一直是100%
java.lang.OutOfMemoryError: Direct buffer memory
写NIO程序时经常使用ByteBuffer来读取或写入数据,这是一种基于channel与buffer的IO方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存引用进行操作。如此可以在某些场景中提升性能,避免在Java堆和native堆中来回复制数据。若不断的分配本地内存,由于没有GC因此会导致本地内存用完,再次分配时就会出现OOM。出现该异常也有可能是设置了DisableExplicitGC参数,使得NIO在分配内存时无法执行System.gc()
ByteBuffer.allocate() 是分配Java堆内存,属于GC管辖范围,由于需要用户态与内核态之间的数据拷贝所以效率较慢
ByteBuffer.allocateDirect() 是分配本地内存,不属于GC管辖
范围,由于不需要用户态与内核态之间的数据拷贝所以效率较快
NIO每次分配新的堆外内存的时,都会调用System.gc()去提醒JVM去主动执行gc回收掉不再引用的DirectByteBuffer对象,释放堆外内存空间。
java.lang.OutOfMemoryError: Metaspace
metaspace 是方法区在Hotspot的实现,它与持久代最大的区别在于metaspace并不在虚拟机内存中,而是使用本地内存,永久代存放的信息有:虚拟机加载的类信息,常量池,静态变量,即时编译后的代码。为模拟metaspace溢出,只需要不断生成类往类空间放即可
溢出区域
MetaSpace
1. 永久代内存参数未指定或分配过小
2. 用cglib之类的技术动态生成一些类,一旦代码中没有控制好,导致你生成的类过于多的时候,就很容易把Metaspace给塞满,进而引发内存溢出
参数
-XX:HandlePromotionFailure 空间分配担保
1. 在发生Minor GC之前,虚拟机会检查老年代最大连续空间是否大于新生代所有对象总空间,若大于则GC安全。
2. 若小于或等于则虚拟机会查看HandlerPromotionFailure是否允许担保失败,若允许则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。若大于在尝试进行一次MinorGC. 如果小于或HandlerPromotionFailure未开启,则需进行FullGC
-XX:PreTenureSizeThrehold , 直接晋升到老年代的对象的大小
-XX:CMSInitiatingOccupancyFaction=92: CMS收
集器在老年代空间被使用多少后(默认92%)发起FullGC
UseParallelGC
Server模式下的默认值,即 Parallel Scavenge + Serial Old
内存分配与回收
1. 对象优先在Eden分配
2. 大对象直接进入老年代
3. 长期存活的对象直接进入老年代
4. 动态对象判定
若Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可直接进入老年代,无需等到 MaxTenuringThrehold 中要求的年龄
5. 空间分配担保
查看GC日志
相关参数
-XX:PrintGCDetails
-XX:PrintGCDetailTimeStamp
Xloggc:gc.log 指定gc日志文件
动态年龄判定
可能几次Young GC过后,Surviovr区域中的对象占用了超过50%的内存,此时会判断如果年龄1+年龄2+年龄N的对象总和超过了Survivor区域的50%,此时年龄N以及之上的对象都进入老年代,这是动态年龄判定规则
分支主题
分支主题
类加载
类加载过程
加载
1. 通过类的全限定名获取此类的二进制字节流
2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
3. 在内存中生成一个代表该类的Class对象,作为方法区这个类的各种数据的访问入口
验证
确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全
四个阶段
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
正式为类的变量(被static修饰的变量)分配内存并
设置类变量初始值,类变量都是在方法区中分配
解析
将常量池中的符号引用替换为直接引用
初始化
使用
卸载
实现方式
loader.loadClass
只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块
class.forName(name)
将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块
Class.forName(name, initialize, loader)
可控制是否加载static块。若为true则只有在调用newInstance()方法时在调用构造函数前才会初始化静态代码块
类加载器
BootstrapClassLoader 启动类加载器
ExtensionClassLoader 扩展类加载器
ApplicationClassLoader 应用程序类加载器 (也称 系统类加载器)
双亲委派模型
好处
缺点
在双亲委派模型中,子类加载器依旧可以使用父类加载器加载过的类,而父类加载器却无法使用子类加载器加载过的类。这就导致了双亲委派模型并不能解决所有的类加载器问题
JVM参数
查看默认参数
查看指定进程JVM参数: jinfo -flags 进程id
-XX:+PrintFlagsInitial
查看整个JVM初始化默认值
-XX:+PrintFlagsFinal
查看修改后的默认值
java -XX:+PrintCommandLineFlags -version
参数类型
标配参数
-version
-help
-showversion
X参数
-Xint
解释执行
-Xcomp
第一次使用就编译成本地代码
-Xmixed
混合模式
XX参数
bool类型
公式
-XX:+ 或者 -某个属性
+表示开启
- 表示关闭
Case
是否打印GC的收集细节
jinfo -flag PrintGCDetails 进程ID
是否使用串行垃圾收集器
jinfo -flag UseParallelGC 进程ID
KV键值类型
公式
-XX:key=value
Case
-XX:MetaspaceSize=128m
-XX:MaxTenuringThreshold=15,年轻代活过15次以后才可以进入老年代
注意
-Xms 等价于 -XX:InitialHeapSize
-Xmx 等价于 -XX:MaxHeapSize
日常调优参数
-Xms
初始堆内存大小,默认为物理内存1/64
-Xmx
最大堆内存,默认为物理内存的1/4
-Xss
单个线程栈的大小,一般默认为512k~1M
-Xmn
设置年轻代大小
-XX:MetaspaceSize
设置元空间大小
元空间取代了之前的老年代,元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下元空间的大小仅受本地物理内存大小限制
+PrintGCDetails
[GC (Allocation Failure) [PSYoungGen: 234291K->696K(344064K)] 234291K->205504K(999424K), 0.2497362 secs] [Times: user=0.06 sys=0.23, real=0.25 secs]
【【GC类型,GC前内存占用 -> GC后内存占用(某代总共大小)】 GC前堆内存占用->GC后堆内存占用(堆总大小),GC耗时】
-XX:MaxTenuringThreshold=15
经过多少次GC后才可以进入到老年代,可取值范围是 0-15
-XX: SurvivorRatio
设置新生代eden与S0,S1的比例,默认 -XX:SurvivorRatio=8, 即表示Eden:S0:S1=8:1:1
-XX: NewRatio
设置新生代与老年代在堆结构的占比,默认为2,即 -XX:NewRatio=2, 新生代占1,老年代占2,新生代占整个堆的1/3
分析工具
jstack
runnable
jstat(JVM Statistics Monitoring Tool)
故障排查
CPU过高
1. 使用Top命令查看占比最高的进程ID
shift + p : 按CPU占用大小排序
shift + m:按内存占用大小排序
2. ps -ef 或 jps 进一步定位程序
3. 定位到具体线程
ps -mp 进程号 -o THREAD,tid,time
-m 显示所有线程
-p pid进程使用CPU的时间
top -Hp [进程号] 结果显示的PID即为线程 ID
4. 找到耗时最高的线程,将其线程号tid转为16进制,英文小写
printf “%x\n” tid
5. jstack 进程号 |grep tid -A60
执行该命令后即可查看代码所在位置, -A60即打印出匹配位置的后60行(after, 前60行则用 -B60)
6. 分析具体线程
1. 若是VM Thread 则为GC线程, 可以判断是因为频繁GC导致CPU飙高
2. 若是业务线程则可以直接定位到具体代码进行分析
服务变慢的诊断思路
1. 整机Top
查看内存、CPU占比,以及load average负载
load average 中的三个数分别表示1分钟前、5分钟前、15分钟前进程的平均数,一般的可以认为这个数值超过 CPU 数目时,CPU 将比较吃力的负载当前系统所包含的进程;
查看负载的精简命令: uptime
分支主题
2. CPU: vmstat
vmstat -n 2 3 (每两秒采样一次,共计采样三次,主要查看procs及cpu)
procs
r : 即 running, CPU运行队列的进程数,原则上1核的CPU的运行队列不能超过2,整个系统的运行队列不能超过总核数的两倍,否则代表压力过大
b: 因等待资源而阻塞进程数,比如正在等待磁盘、网络IO等
cpu
us
用户进程执行时间百分比
sy
系统进程执行时间百分比
wa
IO等待时间与CPU执行时间的百分比,值越高代表IO负载越严重
id
空闲时间百分比
memory
swpd
交互空间
free
cache
高速缓存,由于CPU与主存的速度存在数量级差距,故使用该缓存缓解(可能存在一二三级缓存)
buff
缓冲区,即存储速度不同步的设备之间一种通信方式,如高速缓存与主存间存储速度有数量级的差距,为了提高性能,主存先将数据写入缓冲区,等写满缓冲区后高速缓存在一次性读取,避免速度快的高速缓存一直等待主存读取,以此来提升高速缓存的效率
swap
si
每秒从交换分区写到内存的大小(kb),由磁盘调入内存
so
每秒从内存写入交换分区的大小 (kb),由内存调入磁盘
io
bi
每秒从磁盘读入内存的块数
bo
每秒从内存写入磁盘的块数
system
in
每秒中断数
cs
每秒上下文切换数
额外命令
查看所有CPU核信息
mpstat -P ALL 2
每个进程使用CPU的用量分解信息
pidstat -u 1 -p 进程编号
sar
System Activity Reporter, 能查看CPU的平均信息,还能查看指定CPU的信息。与mpstat相比,sar能查看CPU历史信息
案例: 查看网卡流量,sar -n DEV 1 2。 统计结果包含每秒发送数据包数量,每秒接收数据包数量,每秒发送数据字节数,每秒接收的压缩数据包数,每秒钟接收的多播数据包数等
分支主题
mpstat
multiple processors statistic ,能查看所有CPU的平均信息,还能查看指定CPU的信息。 与sar相比,mpstat对CPU能实时状态进行监控
iostat
3. 内存 free
查看额外
pidstat -p 进程号 -r 时间间隔
minflt/s
每秒次缺页错误次数(minor page faults),次缺页错误次数意即虚拟内存地址映射成物理内存地址产生的page fault次数
majflt/s
每秒主缺页错误次数(major page faults),当虚拟内存地址映射成物理内存地址时,相应的page在swap中,这样的page fault为major page fault,一般在内存使用紧张时产生
VSZ
该进程使用的虚拟内存(以kB为单位)
RSS(Resident Set Size 常驻内存)
该进程使用的物理内存(以kB为单位)
%MEM
该进程使用内存的百分比
4. 硬盘: df
5. 磁盘IO: iostat
磁盘IO性能评估
iostat -xdk 2 3
rkB/s
每秒读取数据量
wkB/s
每秒写入数据量
svctm
IO请求的平均服务时间,单位为毫秒
await
IO请求的平均等待时间,单位毫秒
util
每秒有百分之几的时间用于IO操作,接近100%时,表示磁盘带宽占满
查看额外
pidstat -d 时间间隔 -p 进程号
网络IO:ifstat
性能监控
jps
jinfo (java 配置信息工具)
jmap 内存映像工具
生成应用程序的堆快照和对象的统计信息
案例
映射堆快照
jmap -heap 进程id
抓取堆内存
jmap -dump:file=dump 28196 : 生成hprof文件并下载到本地
MAT分析插件工具
查看类及其实例数
jmap -histo <pid>
jstat 统计信息工具
案例
类加载统计
jstat -class 进程id 1000
编译统计
jstat -compiler 进程id 1000
Compiled:编译数量。
Failed:失败数量
Invalid:不可用数量
Time:时间
FailedType:失败类型
FailedMethod:失败的方法
GC统计
新生代
jstat -gcnew 进程ID 1000 100
S0C:第一个幸存区大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
TT:对象在新生代存活的次数
MTT:对象在新生代存活的最大次数
DSS:期望的幸存区大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
jstat -gcnewcapacity 进程ID 1000 100
NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:当前新生代容量
S0CMX:最大幸存1区大小
S0C:当前幸存1区大小
S1CMX:最大幸存2区大小
S1C:当前幸存2区大小
ECMX:最大伊甸园区大小
EC:当前伊甸园区大小
YGC:年轻代垃圾回收次数
FGC:老年代回收次数
老年代
jstat -gcold 进程ID 1000 100
MC:元数据区大小
MU:元数据区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
OC:老年代大小
OU:老年代使用大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
jstat -gcoldcapacity 进程ID 1000 100
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:老年代大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
元数据区
jstat -gcmetacapacity 进程ID 1000 100
MCMN:最小元数据容量
MCMX:最大元数据容量
MC:当前元数据空间大小
CCSMN:最小压缩类空间大小
CCSMX:最大压缩类空间大小
CCSC:当前压缩类空间大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
全部区域
jstat -gc 进程id 1000
显示各个区域的具体数值
jstat -gcutil 进程id 1000
显示各个区域的占用百分比
查看堆内存各部分使用量,以及加载类的次数
jstack 堆栈异常跟踪工具
死锁的排查
jvisualvm
jconsole
string
Immutable 不可变性
保证线程安全,无需同步。不可变对象可以被自由地共享
如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。
因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。
JDK 漏洞
substring 在 jdk6 引发的内存泄漏
JDK6实现
分支主题
JDK7 改进
分支主题
拼接字符串
+号
底层实现是通过 StringBuilder, 但如果通过for循环拼接时会不断 new StringBuilder 导致效率下降
concat
通过new 字符数组实现
StringBuilder
StringBuffer
StringUtils.Join
字符串长度限制
编译期, 要求字符串常量池中的常量不能超过 65535
运行期, 要求长度不能超过 int 范围
代理模式
好处
为其他对象提供一种代理以控制对这个对象的访问
提供统一接口,在不影响系统调用的情况下进行扩展,即遵循开放封闭原则
代理类型
动态代理
cglib
被代理类不能有final修饰符,否则报错
被代理方法不能有final修饰符,否则代理无效
jdk
通过反射生成实现了相关接口并继承Proxy的代理类,代理类会记录下指定方法的变量,调用指定方法时先执行实现了IvokationHandler接口的实例再执行指定方法
静态代理
只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐
框架
Spring
IOC
优势
对象的统一托管
规范生命周期
灵活的依赖注入
一致的获取对象方式(默认单例)
重要模块
core
context
aop
expression
orm
mvc
1. 用户请求发送至DispatchServlet
2. DispatcherServlet收到请求调用HandlerMapping处理
3. HandlerMapping找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及拦截器(如果有则生成)一并返回给DispatcherServlet
4. DispatcherServlet调用HandlerAdapter处理
5. HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)
6. Controller执行完成返回ModelAndView
7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器
9. ViewReslover解析后返回具体View
dao
ApplicationContext
ClassPathXmlApplicationContext
FileSystemXmlApplicationContext
AnnotationConfigApplicationContext
分支主题
@Transactional
bean
生命周期
singleton
prototype
request
session
application
websocket
创建流程
1. 首先执行BeanPostProcessor中的postProcessBeforeInitialization
2. 执行InitializingBean接口中的afterPropertiesSet方法
3. 再执行xml配置中的 init-method方法
4. 最后执行BeanPostProcessor中的postProcessAfterInitialization
SpringBoot
核心注解 @SpringBootApplication
SpringBootConfiguration
EnableAutoConfiguration
ComponentScan
常见线程安全问题
SimpleDateFormat
分支主题
常见内存泄露类型
ThreadLocal
虽然Entry是弱引用,里面的key在gc发生时会被回收,但是对应的value还是会造成内存泄露
引用类型
强引用
软引用
弱引用
虚引用
并发
volatile
1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2. 禁止进行指令重排序。
synchronized
反射
在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性
线程
wait与sleep
在等待时wait 会释放锁,而sleep 一直持有锁。
wait 通常被用于线程间交互,sleep 通常被用于暂停执行
实例方法thread.isInterrupted() 与 静态方法Thread.interrupted()
中断协议
interrupt ( )
可响应中断线程
wait
sleep
join
不可响应中断线程
InputStream
OutputStream
分支主题
wait与notify
如果是通过notify来唤起的线程,那先进入wait的线程会先被唤起来
如果是通过notifyAll唤起的线程,默认情况是最后进入的会先被唤起来,即LIFO的策略
有无 Thread.Sleep (0) 的区别
Thread.Sleep (0) 的作用,就是 “触发操作系统立刻重新进行一次 CPU 竞争,重新计算所有进程的总优先级”。竞争的结果也许是当前线程仍然获得 CPU 控制权,也许会换成别的线程获得 CPU 控制权。这也是我们在大循环里面经常会写一句 Thread.Sleep (0) ,因为这样就给了其他线程比如 Paint 线程获得 CPU 控制权的权力,这样界面就不会假死在那里。
异常
throwable
Error
Exception
受检异常
RuntimeException
NullPointerException
ArrayIndexOutOfBoundsException
IndexOutOfBoundsException
ArithmeticException
ClassNotFoundException
IllegalAccessException
IllegalArgumentException
IllegalStateException
非受检异常
非运行时异常
IOException
SQLException
InterruptedException
IllegalAccessException
NoSuchMetodException
分支主题
0 条评论
下一页