JVM内存预调优

葵花宝典中_老年代

​老年代2G 新生代1G (eden 800M so和s1 分别100M)情况​



经过估算一笔订单会占用0.2M内存 500笔订单是100M
进入方法里面的局部变量一般都会存活 其他对象一般方法执行完
引用就不存在了 那么就变成了垃圾
对象先在eden区堆积 大概8秒会堆积满
那么就会触发第一次YGC
存活的对象大概100M则会进入S0或S1区

第二次YGC的时候 会回收eden区和S0或S1区
剩余的对象扔到S1或S0区

根据动态年龄判断 只要交换区超过50% 则不会进入交换区
而是提前晋级 会进入老年代

老年代2G 8秒100M进入老年代
所以不到3(20*8=160s)分钟的时候 老年代就填满了 就会引发FGC(FGC耗时相当长 标记整理或标记清除)
原因是不应该晋级的对象却因为动态年龄判断进入到老年代

那么此时老年代存放的对象是朝生夕死的对象 所以就不合理了

​老年代1G 新生代2G (eden 1.2G so和s1 分别400M)情况​

12秒eden才会被填满 触发YGC
S0交换区存100M的时候 此时并没有达到50%
所以S1还可以存放100M
YGC的时候 会回收eden和一个S区 将存活对象放在另外一个S区

老年代长期存活的对象比如SpringBean对象(单例长期存活)
业务对象 在eden和1个s区 存活 避免出现FGC

​JMM-Java内存模型​

葵花宝典中_缓存_02

​缓存架构中数据的流转​

内存和CPU之间有3级缓存

葵花宝典中_老年代_03葵花宝典中_老年代_04

​数据原子性操作​

葵花宝典中_缓存_05葵花宝典中_缓存_06葵花宝典中_老年代_07

flag变量用volatile修饰

主内存有一个共享变量flag=false
cpu调度线程1从主内存读取变量 加载到线程1的工作内存
然后再被线程1使用
线程2同理
线程2 修改线程2工作内存中的flag变量为true
Store到总线 然后总线再write到主内存
cpu具有总线嗅探机制即总线中自己关心的数据发生变化
cpu就会监听到
首先将对应线程工作内存中的flag变量置为null
然后重新从总线中获取flag变量的值

​volatile底层实现原理​

  • 线程可见性1、讲当前处理器缓存行中的数据立即写回主内存
2、这个写操作 会触发总线嗅探机制(MESI协议)
3、完成类似内存屏障功能

​进一步查看volatile对应的汇编代码​

java源码-->class字节码(操作flag)-->汇编指令(lock前缀flag)-->机器码(1010二进制)

执行上面的代码打印汇编日志

​下载安装包​

葵花宝典中_数据_08

加参数打印汇编码

葵花宝典中_老年代_09

jvm 一种解释模式 一种是本地代码模式
解释模式 可能会绕开汇编码 在c++把程序业务逻辑实现了
编译以后的 变成本地代码来执行

葵花宝典中_老年代_10

把汇编日志打印相关的参数加入到启动命令中

葵花宝典中_缓存_11

这是汇编日志 可以看到里面有一个 lock加锁的过程
把值回写到主内存
valitile本身并没有锁机制 是站在总线级别的刷新机制
  • 禁止指令重排序
在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器性能,会对机器指令重排序优化

葵花宝典中_数据_12葵花宝典中_老年代_13

​单例模式​

葵花宝典中_老年代_14

饿汉式的单粒模式 在类加载的时候 加载class 分配对象到堆中的 
但使用时间不确定 可能好几周之后才会用到 所以就会造成严重的内存浪费

​节约内存方式 就是双重检查锁定​

葵花宝典中_缓存_15

注意:INSTANCE实例需要被valitile修饰 防止指令重排序

  • 为什么锁代码块里面还要再加一个if判断
如果没有第二次检查 在多线程并发的情况下 会创建多个实例

线程1和线程2同时获取锁
线程1获得锁 线程2阻塞等待锁
线程1创建了实例对象 释放锁
线程2从wait状态被唤醒去竞争锁
如果得到锁 会再次创建一个实例
所以如果此时再有一个判断 则不会出现多个实例的情况
  • 为什么要对实例变量添加valitile修饰 因为可以防止指令重排序

葵花宝典中_老年代_16

创建实例对象的时候 在字节码层面会执行3条指令

​按照查看字节码的插件​

葵花宝典中_数据_17葵花宝典中_缓存_18

​得到3个关键字节码指令​

葵花宝典中_数据_19

1、在堆中划出一块空间
2、构造方法
3、把引用INSTANCE指向

​指令重排序会导致对象创建的半初始化​

如果把上面的2,3指令 反过来执行
先执行3 再执行2

