JVM 虚拟机与高性能案例秒杀—总结
JVM虚拟机
JVM组成架构
java跨平台的基础就是JVM虚拟机。JVM提供了不同平台的虚拟机,通过加载java字节码到虚拟机运行。java字节码是统一的,也就实现了跨平台。架构如下:
java字节码文件
java所有指令有200个左右,一个字节(8位)可以存储256个不同的指令,一个这样的字节称为字节码(ByteCode)。在代码执行过程中,JVM是通过解释字节码来执行的,屏蔽了系统直接的差异。JVM会分析出热点代码(单位时间执行次数统计),通过JIT动态编译为机器码,提高执行效率。
字节码执行流程
java字节码编译过程
将.java 源文件进行词法分析、语法分析、语义分析,最终生成字节码。
类加载
类加载双亲委派模型
java是面向对象的语言,为了避免重复加载类,当前类加载的时候需要上级类确认,上级类确认未加载过该类并允许加载的时候,才让当前类加载这个未知类。
自定义类加载器
有些场景需要对类加载做自定义的时候,可以通过继承ClassLoader类来实现自定义类加载器。
隔离加载类:同一JVM的不同组件的同一个类的不同版本。
扩展加载元:从网络、数据库等处加载字节码。
字节码加密:加载自定义的加密字节码,在自定义的类加载器中解密。
堆&栈
堆:
每个JVM实例对应一个堆,应用程序在运行中创建的所有类实例都存放在堆中,且由应用内所有的线程共享。
堆栈:
JVM未每个创建的每个线程分配一个堆栈。程序的运行,就是通过堆栈来完成的。java中所有的对象分配基本都在堆中完成(逃逸分析等优化的会在栈中创建),在栈中会创建对象的引用。
方法区&程序计数器
方法区主要存放从磁盘加载进来的类字节码,在程序运行中创建的类实例则放在堆中。当JVM进入启动类的main方法后,就会创建一个主线程,main方法里的代码就会被这个线程执行,每个线程都有自己的(非共享)的栈,栈里面存放的就是方法运行期的局部变量。而当前线性执行到哪一个字节码指令,则存放在程序计数器里。
Java(线程)栈
所有在方法内定义的基础变量,都会被每个运行这个方法的线程放在自己的栈中,线程的栈是彼此隔离的,所以这样的变量是线程安全的。
线程工作内存 & volatile
Java内存模型规定在多线程的情况下,线程操作主内存变量,需要通过线程独有的工作内存拷贝主内存变量副本来进行。
当一个共享变量(类的成员变量、类的静态成员变量)被volitile修饰后,具备两层寓意:
保证了不同线程对这个变量进行操作时的可见性,即一个线程对这个变量做了更改,其它线程立刻得到最新值。
禁止进行指令重排序。
Java运行环境
JVM垃圾回收
JVM垃圾回收就是将程序不再使用的对象清理掉,释放内存资源。
JVM是通过一种可达性分析算法来进行垃圾对象的识别。具体过程是:从线程栈中的局部变量或者是方法区的静态变量出发,将这些变量的引用对象进行标记,然后看这些被标记的对象是否引用了其它对象,引用了就继续标记。所有被标记过的对象就是被使用的对象,其它未被标记的对象就是可以回收的垃圾对象。
进行标记以后,JVM会对垃圾对象占用的内存进行回收,回收的主要方法有以下三种:
清理:将垃圾对象占用的内存清理掉,将这些内存空间标记为空闲。
优点:回收不需要做过多动作,效率高。
缺点:回收后的空间不连续,造成内存碎片化。当需要分配大内存对象的时候可能会导致full gc
压缩:从堆空间的头开开始,将所有存货对象拷贝放在一段连续的内存空间中,那么剩余的内存空间就是连续的了。
优点:回收后的内存空间是连续的,不会造成内存碎片化。
缺点:压缩的痛多需要频繁拷贝对象,对会相对占用跟多系统资源
复制:将堆空间分成两份,只在其中一份里创建对象,当这部分空间用完了后,就将标记过的对象复制到另一份的空间里。
优点:回收后的内存空间是连续的,也不会频繁拷贝对象,只有创建对象的空间用完了才会
缺点:一半的空间不能用来创建对象。
垃圾回收算法
JVM性能诊断工具
基本工具:JPS,JSTAT,JMAP (命令行的JVM监控工具)
JPS:是用于查看有权访问的hotspot虚拟机的进程的 pid (jvmid),常见命令:
-l:输出java应用程序的 main class的完整包
-q: 仅显示pid
-m: 输出传递给 main 方法的参数
-v: 输出传递给JVM的参数。在诊断JVM相关问题的时候,可以通过该参数查看JVM相关参数设置
JSTAT: 是JDK自带的一个轻量级小工具。主要是对java应用程序的资源和性能进行实时的命令行监控,包括对heap size 和垃圾回收状况的监控。语法结构如下:jstat [Options] vmid [interval] [count]
Options:选项,一般用 -gcutil 查看gc情况
vmid: VM的进程号,即当前运行的java进程号
interval:间隔时间,单位为毫秒
count:打印次数,缺省则打印无数次
S0 -- Heap上的Survivor space 0 区已使用空间的百分比
S1 -- Heap上的Survivor space 1 区已使用空间的百分比
E -- Heap上的Eden space 区已使用空间的百分比
O -- Heap上的Old space 区已使用空间的百分比
YGC -- 从应用程序启动到采样时发生Young GC的次数
YGCT -- 从应用程序启动到采样时发生Young GC的时间,单位秒
FGC -- 从应用程序启动到采样时发生Full GC的次数
FGCT -- 从应用程序启动到采样时发生Full GC的时间,单位秒
GCT -- 从应用程序启动到采样时用于垃圾回收时间的总时间,单位秒
JMAP:监控内存内的Java对象,甚至可以将VM中的 heap 以二机制输出文本。使用方法:
jmap -histo pid >a.log
jmap -dump:format=b,filef1 PID ,可以将该PID进程的内存 heap 输出出来到f1文件里
集成工具:JConsole, JVisuaIVM (可视化的JVM监控工具)
Java代码优化
合理并谨慎使用多线程
启动线程数 = [任务执行时间 / (任务执行时间- IO等待时间) ] * CPU核心数
最佳启动线程数和 CPU核数成正比,和IO阻塞时间成反比。
如果任务都是CPU计算型任务,那么线程数最多不要超过CPU核数,因为启动再多线程,CPU也来不及调度,而且会因为频繁的线程上下文切换影响性能。
相反,如果任务需要等待磁盘操作,网络响应,那么启动多线程有助于提高任务并发度,提高系统吞吐能力,改善性能。
竞态条件与临界区
当两个线程同时竞争一个资源的时候,如果程序对资源访问的顺序敏感,那么就称存在竞态条件。导致竞态条件发生的代码区称为临界区。
在临界区使用适当的同步就可以避免竞态条件
Java线程安全
允许被多个线程安全执行的代码,称为线程安全的代码。
局部变量:存储在线程自己的栈中,永远不可能被其它线性访问到,所以基础类型的局部变量是线程安全的
局部的对象引用:如果在某个方法中创建的对象不会逃逸出方法,则会被逃逸分析优化为栈上创建相关对象,那么它也就是线程安全的。
对象成员:它存储在堆上,应用内所有的线程都可以共享,所以它是线程不安全的。
Java内存泄漏
Java内存泄漏是因为开发人员的错误引起的。
如果程序保留对永远不再使用的对象引用,这些对象将会占用内存并耗尽内存。
长生命周期对象
静态容器
缓存
合理使用线程池和对象池
复用线程或对象资源,避免在程序的生命周期中创建和删除大量对象
池管理算法(记录那些对象是空闲的, 那些对象是在使用的)
对象内容清除(ThreadLocal清空)
使用合适的JDK容器类(顺序表,链表,Hash)
LinkList 和 ArrayList的区别和适用场景
区别:ArrayList是基于数组的有序集合,LinkList是基于双向链表的有序集合
适用场景:ArrayList适用于查询多的场景,它基于数组实现,可以基于下标进行快速随机访问;LinkList适用于插入、删除多多场景,它基于双向链表,插入和删除只需要改对应对象的前一个对象指针以及后一个对象的指针即可。
HashMap的算法实现及应用场景。
算法实现:HashMap是基于数组来实现的,数组的每个对象又是一个链表对象。将key做Hash 取到下标,然后将值放到对应下标的位置。当Hash冲突的时候,放到对应下标的链表中的下一个位置。
适用场景:因为它查询效率很高,一般适用于查询较多的场景。
使用 concurrent 包,ConcurrentHashMap和 HashMap的线程安全特性有什么不同?
HashMap线程不安全的,ConcurrentHashMap是线程安全的。
ConcurrentHashMap的线程安全早期实现:
分离锁,也就是将内部进行分段(Segment),里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。
HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的。
ConcurrentHashMap的线程安全在Java 8 和之后的版本中实现:
总体结构上,它的内部存储变得和 HashMap 结构非常相似,同样是大的桶(bucket)数组,然后内部也是一个个所谓的链表结构(bin),同步的粒度要更细致一些。
其内部仍然有 Segment 定义,但仅仅是为了保证序列化时的兼容性而已,不再有任何结构上的用处。
因为不再使用 Segment,初始化操作大大简化,修改为 lazy-load 形式,这样可以有效避免初始开销,解决了老版本很多人抱怨的这一点。
数据存储利用 volatile 来保证可见性。
使用 CAS 等操作,在特定场景进行无锁并发操作。
使用 Unsafe、LongAdder 之类底层手段,进行极端情况的优化。
缩短对象生命周期,加速垃圾回收
减少对象驻留内存时间
在使用时创建对象,用完释放。
创建对象的步骤(静态代码段 - 静态变量 - 父类构造函数 - 子类构造函数)
使用 I/O buffer 和 NIO
延迟写和提前读策略
异步无阻塞IO通信
优先使用组合,组合代替继承
减少对象耦合
避免太深的继承层次带来的对象创建性能损失
合理使用单例模式
无状态对象
线程安全对象
虚拟化所有层次
计算机的任何问题都可以通过间接层来解决
一致性hash算法的虚拟化实现
面向接口编程
7层网络协议
性能优化案例剖析
秒杀系统
设计原则:
静态化:
采用JS自动更新技术将动态页面转化为静态页面
并发控制、防秒杀器:
设置阀门,只放最前面进入系统的用户
三道阀门的设计:
限制进入秒杀页面数:1000
限制进入下单页面数:100
限制进入支付系统:56
简化流程:
砍掉不必要的分支流程
以下单成功作为秒杀成功标志,支付流程在一天内完成即可
前端优化:
采用YSLOW 原则提升页面响应速度
秒杀器的预防
秒杀Detail页面:
URL:随机
秒杀前两秒放入,脚本生存,秒杀前。
1000次访问次数控制
下单页面:
订单ID随机
不能跳过秒杀进入Detail页面
每个秒杀商品,带预先生成的随机Token到url参数中
秒杀过时间,直接跳至秒杀结束页面
100次访问上限控制
Web Server调优
Apache调优
KeepAlive参数调优
其它参数调优
HostnameLookups设为off,对allowfromdomain等不进行正向和反向dns解析
关闭cookies-log 日志
打开 Linux sendfile()
关闭无用的 module
mod_Gzip: 秒杀页面非图片html所占流量比重可忽略不计,zip压缩意义不大
mod_Beacon
mod_hummock (反应不够及时,不适合秒杀场景)
JBoss调优
Mod-jk worker 调优
JBoss AJP Connector
Tomcat ARR 设定
秒杀静态页面优化
图片合并
多张图片合成一张,CSS偏移展示
减少http请求数,减少请求等待数
减少发送cookies的量
HTML 内容压缩
图片压缩
HTML Header Cache-Control设置
CSS,js 精简
CSS,JS 精简到极致,,部分直接写到页面中,减少http请求次数。
下单页面优化
数据库操作:全部砍掉
秒杀流程精简
砍掉填写或选择收货地址,放到秒杀成功后填写
砍掉调用是否开通支付接口,秒杀首页文案提示必须开通成功
采用内存缓存:秒杀Offer数据,支付相关信息
交易系统性能优化
确认交易系统调优目标:下单页面优化到40并发,TPS 400
关闭KeepAlive
JVM优化
优化CMS垃圾回收的参数
消灭TOP 10 Bottlenecks
Velocity 参数调优
采用 DBCP1.4替换C3P0
Offer产品参数的xml解析
应急预案
域名分离,独立域名,不影响其它的业务访问
机动服务器10台,备用
拆东墙补西墙策略
壁虎断尾策略
所有办法均失效的情况下,比如流量耗尽
非核心应用集群统统停止服务
保住首页,Offer Detail
万能出错页:秒杀活动已经结束
任何出错都302跳转到该页面
页面位于其它集群
活动结束后复盘
改进一:采用更轻更快的服务器
采用Lighttpd替代 Apache杀手锏 (AIO)
关键性能(I/O)对比
改进二:前端优化自动化
图片自动压缩(CMS自动压缩)
Cookies服务化(控制Cookies 大小)
前端延迟加载框架 SmartLoad(只加载首屏数据)
Google mod pagespeef module :自动压缩图片,静态资源,智能浏览器缓存技术
Google Diffable :增量下载静态资源技术
改进三:架设镜像站组件山寨 CDN
根据用户请求ipDNS重定向到最近城市的服务器上
改进四:采用反向代理,加速核心页面
通过反向代理分发前端请求
改进五:海量数据透明的垂直切分
评论