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内存模型
缓存架构中数据的流转
内存和CPU之间有3级缓存
数据原子性操作
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二进制)
执行上面的代码打印汇编日志
下载安装包
加参数打印汇编码
jvm 一种解释模式 一种是本地代码模式
解释模式 可能会绕开汇编码 在c++把程序业务逻辑实现了
编译以后的 变成本地代码来执行
把汇编日志打印相关的参数加入到启动命令中
这是汇编日志 可以看到里面有一个 lock加锁的过程
把值回写到主内存
valitile本身并没有锁机制 是站在总线级别的刷新机制
-
禁止指令重排序
在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器性能,会对机器指令重排序优化
单例模式
饿汉式的单粒模式 在类加载的时候 加载class 分配对象到堆中的
但使用时间不确定 可能好几周之后才会用到 所以就会造成严重的内存浪费
节约内存方式 就是双重检查锁定
注意:INSTANCE实例需要被valitile修饰 防止指令重排序
-
为什么锁代码块里面还要再加一个if判断
如果没有第二次检查 在多线程并发的情况下 会创建多个实例
线程1和线程2同时获取锁
线程1获得锁 线程2阻塞等待锁
线程1创建了实例对象 释放锁
线程2从wait状态被唤醒去竞争锁
如果得到锁 会再次创建一个实例
所以如果此时再有一个判断 则不会出现多个实例的情况
-
为什么要对实例变量添加valitile修饰 因为可以防止指令重排序
创建实例对象的时候 在字节码层面会执行3条指令
按照查看字节码的插件
得到3个关键字节码指令
1、在堆中划出一块空间
2、构造方法
3、把引用INSTANCE指向
指令重排序会导致对象创建的半初始化
如果把上面的2,3指令 反过来执行
先执行3 再执行2
上面demo代码中有一个变量i
线程1先执行第三个指令把引用INSTANCE指向 该变量是整型 此时初始化为0
线程2获取该实例 读取该变量的为0
线程1再执行第二个指令执行构造方法 进行初始化i变成13
此时线程1本地内存中的i是13
线程2本地内存中的i是0
所以就造成了数据不一致
那么这种情况就需要禁止指令重排序
给实例变量添加valitle修饰符 valitle提供内存屏障功能 使lock前后指令不能重新排序
查看汇编日志
给实例变量添加valitle修饰
java创建实例对象的时候 字节码需要3个操作
在堆中分配内存空间
执行构造函数
引用指向实例 汇编对这个操作添加lock锁
那么前后的字节码指令在cpu调度的时候不能重排序 相当于加了内存屏障
那么就不会出现 成员变量半初始化的情况
滥用valitile会造成伪共享问题
主内存和工作内存中的数据是一行一行的 每一行是一个缓存行
基于缓存的可见性
每次更新的时候 只更新一行数据
缓存行每64个字节更新一次
假设有一个long类型的数据 long占用8个字节
第一个线程读取第一个元素long[0]
第二个线程读取第二个元素long[1]
但这2个元素在同一个缓存行
如果加了valitile关键字
只要发现a更新了 要把整个缓存行都更新到主内存
线程2也是读取这一行
触发缓存失效机制
线程2的本地内存中的数据更新了一次
明明数据没有关联 但在同一个缓存行 更新了a 导致b的缓存失效
伪共享导致使用的时候效率很低
消息中间件对比
RabbitMQ erlang语言编写 AMQP协议
rabbitmq工作模型
vhost主要用于权限管理 比如给a用户分配一个vhost 给b用户分配另外一个vhost
channel可以共用一个tcp连接
创建10个或100个channel实现多路复用
常用的交换器模式
-
direct 完全匹配 -
topic 正则表达式匹配 -
fanout 只要存在绑定关系 就发送到对应的队列
rabbitmq消息可靠性分析
-
确保消息到MQ(异步--发送者确认机制) 可以确保消息成功发送给交换器
起一个异步监听 发送完之后 再通知我
-
确保消息路由到正确的队列 如果没有正确路由到队列 会有失败通知
-
确保消息在队列正确存储
mq突然宕机或断电
数据默认放在内存 需要放在持久化
交换器、队列、message存储本体都要持久化
性能会下降80%
往往不会做持久化
而是做高可用
mq durable 默认是持久化的
-
确保消息从队列中正确的投递给消费者
自动确认
但为了消息可靠性 需要手动确认
rocketmq 零拷贝技术
客户端读取文件发送给服务端
File.send(file,buf,len)
Socket.send(file,buf,len)
鼠标点击 敲键盘 显示器 网络传输 文件传输 都需要内核处理
MMAP技术
Windows操作系统也有这个技术
RandomAccessFile应用了MMAP技术
耗时情况比较
200M的数据 一般处理方式 耗时404ms
MMAP技术处理 耗时202ms
MMAP处理性能只能提升一倍
rocketmq为什么不像kafka一样使用sendfile技术
这是由于使用场景决定的
rocketmq还需要对数据进一步的包装
rocketmq使用了mmap仅能提升1倍的性能 那什么比activemq性能好20倍?
原因是rocketmq 使用了并发编程技术 jvm底层优化技术
-
rokcetmq使用原子操作类 cas提高并发性 无锁 多线程安全
-
rocketmq使用写时复制容器
用来存储内存映射的map文件
可以并发读
写的时候 加锁 复制一个副本
往副本上写 写好了之后
将指针引用到新副本
-
rocketmq大量使用堆外内存
不属于JVM内存
java可以使用
避免GC(GC会发生STW 会暂停 减少并发量)