葵花宝典中_老年代_20

上面demo代码中有一个变量i 
线程1先执行第三个指令把引用INSTANCE指向 该变量是整型 此时初始化为0
线程2获取该实例 读取该变量的为0
线程1再执行第二个指令执行构造方法 进行初始化i变成13
此时线程1本地内存中的i是13
线程2本地内存中的i是0
所以就造成了数据不一致

那么这种情况就需要禁止指令重排序

给实例变量添加valitle修饰符 valitle提供内存屏障功能 使lock前后指令不能重新排序

​查看汇编日志​

葵花宝典中_数据_21

给实例变量添加valitle修饰
java创建实例对象的时候 字节码需要3个操作
在堆中分配内存空间
执行构造函数
引用指向实例 汇编对这个操作添加lock锁
那么前后的字节码指令在cpu调度的时候不能重排序 相当于加了内存屏障
那么就不会出现 成员变量半初始化的情况

​滥用valitile会造成伪共享问题​

葵花宝典中_老年代_22

主内存和工作内存中的数据是一行一行的 每一行是一个缓存行
基于缓存的可见性
每次更新的时候 只更新一行数据
缓存行每64个字节更新一次
假设有一个long类型的数据 long占用8个字节

第一个线程读取第一个元素long[0]
第二个线程读取第二个元素long[1]
但这2个元素在同一个缓存行
如果加了valitile关键字
只要发现a更新了 要把整个缓存行都更新到主内存
线程2也是读取这一行
触发缓存失效机制
线程2的本地内存中的数据更新了一次
明明数据没有关联 但在同一个缓存行 更新了a 导致b的缓存失效
伪共享导致使用的时候效率很低

​消息中间件对比​

葵花宝典中_数据_23

RabbitMQ  erlang语言编写 AMQP协议

​rabbitmq工作模型​

葵花宝典中_缓存_24

vhost主要用于权限管理 比如给a用户分配一个vhost 给b用户分配另外一个vhost
channel可以共用一个tcp连接
创建10个或100个channel实现多路复用

​常用的交换器模式​

  • direct 完全匹配
  • topic 正则表达式匹配
  • fanout 只要存在绑定关系 就发送到对应的队列

​rabbitmq消息可靠性分析​

葵花宝典中_缓存_25

  • 确保消息到MQ(异步--发送者确认机制) 可以确保消息成功发送给交换器

葵花宝典中_数据_26

起一个异步监听 发送完之后 再通知我

葵花宝典中_数据_27葵花宝典中_老年代_28

  • 确保消息路由到正确的队列 如果没有正确路由到队列 会有失败通知

葵花宝典中_数据_29葵花宝典中_缓存_30

  • 确保消息在队列正确存储

mq突然宕机或断电
数据默认放在内存 需要放在持久化
交换器、队列、message存储本体都要持久化
性能会下降80%
往往不会做持久化
而是做高可用

mq durable 默认是持久化的
  • 确保消息从队列中正确的投递给消费者

​自动确认​

葵花宝典中_数据_31

​但为了消息可靠性 需要手动确认​

葵花宝典中_老年代_32葵花宝典中_老年代_33

​rocketmq 零拷贝技术​

客户端读取文件发送给服务端
File.send(file,buf,len)
Socket.send(file,buf,len)

葵花宝典中_老年代_34

鼠标点击 敲键盘 显示器 网络传输 文件传输 都需要内核处理

​MMAP技术​

葵花宝典中_老年代_35

Windows操作系统也有这个技术

葵花宝典中_数据_36

​RandomAccessFile应用了MMAP技术​

葵花宝典中_数据_37

​耗时情况比较​

葵花宝典中_老年代_38

200M的数据 一般处理方式 耗时404ms
MMAP技术处理 耗时202ms
MMAP处理性能只能提升一倍

​rocketmq为什么不像kafka一样使用sendfile技术​

葵花宝典中_缓存_39

这是由于使用场景决定的 
rocketmq还需要对数据进一步的包装

​rocketmq使用了mmap仅能提升1倍的性能 那什么比activemq性能好20倍?​

原因是rocketmq 使用了并发编程技术 jvm底层优化技术
  • rokcetmq使用原子操作类 cas提高并发性 无锁 多线程安全

葵花宝典中_老年代_40葵花宝典中_缓存_41

  • rocketmq使用写时复制容器

葵花宝典中_老年代_42

用来存储内存映射的map文件
可以并发读
写的时候 加锁 复制一个副本
往副本上写 写好了之后
将指针引用到新副本
  • rocketmq大量使用堆外内存
不属于JVM内存
java可以使用
避免GC(GC会发生STW 会暂停 减少并发量)

葵花宝典中_老年代_43