两种测试范围
微基准性能测试
微基准性能测试可以精准定位到某个模块或者某个方法的性能问题,特别适合做一个功能模块或者一个方法在不同实现方式下的性能对比。例如,对比一个方法使用同步实现和非同步实现的性能。
微服务下由单元测试
宏基准性能测试
宏基准性能测试是一个综合测试,需要考虑到测试环境、测试场景和测试目标。
String注意事项
String.intern减少堆内存使用量
在类中,如果String是被重复使用的情况下,:
Location.setCity(messageInfo.getCity().intern());
Location.setCountryCode(messageInfo.getRegion().intern());
String.split 注意事项
split 两种不使用正则表达式进行拆分的情况
第一种为传入的参数长度为1,且不包含“.$|()[{^?*+\”regex元字符的情况下,不会使用正则表达式;
第二种为传入的参数长度为2,第一个字符是反斜杠,并且第二个字符不是ASCII数字或ASCII字母的情况下,不会使用正则表达式。
split使用正则表达式导致慢的情况下,需优化正则表达式
正则表达式优化
减少捕获嵌套
正则表达式减少使用()组捕获的嵌套
尽量减少贪婪模式,多使用独占模式
贪婪模式 regex=“ab{1,3}c” ,对于"abbc" 的NFA自动机会产生回溯,先使用abbb跟"abb"匹配,然后回溯为abb匹配,匹配成功后,再依次向后匹配
惰性模式 regex=“ab{1,3}?c” 使用?开启惰性模式,惰性模式下,会先以最少量的b去匹配,一旦匹配成功,就向后匹配,避免回溯。(先ab 然后abb 然后abbb)
独占模式regex=“ab{1,3}+c” 使用+开启独占模式,独占模式下,会以最多的b去匹配,一旦匹配成功,就先后匹配避免回溯。
例子:
使用“ab{1,3}+c” 匹配abbc
ArrayList 还是 KinkedList
在不发生扩容的情况下:
从集合头部新增元素 ArrayList 慢于 LinkedList
中部和尾部新增元素ArrayLisy快于LinkedList
在元素容量已知,初始化容量足够的情况下,元素基本从尾部插入的情况下,使用ArrayList。
在元素容量未知,动态扩容的情况下,使用LinkedList。
需要知道元素位置的情况下,使用ArrayList。
如果只使用迭代器循环的情况下,两List查询效率几乎相同。
HashMap注意事项
降低hash冲突,从而减少链表的产生
为什么hash算法要采用对象的hashcode = (ObjectHash) ^ (ObjectHash>>>16);注 hashcode 为32位下
目的是尽可能的打乱参与运算的低位。
(容量-1)&hashcode
设置好的初始容量及加载因子,降低扩容带来的问题。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//1、判断当 table 为 null 或者 tab 的长度为 0 时,即 table 尚未初始化,此时通过 resize() 方法得到初始化的 table
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//1.1、此处通过(n - 1) & hash 计算出的值作为 tab 的下标 i,并另 p 表示 tab[i],也就是该链表第一个节点的位置。并判断 p 是否为 null
tab[i] = newNode(hash, key, value, null);
//1.1.1、当 p 为 null 时,表明 tab[i] 上没有任何元素,那么接下来就 new 第一个 Node 节点,调用 newNode 方法返回新节点赋值给 tab[i]
else {
//2.1 下面进入 p 不为 null 的情况,有三种情况:p 为链表节点;p 为红黑树节点;p 是链表节点但长度为临界长度 TREEIFY_THRESHOLD,再插入任何元素就要变成红黑树了。
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//2.1.1HashMap 中判断 key 相同的条件是 key 的 hash 相同,并且符合 equals 方法。这里判断了 p.key 是否和插入的 key 相等,如果相等,则将 p 的引用赋给 e
e = p;
else if (p instanceof TreeNode)
//2.1.2 现在开始了第一种情况,p 是红黑树节点,那么肯定插入后仍然是红黑树节点,所以我们直接强制转型 p 后调用 TreeNode.putTreeVal 方法,返回的引用赋给 e
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//2.1.3 接下里就是 p 为链表节点的情形,也就是上述说的另外两类情况:插入后还是链表 / 插入后转红黑树。另外,上行转型代码也说明了 TreeNode 是 Node 的一个子类
for (int binCount = 0; ; ++binCount) {
// 我们需要一个计数器来计算当前链表的元素个数,并遍历链表,binCount 就是这个计数器
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
// 插入成功后,要判断是否需要转换为红黑树,因为插入后链表长度加 1,而 binCount 并不包含新节点,所以判断时要将临界阈值减 1
treeifyBin(tab, hash);
// 当新长度满足转换条件时,调用 treeifyBin 方法,将该链表转换为红黑树
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
序列化
Java 提供的RMI等框架的默认序列化方式为Java 序列化,1. 无法跨语言 2.易被攻击 (可以被反序列化程任何对象实例,从而执行任意类型的代码 创建循环对象链)3.序列化流太大 4.序列化性能太差
JDK 提供的输入输出流对象 ObjectInputStream和ObjectOutputStream,他们只能对实现了Serializable接口的类的对象进行反序列化和序列化。
serialVersionUID作用
反序列化过程中 验证 序列化对象是否加载了反序列化的类,相同类名不同版本号,反序列化无法获取对象。
实现序列化的时writeObject和readObject
具体实现序列化的时writeObject和readObject,通常为默认,但是对于数组,链表等数据结构,还要序列化所属元素,故需要自己实现Serialiazable接口并进行重写这两个方法。
TCP参数设置选项
/etc/sysctl.conf 配置
ulimit 最大连接数
net.ipv4.tcp_keepalive_time 检测客户端连接状态时间
net.ipv4.tcp_max_syn_backlog 当SYN等待队列溢出时,启动cookies来处理。
net.ipv4.ip_local_port_range 默认TCP端口为 32768-61000 可以扩大该范围
net.ipv4.tcp_max_tw_buckets 当一个连接关闭时,TCP会通过四次握手来完成一次关闭连接操作。在请求量比较大的情况下,消费端会有大量TIME_WAIT状态的连接。该参数可以限制TIME_WAIT状态连接数量(数量超过该值时,会被立刻清除并打印警告信息)
net.ipv4.tcp_tw_resuse 一个TIME_WAIT端口被重复复用的间隔时间(与四部握手关闭有关,防止前一个关闭端口的信息发送到新端口)
I/O复用
select
调用后开始阻塞,当异常或就绪时,函数返回。
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回写文件描述符 读文件描述符 异常事件文件描述符
poll
与select类似,无最大文件描述符数量限制。
select与poll共通
需要将大量文件描述符数组被复制到用户态和内核的地址空间之间,无论这些文件描述符是否就绪,他们的开销都会随着文件描述符数量增大而线性增大。并且顺序顺序轮询fd数组中的fd是否就绪,而且支持的fd数量不宜过大。
epoll
时间驱动的方式代替轮询扫描,epoll_ctl()注册文件描述符,放到内核时间表(红黑树),插入和删除性能较好。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
op 代表操作事件类型
fd 关联文件描述符
event 监听指定事件类型
应用进程调用epoll_wait()被文件描述符就绪事件给激活
int epoll_wait(int epfd, struct epoll_event events,int maxevents,int timeout)
阻塞直到文件描述符就绪时
NIO 用户层模型
Acceptor(事件接收器)
主要负责接受请求连接
Reactor(事件分离器)
接受请求后,建立连接注册到分离器中,依赖循环监听多路复用器Selector,一旦监听到事件,就会将事件dispatch到事件处理器;
基于线程模型的 Tomcat 参数调优
Tomcat 中,BIO、NIO 是基于主从 Reactor 线程模型实现的。
在 BIO 中,Tomcat 中的 Acceptor 只负责监听新的连接,一旦连接建立监听到 I/O 操作,将会交给 Worker 线程中,Worker 线程专门负责 I/O 读写操作。
在 NIO 中,Tomcat 新增了一个 Poller 线程池,Acceptor 监听到连接后,不是直接使用 Worker 中的线程处理请求,而是先将请求发送给了 Poller 缓冲队列。在 Poller 中,维护了一个 Selector 对象,当 Poller 从队列中取出连接后,注册到该 Selector 中;然后通过遍历 Selector,找出其中就绪的 I/O 操作,并使用 Worker 中的线程处理相应的请求。
你可以通过以下几个参数来设置 Acceptor 线程池和 Worker 线程池的配置项。
acceptorThreadCount:该参数代表 Acceptor 的线程数量,在请求客户端的数据量非常巨大的情况下,可以适当地调大该线程数量来提高处理请求连接的能力,默认值为 1。
maxThreads:专门处理I/O操作的Worker线程数量,默认200。
acceptCount:Tomcat 的 Acceptor 线程是负责从 accept 队列中取出该 connection,然后交给工作线程去执行相关操作,这里的 acceptCount 指的是 accept 队列的大小。
vmstat命令查看虚拟机线程运行情况(主要CS)
procs r:等待运行的进程数 b:处于非终端睡眠状态的进程数
memory swpd:虚拟内存使用情况 free:空闲的内存 buff:用来作为缓冲的内存数 cache:缓存大小
swap si:从磁盘交换到内存的交换页数量 so:从内存交换到磁盘的交换页数量
io bi:发送到块设备的快数 bo:从块设备接收到的块数
sysyemin in:每秒中断数 cs:每秒上下文切换次数
cpu us:用户cpu时间 sy:内核cpu时间 id:空闲时间 wa:等待I/O时间 st:运行虚拟机窃取的时间