架构师训练营 -W09S- 性能优化

用户头像
BlazeLuLu
关注
发布于: 2020 年 08 月 05 日

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

  • 采用反响代理加速核心页面

  • 海量数据的透明垂直切分



用户头像

BlazeLuLu

关注

还未添加个人签名 2018.05.30 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营 -W09S- 性能优化