参考
- https://www.freesion.com/article/42891183904/
内存结构
- https://blog.youkuaiyun.com/aajjw/article/details/115672226
- TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区:
- 如果是共享空间(堆上的其他区域)分配空间必须要有同步机制,
- TLAB是给本线程在堆空间上开辟出一块独享区域,因此无需同步
- TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。
垃圾收集器
- 内存分配的方式
- 碰撞指针
- 空闲列表
- 垃圾收集器处理回收的是堆内存和方法区的内存
- 如何判断对象存活
- 引用计数法
- 可达性算法
- GC Roots
- 栈,本地方法栈中引用的对象
- 方法区中引用,如类静态属性引用的对象,常量池引用
- JVM内部的引用
- 基本数据类型对应的Class对象,
- 常驻对象:异常类
- 同步锁持有的对象
- 代码缓存
- GC Roots
- 常用垃圾回收的算法
- 标记-清除
- 复制
- 标记-整理
- 分代思想
分代算法与GC过程
- 分代
- GC过程
- 堆内存管理-对象内存空间分配与转移
- 对象优先在Eden分配
- 大对象直接分配到Old
- 长期存活进入Old
- Eden,S0,S1,from to。过程如下
- 年龄大于15进入Old,也可以提前
- 空间分配担保
- Old区域连续空间大于新生代所有对象空间 或者 大于历次晋升到老年代对象的平均大小,则进行Minor GC。
- 否则进行Full GC
- GC日志打印:https://blog.youkuaiyun.com/x763795151/article/details/89981686
- 使用jvm命令,可以打印GC的信息(GC类型,GC开始时间,CG耗时,GC前后的内存空间统计情况),并可以指定文件
- GC类型:
- Minor GC:新生代GC,
- 一般采用复制算法回收垃圾,
- 一般回收速度也比较快
- 触发条件
- eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
- 新创建的对象大小 > Eden所剩空间时触发Minor GC
- Major GC:老年代GC,
- 通常执行Major GC会连着Minor GC一起执行
- Major GC的速度要比Minor GC慢的多
- 可采用标记清楚法和标记整理法
- Full GC:整个堆空间
- 触发情况和Major GC相似
- 每次晋升到老年代的对象平均大小>老年代剩余空间
- MinorGC后存活的对象超过了老年代剩余空间
- 永久代空间不足
- 执行System.gc()
- CMS GC异常
- 堆内存分配很大的对象
- 触发情况和Major GC相似
- Minor GC:新生代GC,
机制
- STW:stop the world
- OopMap:对象表,记录了内存中哪些位置是引用
- 安全点:不是所有的指令都记录Oop,而是在“需要长时间执行的指令处”记录Oop,如:方法调用,循环跳转,异常跳转等。
- 主动式抢占:GC需要中断时,设置标记,线程执行时主动轮询标记,如果是真就挂起。主动轮询的时点和记录Oop的时机重合并多一个创建对象分配内存时。
- 安全区:
- 以上机制在线程是运行状态是可以完美运行的。但是如果线程挂起,就不能了。
- 安全区:在这个区域内,引用关系不发生改变
- 线程进入安全区时,标识自己进入了安全区。
- GC时不用管在安全区内的线程,因为引用状态不发生改变。
- 线程离开安全区时,检查是否完成根节点枚举,如果完成就可以继续执行,否则等待GC完成根节点枚举。
垃圾收集器类型
- CMS
- 目标:最短回收停顿时间为目标的收集器
- 过程:
- 初始标记(CMS initial mark):需要Stop The World,标记GC Roots能直接关联到的对象,速度很快
- 并发标记(CMS concurrent mark):耗时长,但是可以和用户线程并发
- 重新标记(CMS remark):需要Stop The World,标记并发标记期间变动的对象,时间较短
- 并发清除(CMS concurrent sweep):不需要移动对象,可以并发
- 缺点:
- 阈值不好确定
- 因为GC过程中可能产生新的垃圾,因而不能老年区满了再收集
- 阈值小,浪费空间,频繁GC
- 阈值大,回收失败就用单线程的垃圾收集器兜底,慢。
- 标记清除算法,内存碎片。可以配置参数,每几次Full GC后的Full GC先整理碎片。
- 核心数少时,应用性能降低很厉害。
- 阈值不好确定
- G1
- 参考
- https://www.cnblogs.com/sidesky/p/10797382.html
- https://www.cnblogs.com/aspirant/p/8663872.html
- 目标:停顿时间模型。不是完全低延时,而是延时可控下的最高吞吐,因此是全功能。
- 特点:
- 每次根据用户设定允许的收集停顿时间,默认值是200毫秒,优先处理回收价值收益最大的那些Region
- 分块+分区:
- Humongous与大对象:当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
- 回收算法:整体是标记-整理。局部是标记-复制。不产生碎片
- 全年龄段收集
- 实现:
- 跨块引用:记忆集中存双向指针,因此(垃圾收集器本身)内存占用高,经验值10%-20%堆空间
- 分块监控,获取预期值。算法:衰减均值
- GC模式:
- young gc:
- 触发:Eden空间耗尽
- 回收对象:新生代
- 回收方式:
- 因为复制对象,所以stop the world
- 逻辑上参考分代算法
- Eden->Survivor
- Eden->Old
- Survivor->Survivor
- Survivor->Eden
- 物理上是通过标记复制完成
- mixed gc
- 回收对象:新生代和部分老年代
- 过程
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation):
- 更新Region的统计数据,根据用户期望和统计数据制定回收计划
- 并发处理:块间复制,涉及对象移动,必须暂停线程
- G1命令优化参考:https://www.cnblogs.com/aspirant/p/8663872.html
- young gc:
- 参考
- ZGC
- 选择
- JDK版本新:ZGC,至少JDK11,性能优秀,特别是延时方面
- JDK版本老:
- CMS:4GB到6GB以下的堆内存
- G1:更大的内存
内存模型
- 并发编程的三个概念
- 多核CPU:
- 多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致
- 解决方式
- 锁总线:效率低
- 缓存一致性协议:效率高,单各家CPU的协议不一样
- JMM将不同(CPU)平台的内存模型统一
- 逻辑模型和操作
- 主内存,每个线程都有自己的工作内存
- 8种操作及基本规则
- JMM主要规定了一个线程对共享变量的写入何时对其他线程是可见的
- 逻辑模型和操作
- 通过三大特性对比volatile,synchronized和final
- 原子性:对于基本数据类型的读取和赋值操作都是原子性操作
- 可见性:一个线程修改了共享变量的值,其他线程能够立即得知这个修改
- volatile:通过添加lock指令实现可见性
* lock指令在多核的情况下会做两件事:
* 将当前处理器缓存行的数据写回到系统内存
* 写回内存的操作会使其他CPU里缓存了该内存地址的数据无效 - synchronized
- 当线程获取锁时会从主内存中获取共享变量的最新值
- 释放锁的时候会将共享变量同步到主存中
- final
- 被final关键字修饰的字段在构造器中一旦初始化完成
- 并且没有发生this逃逸(其他线程通过this引用访问到初始化了一半的对象)
- 那么其他线程就能看见final字段的值
- volatile:通过添加lock指令实现可见性
- 有序性:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的
- 允许编译器和处理器对指令进行重排序,重排序的过程不会影响到单线程程序的执行
- 无序是因为发生了指令重排序和工作内存与主内存同步延迟
- 指令重排序
- as-if-serial:不管怎么进行重排序,单线程程序的执行结果不能被改变。
- happens-before:先行发生原则
- 不违背两个原则,爱怎么排序就怎么排序
- 指令重排序
- volatile关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前
- 内存屏障的4种类,在每家CPU上情况可能不同
- synchronized关键字同样可以保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码
- 有用的事例
- 读取和赋值操作是原子的,但是运算操作不是
- volatile正确使用:https://zhuanlan.zhihu.com/p/112742540
- 状态标记立刻被感知
- 多线程下,不加volatile,状态标记的改变不被其他线程看到
- 一改多读
- 原则
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中。
- 状态标记立刻被感知
synchronized机制
java工具及命令
- 命令行工具
- 图形化工具
- jvisualvm:
- 图形化监控,可以远程,但是需要应用打开对应端口
- 插件丰富
- Buffer Pools:可以看直接内存大小
- arthas
- 监控内容丰富
- dashboard:线程信息,内存情况(包括直接内存)
- 获取反编译文件,确认线上代码版本
- watch:查看方法的调用:https://www.cnblogs.com/alisystemsoftware/p/13087196.html
- 使用Instrument代码热替换
- JVMTI,JVM层级的接口,
- 监控内容丰富
- jvisualvm:
Java 动态字节码
- 动态生成字节码ASM等,但无法做到替换
- Instrument
- BTrace
常见JVM内存问题的定位
- 事前配置
- 异常日志记录,主要是OOM这类,通常和应用日志一起记录
- 指定jvm参数,在OOM时,转存dump文件,方便定位内存泄露的问题
- GC日志记录,方便还原GC过程。
- 参考:https://blog.youkuaiyun.com/x763795151/article/details/89981686
- 使用jvm启动参数,可以打印GC的信息(GC类型,GC开始时间,CG耗时,GC前后的内存空间统计情况),并可以指定文件
- 监控,如arthas,或者监控平台
- 常见问题
- OOM
- 先分析异常信息,哪个内存区域的内存溢出
- 堆内存一般要分析dump文件
- 比如,线上hibernate级联查询
- 监控显示内存占用大,但堆内存空间不大
- 查看jvm启动参数,是否配置了其他内存空间大小,针对没配置大小的一一排查,
- 优先排查直接内存,因为直接内存默认和堆内存一样大
- 通常是堆外内存
- 直接内存,如NIO
- 元空间中的类信息,如使用字节码技术大量生成类(如:cglib动态代理)
- 查看jvm启动参数,是否配置了其他内存空间大小,针对没配置大小的一一排查,
- 应用进程被杀死,但是没有dump文件
- 没有dump文件,证明没有OOM
- 极有可能是Linux OOM killer,查linux的messages日志,确认是否是Linux OOM killer
- OOM
JVM调优
- 主要是选择垃圾收集器,配置垃圾收集器参数
- 经验:
- 堆内存大小最大最小一致,避免内存缩扩容
- 调整年轻代和老年代比例
- 根据应用特点
- 根据GC日志