极客大学 - 架构师训练营 第九周
第九周 性能优化(三)
第九周感悟
眼界和格局
不要让自己只局限于小组内,要慢慢的学会和争取做跨部门的沟通和分享,在整个公司或者企业内要解决企业内的问题,做出NB的工具、框架、软件,让别的部门或者小组来依赖你做出来的产品。
低风险,高回报
做新的东西(新系统、接口、模块等)是低风险,高回报的事情,因为是从无到有,别人都知道是你做的。而且相对来说设计、开发、验证时间比较充裕。
但维护老的系统是高风险,低回报的,因为维护的好,别人会认为是应该的。维护的不好,那么所有的矛头也都会指向你,一般维护这种项目通常维护时间较为紧迫、沟通成本较高(可能老系统牵扯到各方的利益)。
数据库的基本原理
无论是构建什么样的应用,大都离不开数据。而在应用的架构设计中,如何设计数据库,使用什么类型的数据库,就是一个架构师必须了解的。所有的数据库的共同点都是以某种方式存储数据,以某种接口来访问存储的数据。
关系型数据库
关系型数据库以数据表Table为核心来存储数据。数据是一行一行的表记录Record。表之间通过关联关系相互关联。 关系模型是表(行,列)组成的二维结构。SQL是关系型数据库的统一查询接口。
关系型数据库的架构设计,主要是要解决存储和事务。存储是要解决数据的查询问题。而事务则包含了四个特性,A(原子性)C(一致性)I(隔离性)D(持久性)
数据库架构
连接器:为每个请求分配一块专用的内存空间用于会话上下文。应用启动时通常会创建一个连接池用于加速数据库连接。
语法分析器:检测 SQL 语法是否正确
语义分析与优化:将常见的复杂嵌套语句转化成更高效的索引语句,但是种类比较有限吧
执行引擎:生成执行计划,可以用
explain
查看执行计划包含的信息,一般包括 Id、type、key,rows 等等信息。
数据库索引
索引的存在是为了提高数据查询的性能。索引通常是B/B+树。在没有索引的情况下,通过ID来查找一条数据记录的时间复杂度是O(n),也就是说随着数据量的增加,查询的速度随线性增加。这个是用户不能接受的。在建立了索引之后,如上图所示,访问记录需要从树的根节点通过两次跳转,达到记录。也就是说访问的速度取决于树的深度。这样的时间复杂度为 O(log n) 。访问速度受数据量的影响非常小。(其实在海量数据的情况下,即使是O(log n)也是个问题)
数据库日志
日志是关系型数据库的另一个核心架构设计概念。因为每一个数据库的操作都会以日志的形式记录。系统可以方便的进行事务的回滚。利用日志。系统可以通过日志回放的方式,构建任何一个时间点的系统状态。
优点:
通过事务处理保持数据的一致性
数据更新的开销很小
可以进行Join等复杂查询
20多年的技术历程,技术成熟
缺点:
数据读写必须经过sql解析,大量数据、高并发下读写性能不足
为保证数据一致性,需要加锁,影响并发操作
无法适应非结构化的存储
数据库中存储的对象与实际的对象实体有一定的差别
水平扩展困难,很难提供高扩展性和高可用性。
数据库庞大,价格昂贵
JVM虚拟机架构原理
Java的运行环境
主要过程:
Java 是一种跨平台的语言,JVM 屏蔽了底层系统的不同,java源文件可以由JVM解析为 Java 字节码文件,JVM为Java字节码文件构造了一个统一的运行环境。
其中,java字节码文件编译过程为:
而Java 字节码执行流程则为
Java字节码文件内容都是字节码。Java 所有的指令有200个左右,一个字节码(8位)可以存储256中不同的指令信息,一个这样的字节称为字节码(Bytecode)。在代码的执行过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖,JVM也可以将字节码编译执行,如果是热点代码,会通过JIT动态的编译为与操作系统相关的机器码,提高执行效率。字节码内容如图
其中开头 cafa baba是java字节码文件特定的魔法值,表示文件是一个java字节码文件,0000 0037表示jdk的版本号
java字节码文件的执行流程
类加载的双亲委托模型
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,需要上级加载器类确认,只有当上级记载器类没有加载过这个类,也允许加载的时候,才让当前类加载器加载这个未知类。目的就是为了防止一个类重复加载,一个类多个版本同时被加载。
类加载器的种类:
启动类加载器:这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。
扩展类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。
应用程序类加载器:这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。
自定义加载器:用户自己定义的类加载器。
JVM的内存模型
JVM 堆 + 栈 + 方法区 + 程序技术器
堆:每个JVM实例唯一对应一个堆。应用程序在运行中所创建的类的实例或数组都放在这个堆中,并有应用所有的线程共享
堆栈:JVM为每个新创建的线程都分配一个堆栈。也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。
方法区 + 程序计数器:方法区主要存放从磁盘加载进来的类字节码,二在程序运行过程中创建的类实例则存放在堆里。程序运行的时候,实际上是以线程为单位运行的,当JVM进入启动类的main方法的时候,就会为应用程序创建一个主程序类,main 方法里的代码就会被这个主线程执行,每个线程有自己的 Java栈,栈里存放着方法运行期的局部变量。而当前线程执行到哪一行字节码指令,这个信息则被存放在程序技术寄存器。
JVM 的垃圾回收
JVM的垃圾回收就是讲JVM堆中的已经不再被使用的对象清理掉,释放宝贵的内存资源。
JVM 通过一种可达性分析算法进行垃圾对象的识别,具体过程是:从线程栈桢中的局部变量,或者方法区的静态变量出发,将这些变量引用的对象进行标记,然后看这些被标记的对象是否引用了其他对象,继续进行标记,所有被标记的对象都是被使用的对象,而那些没有被标记的对象就是可回收的对象。
垃圾对象标记的方法
引用计数法: 基本想法就是 一个对象,如果有地方在引用它,那么它就不是垃圾,因此呢,我给这个对象创建一个引用计数器,对象 被引用一次,计数器就+1,引用失效,计数器-1,垃圾回收的时候一看这个对象计数器值为0,说明这个对象没被引用了,就被当作垃圾回收了。这个算法非常高效,但是存在的问题就是两个对象互相引用,这样这两个对象就永远无法被回收。
根搜索法: 这个方法的基本思路是,从一些根节点,称为“GC Roots”,为起始点往下搜索,形成一条引用链,在引用链之外的对象被视作垃圾。java语言中的“GC Roots”有如下几种:帧栈中本地变量表中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。
垃圾回收使用的算法
标记清除算法:在这种回收机制中,分为两个过程,一是标记,再就是清除,标记阶段就是用根搜索法来判断对象是否需要回收,清除就是回收内存空间。这个算法是垃圾回收中最基本的算法,许多高级的垃圾回收算法都是根据它为基础。这个算法的缺点是效率低,回收后的内存空间过于碎片化。
复制算法:针对标记清除算法的效率问题,复制算法给出的解决方法是,将内存空间一分为二,每次只用其中一块空间,当进行垃圾回收时,把活着的对象复制到另一块空间中,清除时将原空间全部清除,这样效率既高也解决了内存碎片,缺点就是太废内存了。
标记整理算法:过程和标记清除算法类似,区别就是它不直接把垃圾空间清除掉,而是将活着的对象都移动到一起,剩下的空间就是垃圾空间了,这时候直接清除掉就行。
分代回收算法:分代回收法就是根据对象的生存周期,分为老年代和新生代两个内存空间,对于新生代这种对象生成和死亡都比较快的区域使用复制算法进行回收,对于老年代则使用标记清除或标记整理算法。
垃圾回收器分类
串行回收器:一个垃圾回收线程,stw(stop-the-world)停止所有的用户线程回收。
并行回收器:多个垃圾回收线程,stw停止所有线程回收垃圾。
并发回收期CMS分为以下4个阶段:
初始标记:标记一下GC Roots能直接关联到的对象,会“Stop The World”。
并发标记:GC Roots Tracing,可以和用户线程并发执行。
重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。
并发清除:清除对象,可以和用户线程并发执行。
G1回收器:其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)器垃圾回收过程如下:
初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)
JVM 的性能诊断工具
基本工具:
JPS:用来查看运行的所有java进程的pid
https://docs.oracle.com/javase/7/docs/technotes/tools/share/jps.html
JSTAT:JVM统计监测工具
https://docs.oracle.com/javase/7/docs/technotes/tools/share/jstat.html
JSTACK:用来查看某个Java进程内的线程堆栈信息。
https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr016.html
JMAP:是一个可以输出所有内存中对象的工具
https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr014.html
集成工具:
JConsole:一个内置Java性能分析器,可以从命令行或在GUI shell中运行。
https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html
JVisualVM:JDK提供的一个JVM运行监控工具,可用于查看dump文件,CPU及内存的占用情况,GC垃圾回收情况等信息
Java代码优化技巧及原理
多线程:合理并谨慎使用
使用场景:IO/阻塞,需要多cpu并发
带来的问题 :资源争用于同步问题
合理使用:使用juc并发包,启动线程数=[任务执行时间/(任务执行时间-IO等待时间)*cup内核数]。 cup密集型任务 线程数最多不超过CPU内核数,因为启动再多线程,CPU也来不及调度。对于IO密集型任务,多启动线程有助于提高任务并发度,提升高系统吞吐能力,改善系统性能。
竞态条件与临界区
两个线程竞争统一资源时,如果对资源的访问顺序敏感,就存在竞态条件。倒置竞态条件发生的代码去称作临界区。
避免竞态条件:在临界区中适当使用同步。
内存泄漏优化
原因:程序保留了不会在使用的对象的引用,垃圾回收器无法回收,这些对象会慢慢耗尽内存。
容易内存泄漏的点 : 长生命周期对象,竞态容器,缓存
避免方式:
复用线程对象资源,避免程序生命周期中创建和删除大量对象
池管理算法(记录哪些对象是空闲的,哪些对象正在使用)
对象内容清除(ThreadLocal 清空)
使用合适的JDK容器类提高程序效率和保证线程安全性
缩短对象生命周期,加速垃圾回收
减少对象驻留内存的时间
在使用时创建对象,用完释放
IO方面
使用I/O buffer 及 NIO提升IO操作的效率
延迟写与提前读策略
使用异步无阻塞IO通信
优先使用组合代替集成
减少对象耦合
避免太深的集成层次带来的对象创建性能损失 (对象创建的步骤:静态代码段-静态变量-父类构造函数-子类构造函数)
合理单例模式
使用无状态的单例对象
保证线程安全
JVM虚拟机跨平台的思想的启示:
计算机领域的任务问题都可以通过增加中间层(虚拟层)来解决问题
如:JVM虚拟机,相当于操作与java代码的的中间层,解决java跨平台问题 代码提倡面向接口编程,接口相当于一个中间层
版权声明: 本文为 InfoQ 作者【9527】的原创文章。
原文链接:【http://xie.infoq.cn/article/3141346f60f06da80632d4ff9】。文章转载请联系作者。
评论