架构师训练营 -W09S- 性能优化
JVM虚拟机原理
JVM组成架构
Java是一种跨平台的语言,JVM屏蔽了底层系统的不同,为Java字节码文件构造了一个统一的运行环境
每个环境的执行引擎是不一样的
字节码.class文件可以在所有环境上执行
组成
类加载器
加载到方法区
运行期数据区
所有线程共享的运行期数据区
方法区(元空间)
堆
创建的对象都放在堆中
每个线程独享的运行期数据区
Java栈
程序计数寄存器
给线程使用,记录字节码执行到哪一行
执行引擎
Java字节码文件
计算机领域的任何问题都可以通过增加个中间层(虚拟层)来解决
Java所有指令有200个左右
一个字节(8位)可以存储256种不同的指令信息,一个这样的字节称为字节码(Bytecode)
JVM讲字节码解释执行,屏蔽对底层操作系统的依赖
JVM也可以讲字节码编译执行,如果是热点代码,会通过JIT动态地编译为机器码,提高执行效率
.class字节码文件
cafe babe
字节码执行流程
方法调用(字节码指令)-->执行引擎
-->未编译-->方法调用计数器加1
-->计数器超过阈值(热点代码)
-->提交编译请求-->编译器-->后台执行编译成可执行机器码-(放入)->Code Cache
-->(解释器)解释方法执行-->方法返回
-->计数器未超过阈值-->(解释器)解释方法执行-->方法返回
-->已编译-->执行编译后的机器码(来自Code Cache)-->方法返回
字节码的编译过程
Java源文件-->词法解析--(token流)-->语法解析-(语法数)->语义分析-->生成字节码-->字节码
类加载器的双亲委托模型
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类
如果低层次的类加载器想加载一个未知类,需要上级类加载器确认,只有当上级类加载器没有加载过这个类,也允许加载的时候,才让当前类加载器加载这个未知类
自定义类加载器
隔离加载类
同一个JVM中不同组件加载同一个类的不同版本
扩展加载源
从网络、数据库等处加载字节码
字节码加密
加载自定义的加密字节码,在ClassLoader中解密
堆 & 栈
堆
每个JVM实例唯一对应一个堆
应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享
堆栈
JVM为每个新创建的线程都分配一个堆栈
对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的
Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配的
Java在建立一个对象时从两个地方都分配内存
在堆汇总分配的内存实际建立这个对象
在堆栈中分配的内存只是一个只想这个堆对象的引用而已
方法区 & 程序计数器
方法区主要存放从磁盘加载进来的类字节码
在程序运行过程中创建的类实例则存放在堆里
程序运行时是以线程为单位运行的
当JVM进入启动类的main方法的时候,就会为应用程序创建一个主线程,main方法里的代码就会被这个主线程执行
每个线程有自己的Java栈,栈里存放着方法运行期的局部变量
当前线程执行到哪一行字节码指令,这个信息则被存放在程序计数寄存器
Java(线程)栈
所有在方法内定义的基本类型变量,都会被每个运行这个方法的线程放入自己的栈中
线程的栈彼此隔离,所以这些变量一定是线程安全的
线程工作内存 & volatile
Java内存模型规定在多线程情况下,线程操作主内存变量,需要通过线程独有的工作内存拷贝主内存变量副本来进行
一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,保证了不同线程堆这个变量进行操作的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
Java运行环境
Java编译环境
Java源文件(java文件)-->Java编译器 -->Java字节码(class文件)
Java运行期环境
Java字节码(class文件)-->字节码本地或网络-->类装载器 字节码验证(Java类库)
-->即时编译器-->运行期系统-->操作系统-->硬件
-->Java解释器-->运行期系统-->操作系统-->硬件
JVM的垃圾回收
JVM垃圾回收就是将JVM堆中的已经不在被使用的对象清理掉,释放宝贵的内存资源
解决四方面的问题
1、需要知道哪些对象需要回收
2、如何回收
3、回收过程中的内存空间如何管理
4、用什么过程进行回收
JVM通过一种可达性分析算法进行垃圾对象的识别
具体过程
1、从线程栈帧中的局部变量,或者是方法区的静态变量出发,将这些变量引用的对象进行标记
2、看这些被标记的对象是否引用了其他对象,继续进行标记
3、所有被标记的对象是被使用的对象,而没有被标记的对象就是可回收的垃圾对象了
从对象的根(静态变量、线程栈)出发,依次(递归)标记被引用的对象,标记完所有的对象后,没有被标记的对象就是可回收的垃圾对象
标记完以后,JVM就会对垃圾对象占用的内存进行回收,主要有四种方法
清理回收算法
将垃圾对象占据的内存清理掉,其实JVM并不会真的将这些垃圾内存进行清理,而是将这些垃圾对象占用的内存空间标记为空闲,记录在一个空闲列表里,当应用程序需要创建新对象的时候,就从空闲列表中找一段空闲内存分配给这个新对象
压缩回收算法
从堆空间的头部开始,将存活的对象拷贝放在一段连续的内存空间中,那么其余的空间就是连续的空闲空间
复制回收算法
将堆空间分成两部分,只在其中一部分创建对象,当这个部分空间用完的时候,将标记过的可用对象复制到另一个空间中
分代垃圾回收算法
新生代
程序新创建的对象放在新生代,分配内存空间较小,比老年代空间小很多,很大比例的内存空间都可回收,回收速度快
分区
Eden区
From区
To区
老年代
处理过程
1、新分配对象创建在Eden区
2、Eden区满后,启动一次新生代的垃圾回收
3、在垃圾回收过程中引用的对象拷贝到From区,Eden区又变为连续空闲空间
4、Eden区又满后,标记Eden区和From区的引用对象,全部拷贝至To区,Eden区和From区都为连续空闲空间
5、Eden区再满后,标记Eden区和To区的引用对象,全部拷贝至From区,Eden区和To区此时都为连续空闲空间
6、Eden区+From区和Eden区+To区反复拷贝,经历多次后还在存活的对象就会被拷贝到老年代
7、持续运行一段时间后,已无法拷贝到From区或To区,就会进行一次全量的垃圾回收,新生代和老年代进行一次全量回收,然后将新生代放不下的对象放入到老年代
JVM垃圾回收器
一个或几个线程承担标记、拷贝、清理空间等回收工作
串行回收器
单核CPU时代
当需要进行垃圾回收时,停止所有运行的应用程序线程(stop-the-world),启动一个垃圾回收线程进行垃圾回收,清理完成后,恢复应用程序线程继续运行
并行回收器
多核CPU时代
当需要进行垃圾回收时,停止所有运行的应用程序线程(stop-the-world),启动多个垃圾回收线程进行垃圾回收,清理完成后,恢复应用程序线程继续运行
效率高于串行垃圾回收
需要停下所有应用的线程,用户体验差
并发回收器CMS
并发标记清理
四个阶段
1、启动一个线程进行初始化标记,至标记根
2、并发标记,垃圾回收线程和应用程序线程并发执行
3、由于在并发标记过程中有可能会漏标记应用新创建的对象,因此还需要停止所有应用程序线程(stop-the-world),启动多个垃圾回收线程进行重标记(时间很短)
4、垃圾回收线程和应用程序线程并发执行,垃圾回收线程进行并发清理
早期web应用使用,消耗资源比并行回收器要高
G1回收器
Java 1.7-1.8版本
G1垃圾回收内存管理机制
分区(2000个区域)
Eden
Survivor
Old
Humongous(大对象区)
过程也是stop-the-world,启动多个垃圾清理线程进行清理,但只清理一小部分区域
Java启动参数
标准参数,所有的JVM实现都必须实现这些参数的功能,而且向后兼容
运行模式
-server 客户端
-client 服务端
类加载路径
-cp
-classpath
运行调试
-verbose
系统变量
-D
非标准参数,默认JVM实现这些参数,但不保证所有JVM实现都实现,且不保证向后兼容
-Xms 初始堆大小
-Xmx 最大堆大小
-Xmn 新生代大小
-Xss 线程堆栈大小
非Stable(稳定)参数,此类参数各个JVM实现会有所不同,将来可能会随时取消
-xx:-UseConcMarkSweepGC 启动CMS垃圾回收
JVM性能诊断工具
基本工具
JPS
用来查看host上运行的所有java进程的pid(jvmid)
一般用来找出运行的JVM进程ID,即lvmid,然后进一步使用其他工具来监控和分析JVM
常用参数
-l
输出java应用程序的main class的完整包
-q
仅显示pid,不限时其它任何相关信息
-m
输出传递给main方法的参数
-v
输出传递给JVM的参数,诊断JVM问题时,这个参数可以查看JVM相关参数的设置
JSTAT
Java Virtual Machine statistics monitoring tool
JDK自带的一个轻量级小工具
主要堆Java应用程序的资源和性能进行实时的命令行的监控,包括堆Heap size和垃圾回收状况的监控
语法:jstat [Options] vmid [interval] [count]
Options --选项,一般用-gcutil查看gc情况
vmid --VM进程号,即当前运行的java进程号
interval --间隔时间,单位为毫秒
count --打印次数,如果缺省则打印无数次
JMAP
JMAP是一个可以输出所有内存中对象的工具,甚至可以将VM中的heap,以二进制输出成文本
使用方法
jmap -histo pid>a.log 一段时间后使用文本比对工具,可以对比出GC回收了哪些对象
jmap -dump:format=b,file=f1 PID 可以将该PID进程的内存heap输出出来到f1 文件里
JSTACK
可以查看jvm内的线程堆栈信息
集成工具(可视化)
JConsole
JVisualVM
Java代码优化
合理并谨慎使用多线程
启动线程数 = [任务执行时间/(任务执行时间 - IO等待时间)] * CPU内核数
最佳启动线程数和CPU内核数量成正比,和IO阻塞时间成反比。
计算密集型
线程数量不应超过CPU的内核数
I/O密集型
多启动线程有助于提高任务并发度,提高系统吞吐能力,改善系统性能
关注
使用场景(I/O阻塞,多CPU并发)
资源争用与同步问题
java.util.concurrent
静态条件与临界区
同一程序中运行多个线程;本身不会导致问题,问题在于多个线程访问了相同的资源
当两个线程竞争同一个资源时,如果堆资源的访问顺序敏感,就存在竞态条件
导致竞态条件发生的代码区称作临界区
在临界区使用适当的同步就可以避免竞态条件
Java线程安全
允许被多个线程安全执行的代码称作线程安全代码
方法局部变量
局部变量存储在线程自己的栈中,永远不会被多个线程共享,所以基础类型的局部变量是线程安全的
方法局部的对象引用
如果在某个方法中创建的对象不会逃逸出该方法,那么它就是线程安全的
对象成员变量
对象成员存储在堆上,如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的
问题
java Web应用的多线程从哪儿来的?
Servlet是线程安全的吗?
ThreadLocal
创建一个ThreadLocal变量(X类静态成员变量)
public static ThreadLocal myThreadLocal = new ThreadLocal();
存储此对象的值(A类a方法)
X.myThreadLocal.set(“A thread local value”);
读取一个ThreadLocal对象的值(B类b方法)
String threadLocalValue = (String)X.myThreadLocal.get();
Java内存泄漏
Java内存泄漏是由于开发人员的错误引起的
如果程序保留堆永远不再使用的对象的引用,这些对象将会占用并耗尽内存
长生命周期对象
静态容器
缓存
合理使用线程池和对象池
复用线程或对象资源,避免在程序的生命周期中创建和删除大量对象
池管理算法(记录哪些对象是空闲的,哪些对象正在使用)
对象内容清除(ThreadLocal的清空)
使用合适的JDK容器类(顺序表,链表,Hash)
LinkList和ArrayList的区别及使用场景
HashMap的算法实现及应用场景
使用concurrent包,ConcurrentHashMap和HashMap的线程安全特性有什么不同?
缩短对象生命周期,加速垃圾回收
减少对象驻留内存的时间
在使用时创建对象,用完释放
创建对象的步骤(静态代码段 - 静态成员变量 - 父类构造函数 - 子类构造函数)
使用I/O buffer 及 NIO
延迟写与提前读策略
异步无阻塞IO通信
优先使用组合代替继承
减少对象耦合
避免太深的继承层次带来的对象创建性能损失
合理使用单例模式
无状态对象
线程安全
计算机的任何问题都可以通过虚拟层(或者中间层)解决
面向接口编程
七层网络协议
JVM
编程框架
一致性hash算法的虚拟化实现
高性能秒杀案例
秒杀是营销活动
集中在某个时间点发生超高并发请求
并发用户和并发请求是系统的压力
思考点
1、旧系统重构、优化
时间问题
只能逐步优化迭代,不可能一步到位(优化50倍)
部门、团队间的沟通问题
2、构建新的秒杀系统
开发时间快
开发新系统远远优于维护迭代旧系统
低风险高回报
但不容易得到开发新系统的机会
XXXX.com秒杀系统案例
问题与挑战
1、瞬间高并发问题
2、网络带宽耗尽问题
3、服务器崩溃问题
4、数据库瘫痪问题
5、秒杀器问题
解决方案
服务器和网络准备
服务器准备
style服务器(Lighttpd集群):5台
图片服务器(Nginx集群):5台
静态服务器(Apache集群):10台
交易服务器(JBoss动态集群):10台
带宽准备
图片出口带宽上限2.5G
出口带宽支持10G,但受限于图片服务器集群的处理能力:图片服务集群最大并发处理能力 * 网站平均图片大小 = 2.5G
CDN准备
CHinacache沟通;借用CCCC CDN
架构目标
图片网络带宽:1.0G
新增图片带宽控制在1.0G左右
每件商品秒杀页面的图片总大小不得超过:1000000/(1000*8) = 125K/每商品
网站并发
单件商品并发:1000 (预估)
总并发:8(件商品) * 1000(人/商品) = 8000
组成
简单系统
三个页面
XXXX.com静态集群
秒杀商品列表
秒杀商品介绍
图
产品文字
介绍
购买按钮
XXXX 交易动态集群
填写订单页面
下单表单
提交按钮
下单成功后,进入支付系统,走支付流程
设计原则
静态化
不访问数据库
采用JS自动更新技术将动态页面转化为静态页面
并发控制,防秒杀器
设置阀门。只放最前面的一部分人进入秒杀系统
简化流程
砍掉不重要的分支流程,如下单页面的所有数据库查询
以下单成功作为秒杀成功标志,支付流程只要在1天内完成即可
前端优化
采用YSLOW原则提升页面响应速度
优化措施
静态化
秒杀商品list和Detail是静态HTML页面
秒杀商品列表/秒杀商品介绍页面,如何判断秒杀是否开始?
并发控制,防秒杀器
三道阀门的设计
阀门:基于TT的计数器
1、限制进入秒杀页面,1000
2、限制进入下单页面,100
3、限制进入支付系统,56
秒杀器的预防
秒杀Detail页面
URL:随机
秒杀前2秒放出,脚本生成,秒杀前
1000次访问上限控制(每件商品只能放入1000人浏览)
下单页面
订单ID,随机
不能直接跳过秒杀Detail页面进入
每个秒杀商品,带预先生成的随机Token作URL参数
如果秒杀过,直接跳到秒杀结束页面
100次访问上限控制(每件商品只能放入100人下单)
Web Server调优
Apache调优
KeepAlive相关参数调优
其它参数调优
关闭cookies-log日志
打开Linux sendfile()
关闭无用的module
JBoss调优
Mod-jk worker调优
JBoss AJP Connector
Tomcat APR设定
秒杀静态页面优化
图片合并
HTML内容压缩
图片压缩
HTML Header Cache-Control设置
CSS、JS精简
下单页面优化
全部砍掉数据库操作
秒杀流程精简
砍掉填写或选择收获地址,放在秒杀成功后填写
砍掉调用是否开通支付接口,秒杀首页文案提示必须开通
采用内存缓存
秒杀Offer数据,支付相关信息,缓存
交易系统性能优化
关闭KeepAlive
JVM优化
优化CMS垃圾回收器的参数
消灭Top10 Bottlenecks
二跳页面的优化
XXXX.com其他页面
前端优化:Yslo规则调优
图片压缩
避免发送Cookies
交易系统优化
普通订单管理列表和XXXX秒批订单管理列表分离
禁止用模糊查询功能
应急预案
域名分离,独立域名,不影响XXXX原有业务
Style集群:style.XXXX.china.XXXX.com
图片服务器集群 :img.XXXX.china.XXXX.com
静态页面集群:page.XXXX.china.com
出问题直接把XXXX相关域名卡掉,所有请求跳到万能出错页面
机动服务器10台,备机
拆东墙补西墙策略
SA待命,随时准备将非核心应用集群的冗余服务器下线,加入到秒杀集群
壁虎断尾策略
所有办法均失效的情况下,例如流量耗尽
非核心应用集群统统停止服务,如咨询、论坛、博客等社区系统
保住首页,Offer Detail,旺铺页面等核心应用的可能性
万能出错页:秒杀活动已经结束
任何出错都302跳转到此页面
位于另外集群
万幸:最终所有的预案都没有用上
后续改进
采用更轻量/快速的服务器
采用Lighttpd替代Apache杀手锏(AIO)
使用不同的部署方案提升性能
前端优化自动化
架设镜像站组建山寨CDN
采用反响代理加速核心页面
海量数据的透明垂直切分
评论