synchronized是java中的一个关键字,也就是说是java语言内置的特性。那么为什么会出现Lock呢?
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁,这个是synchronized无法办到的。
总结一下,通过Lock提供了比synchronized更多的功能。但是要注意以下几点:
下面我们就来探讨一下java.util.concurrent.lock包中常用的类和接口。
首先要说明的就是Lock,通过查看Lock的源码可知,Lock是一个接口:
1 | public interface Lock { |
下面来逐个讲述Lock接口中每个方法的使用,lock(),tryLock(),tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。
在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?
首先lock()
方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被释放,以防死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
1 | Lock lock = ...; |
tryLock()
方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回,在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)
方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待时间内拿到了锁,则返回true。
所以,一般情况下通过tryLock来获取锁时是这样使用的:
1 | Lock lock = ...; |
lockInterruptibly()
方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
1 | public void method() throws InterruptedException { |
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的
。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
ReentrantLock,意思是“可重入锁”,关于可重入锁的概念在下一节讲述。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
基本语法上,ReentrantLock与synchronized很相似,它们都具备一样的线程重入特性,只是代码写法上有点区别而已。一个表现为API层面的互斥锁(Lock),一个表现为原生语法层面的互斥锁(synchronized)。
ReentrantLock相对synchronized而言还是增加了一些高级功能,主要有以下三项:
下面通过一些实例具体看一下如何使用ReentrantLock。
例子1,lock()的正确使用方法:
1 | public class Test { |
想一下这段代码的输出结果是什么?
1 | Thread-0得到了锁 |
也许有朋友会问,怎么会输出这个结果?第二个线程怎么会在第一个线程释放锁之前得到了锁?原因在于,insert方法中的lock变量是局部变量,每个线程执行该方法时都会保存一个副本,那么理所当然每个线程执行到lock.lock()处获取的是不同的锁,所以就不会发生冲突。
知道了原因改起来就比较容易了,只需将lock声明为类的属性即可。
1 | public class Test { |
这样就是正确使用Lock的方法。
例子2,tryLock()的使用方法:
1 | public class Test { |
输出结果:
1 | Thread-0得到了锁 |
例子3,lockInterruptibly()响应中断的使用方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 public class Test {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
Test test = new Test();
MyThread thread1 = new MyThread(test);
MyThread thread2 = new MyThread(test);
thread1.start();
thread2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}
public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
try {
System.out.println(thread.getName()+"得到了锁");
long startTime = System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
break;
//插入数据
}
}
finally {
System.out.println(Thread.currentThread().getName()+"执行finally");
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}
}
class MyThread extends Thread {
private Test test = null;
public MyThread(Test test) {
this.test = test;
}
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中断");
}
}
}
运行之后,发现thread2能够被正确中断。
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
1 | public interface ReadWriteLock { |
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。
不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
总结来说,Lock和synchronized有以下几点不同:
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
在前面介绍了Lock的基本使用,这一节来介绍一下与锁相关的几个概念。
如果锁具备可重入性,则称为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外要给synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。看下面这段代码就明白了:
1 | class MyClass { |
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法。假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成要给问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待,永远不会获取到的锁。
而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。
可中断锁:顾名思义,就是可以相应中断的锁。
在java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
看一下这两个类的源代码就清楚了:
在ReentrantLock中定义了2个静态内部类,一个是NotFairSync
,一个是FairSync
,分别用来实现非公平锁
和公平锁
。
我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:
1 | ReentrantLock lock = new ReentrantLock(true); |
如果参数为true表示为公平锁,为false则表示非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。
另外在ReentrantLock类中定义了很多方法,比如:
在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock,它实现的是ReadWriteLock接口。
读写锁将对一个资源(比如文件)的访问分成2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。上面的已经演示了读写锁的使用方法,在此不再赘述。
ReentrantLock是Lock的实现类,是一个互斥的同步器,在多线程高竞争条件下,ReentrantLock比synchronized有更加优异的性能表现。
ReentrantLock的优势体现在:
在使用ReentrantLock类的时候,一定要注意三点:
可能有人会认为这两个用法会比较冷门,但是在跨系统调用api的过程中,表的数据量比较大时,sql查询性能太差,会导致接口响应超时,就会对相应的业务产生非常大的影响。系统优化,大家千万不要以为只是后端的代码优化而已,sql的优化同样也是重点
。
可能有小伙伴会问,Covering Indexes到底是什么神器呢?它又是如何来提升性能的呢?接下来我会用最通俗易懂的语言来进行介绍,毕竟不是每个程序猿都要像DBA那样深刻理解数据库,知道如何用以及如何用好神器才是最关键的。
Covering Indexes就是一个索引覆盖所有要查询的字段(ps:这句话我挖个坑,文末我来解释)。
An index that contains all required information to resolve the query is known as a “Covering Index” – it completely covers the query.
Covering Index includes all the columns, the query refers to in the SELECT, JOIN, and WHERE clauses.
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
接下来我们通过一个非常简单的sql来进行分析:
1 | SELECT column1, column2 FROM tablename WHERE column3=xxx; |
你能想象将sql的执行时间从1.8秒,降到1.2秒,继续压榨到0.5,0.2…..,酣畅淋漓,怎一个爽字了得。就跟排兵布阵一样,打胜仗固然重要,但得想出成本最低效果最好的阵法,定会收获满满的成就感。
这条sql要如何来进行优化呢?第一反应可能就是说给“column3”加索引(普通索引或唯一索引)啊,没错,这样确实能在很大程度上提升这条sql的性能。
我们来分析下上面sql的执行计划:因为给“column3”建了索引,就会快速根据这个索引查询到符合条件的结果;然后再去这些符合条件的结果里查找所需的column1、column2字段;请注意,整个过程出现了两次查询,一次是查询索引,另一次查询结果的所需字段。
那能不能将上面说的执行计划再优化一下呢?大杀器Covering Indexes就是用来干这事的。给column3、column1、column2建个复合索引,如下:
1 | alter table table_name add index index_column3 (column3,column1,column2) ; |
这样就可以直接通过索引就能查询出符合条件的数据,而不必像上面那样先去查索引,然后再去查数据的两个过程。
光说不练那是假把式!小伙伴们可以用explain去试试上面的两种情况,如果执行复合索引后的情况,你会发现Extra里出现Using index。
刚开始我说挖了个坑,现在我把坑填上。既然神器Covering Indexes这么好用,以后select语句的我都不管三七二十一的都亮出神器。难不成你select *也要亮神器?一个表那么多字段,全建成索引?那索引文件会不堪重负的,这就会适得其反,带来一系列恶果的。索引文件过大会造成insert、update非常慢,你select倒是爽快了,不能不顾其他兄弟吧,不仗义的事咱不能干,切记!
如果看完这个分析还不过瘾,下面我给几篇扩展文章:
https://www.c-sharpcorner.com/UploadFile/b075e6/improving-sql-performance-using-covering-indexes/
https://www.red-gate.com/simple-talk/sql/learn-sql-server/using-covering-indexes-to-improve-query-performance/
https://stackoverflow.com/questions/609343/what-are-covering-indexes-and-covered-queries-in-sql-server
https://stackoverflow.com/questions/62137/what-is-a-covered-index
接下来给大家下另一个性能提升神器-STRAIGHT_JOIN,在数据量大的联表查询中灵活运用的话,能大大缩短查询时间。
首先来解释下STRAIGHT_JOIN到底是用做什么的:
STRAIGHT_JOIN is similar to JOIN, except that the left table is always read before the right table.
This can be used for those (few) cases for which the join optimizer puts the tables in the wrong order.
意思就是说STRAIGHT_JOIN功能同join类似,但能让左边的表来驱动右边的表,能改表优化器对于联表查询的执行顺序。
接下来我们举个例子进行大致的分析:
1 | select t1.* |
以上sql大数据量下执行需要30s,是不是很奇怪?明明Table1表的FilterID字段建了索引啊,Table1和Table2的CommonID也建了索引啊。通过explain来分析,你会发现执行计划中表的执行顺序是Table2->Table1。这个时候要略微介绍下驱动表的概念,mysql中指定了连接条件时,满足查询条件的记录行数少的表为驱动表;如未指定查询条件,则扫描行数少的为驱动表。mysql优化器就是这么粗暴以小表驱动大表的方式来决定执行顺序的。
但如下sql的执行时间都少于1s:
1 | select t1.* |
或
1 | select t1.* |
这个时候STRAIGHT_JOIN就派上用场,我们对sql进行改造如下:
1 | select t1.* |
用explain进行分析,发现执行顺序为Table1->Table2,这时就由Table1来作为驱动表了,Table1中相应的索引也就用上了,执行时间竟然低于1s了。
分析到这里,必须要重点说下:
扩展阅读:
https://stackoverflow.com/questions/512294/when-to-use-straight-join-with-mysql
https://stackoverflow.com/questions/5818837/why-does-straight-join-so-drastically-improve-this-query-and-what-does-it-mean
https://dev.mysql.com/doc/refman/8.0/en/join.html
来源:https://www.cnblogs.com/kingszelda/p/9034191.html
下面是线上机器的cpu使用率,可以看到从4月8日开始,随着时间cpu使用率在逐步增高,最终使用率达到100%导致线上服务不可用,后面重启了机器后恢复。
简单分析下可能出问题的地方,分为5个方向:
查看日志,没有发现集中的错误日志,初步排除代码逻辑处理错误。
首先联系了内部下游系统观察了他们的监控,发现一起正常。可以排除下游系统故障对我们的影响。
查看provider接口的调用量,对比7天没有突增,排除业务方调用量的问题。
查看tcp监控,TCP状态正常,可以排除是http请求第三方超时带来的问题。
查看机器监控,6台机器cpu都在上升,每个机器情况一样。排除机器故障问题。
即通过上述方法没有直接定位到问题。
重启了6台中问题比较严重的5台机器,先恢复业务。保留一台现场,用来分析问题。
查看当前的tomcat线程pid
查看该pid下线程对应的系统占用情况。top -Hp 384
top -Hp 进程pid
//该进程下的线程进行观察
使用命令top -p
发现pid 4430 4431 4432 4433 线程分别占用了约40%的cpu
将这几个pid转为16进制,分别为114e 114f 1150 1151
//转换成为 16 进制
printf “%x” your_pid
下载当前的java线程栈 sudo -u tomcat jstack -l 384>/1.txt
查询5中对应的线程情况,发现都是gc线程导致的
jstack [进程] | grep -A 10 [线程的16进制]
即: jstack 21125 | grep -A 10 52f1
-A 10表示查找到所在行的后10行。21233用计算器转换为16进制52f1,注意字母是小写。
sudo -u tomcat jmap -dump:live,format=b,file=/dump201612271310.dat 384
// 获取所有对象的dumpjmap -dump:format=b,file=/tmp/heap.hprof <PID>
// 获取存活对象的dump,实际效果是先执行一次FULL GCjmap -dump:live,format=b,file=/tmp/heap-live.hprof <PID>
heap dump会造成JVM比较长时间的停顿,必须摘流量执行
dump文件一定要zip后再传输,能节约大量传输时间
tar -zcf /tmp/heap.hprof.gz /tmp/heap.hprof
MAT下载地址:http://www.eclipse.org/mat/
我们代码中有一块是这样写的:
这是加解密的功能,每次运行加解密都会new一个BouncyCastleProvider对象,放倒Cipher.getInstance()方法中。
看下Cipher.getInstance()的实现,这是jdk的底层代码实现,追踪到JceSecurity类中
verifyingProviders每次put后都会remove,verificationResults只会put,不会remove。
看到verificationResults是一个static的map,即属于JceSecurity类的。
所以每次运行到加解密都会向这个map put一个对象,而这个map属于类的维度,所以不会被GC回收。这就导致了大量的new的对象不被回收。
将有问题的对象置为static,每个类持有一个,不会多次新建。
遇到线上问题不要慌,首先确认排查问题的思路:
一般的应用系统,读写比例在10:1左右,而且插入操作和一般的更新操作很少出现性能问题,遇到最多的,也是最容易出问题的,还是一些复杂的查询操作,所以查询语句的优化显然是重中之重。
在数据量和访问量不大的情况下,mysql访问是非常快的,是否加索引对访问影响不大。但是当数据量和访问量剧增的时候,就会发现mysql变慢,甚至down掉,这就必须要考虑优化sql了,给数据库建立正确合理的索引,是mysql优化的一个重要手段。
索引的目的在于提高查询效率,可以类比字典,如果要查“mysql”这个单词,我们肯定需要定位到m字母,然后从上往下找到y字母,再找到剩下的sql。如果没有索引,那么你可能需要把所有单词看一遍才能找到你想要的。除了词典,生活中随处可见索引的例子,如火车站的车次表、图书的目录等。它们的原理都是一样的,通过不断的缩小想要获得数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是我们总是通过同一种查找方式来锁定数据。
在创建索引时,需要考虑哪些列会用于 SQL 查询,然后为这些列创建一个或多个索引。事实上,索引也是一种表,保存着主键或索引字段,以及一个能将每个记录指向实际表的指针。数据库用户是看不到索引的,它们只是用来加速查询的。数据库搜索引擎使用索引来快速定位记录。
INSERT 与 UPDATE 语句在拥有索引的表中执行会花费更多的时间,而SELECT 语句却会执行得更快。这是因为,在进行插入或更新时,数据库也需要插入或更新索引值。
索引的类型:
温馨提示:根据《阿里巴巴Java开发手册》里的mysql规约,唯一索引建议命名为uk_字段名,普通索引名则为idx_字段名。(uk_即unique key; idx_即index的简称)。
应用于表创建完毕之后再添加。
1 | ALTER TABLE 表名 ADD 索引类型 (unique,primary key,fulltext,index)[索引名](字段名) |
1 | //普通索引 |
ALTER TABLE可用于创建普通索引、UNIQUE索引和PRIMARY KEY索引3种索引格式,table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。索引名index_name可选,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以同时创建多个索引。
CREATE INDEX可用于对表增加普通索引或UNIQUE索引,可用于建表时创建索引。
1 | CREATE INDEX index_name ON table_name(username(length)); |
如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定 length。
1 | //只能添加这两种索引; |
table_name、index_name和column_list具有与ALTER TABLE语句中相同的含义,索引名不可选。另外,不能用CREATE INDEX语句创建PRIMARY KEY索引。
删除索引可以使用ALTER TABLE或DROP INDEX语句来实现。DROP INDEX可以在ALTER TABLE内部作为一条语句处理,其格式如下:
1 | drop index index_name on table_name ; |
其中,在前面的两条语句中,都删除了table_name中的索引index_name。而在最后一条语句中,只在删除PRIMARY KEY索引中使用,因为一个表只可能有一个PRIMARY KEY索引,因此不需要指定索引名。如果没有创建PRIMARY KEY索引,但表具有一个或多个UNIQUE索引,则MySQL将删除第一个UNIQUE索引。
如果从表中删除某列,则索引会受影响。对于多列组合的索引,如果删除其中的某列,则该列也会从索引中删除。如果删除组成索引的所有列,则整个索引将被删除。
在这里要指出,组合索引和前缀索引是对建立索引技巧的一种称呼,并不是索引的类型。为了更好的表述清楚,建立一个demo表如下。
1 | create table USER_DEMO |
为了进一步榨取mysql的效率,就可以考虑建立组合索引,即将LOGIN_NAME,CITY,AGE建到一个索引里:
1 | ALTER TABLE USER_DEMO ADD INDEX name_city_age (LOGIN_NAME(16),CITY,AGE); |
建表时,LOGIN_NAME长度为100,这里用16,是因为一般情况下名字的长度不会超过16,这样会加快索引查询速度,还会减少索引文件的大小,提高INSERT,UPDATE的更新速度。
如果分别给LOGIN_NAME,CITY,AGE建立单列索引,让该表有3个单列索引,查询时和组合索引的效率是大不一样的,甚至远远低于我们的组合索引。虽然此时有三个索引,但mysql只能用到其中的那个它认为似乎是最有效率的单列索引,另外两个是用不到的,也就是说还是一个全表扫描的过程。
建立这样的组合索引,就相当于分别建立如下三种组合索引:
1 | LOGIN_NAME,CITY,AGE |
为什么没有CITY,AGE等这样的组合索引呢?这是因为mysql组合索引“最左前缀”的结果。简单的理解就是只从最左边的开始组合,并不是只要包含这三列的查询都会用到该组合索引。也就是说name_city_age(LOGIN_NAME(16),CITY,AGE)从左到右进行索引,如果没有左前索引,mysql不会执行索引查询。
如果索引列长度过长,这种列索引时将会产生很大的索引文件,不便于操作,可以使用前缀索引方式进行索引,前缀索引应该控制在一个合适的点,控制在0.31黄金值即可(大于这个值就可以创建)。
1 | SELECT COUNT(DISTINCT(LEFT(`title`,10)))/COUNT(*) FROM Arctic; -- 这个值大于0.31就可以创建前缀索引,Distinct去重复 |
EXPLAIN可以帮助开发人员分析SQL问题,explain显示了mysql如何使用索引来处理select语句以及连接表,可以帮助选择更好的索引和写出更优化的查询语句。
使用方法,在select语句前加上Explain就可以了:
简要说明下:
参数 | 解释 |
---|---|
id | 选择标识符。id越大优先级越高,就越先被执行 |
select_type | 查询的类型 |
table | 输出结果集的表 |
partitions | 匹配的分区 |
type | 表的连接类型 |
possible_keys | 查询时可能使用的索引 |
key | 查询时可能使用的索引 |
key_len | 索引字段的长度 |
ref | 列与索引的比较 |
rows | 大概估算的行数 |
filtered | 按表条件过滤的行百分比 |
Extra | 执行情况的描述和说明 |
以上字段中type
字段是非常重要的,表的连接类型如下:
值 | 含义 |
---|---|
all | 扫描全表数据 |
index | 遍历索引 |
range | 索引范围查找 |
index_subquery | 在子查询中使用ref |
unique_subquery | 在子查询中使用eq_ref |
ref_or_null | 对null进行索引的优化的ref |
fulltext | 使用全文索引 |
ref | 使用非唯一索引查找数据 |
eq_ref | 在join查询中使用主键或唯一索引关联 |
select_type
:主要用于区别普通查询, 联合查询, 子查询等复杂查询。
possible_keys
:指出MySQL能使用哪个索引在表中找到行,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。
key
:显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL。
查询中若使用了覆盖索引,则该索引仅出现在key列表中。
key_len
:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。
key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的。
ref
:表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值。
rows
:表示MySQL根据表统计信息及索引选用情况,估算
(注意是估算,不是实际扫描的行数)的找到所需的记录所需要读取的行数。
Extra
:包含不适合在其他列中显示但十分重要的额外信息
下面来简单的介绍Extra中的一些区别:
以上四点就能看出它们之前的区别,或许有部分人都存在疑惑 using index & using where 和using index condition那个比较好,从上面的的解释中就能看出是前者比较好,毕竟不需要回表查询数据,效率上应该比较快的。
在stackoverflow中找到一个非常经典的描述:
尽量避免这些不走索引的sql:
1 | SELECT name,phone FROM `user` WHERE `age`+10=30; -- 不会使用索引,因为所有索引列参与了计算 |
索引虽然好处很多,但过多的使用索引可能带来相反的问题,索引也是有缺点的:
索引只是提高效率的一个方式,如果mysql有大数据量的表,就要花时间研究建立最优的索引,或优化查询语句。
尽可能的使用主键查询,因为主键查询不会触发回表查询
,能节省一部分时间,变相提高了查询的性能。
索引不会包含有NULL的列
只要列中包含有NULL值,都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。
使用短索引
对串列进行索引,如果可以就应该指定一个前缀长度。例如,如果有一个char(255)的列,如果在前10个或20个字符内,多数值是唯一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。
索引列排序
mysql一张表查询只能用到一个索引。因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作,尽量不要包含多个列的排序,如果需要最好给这些列建复合索引。这一点是很多程序猿容易忽略的,如where子句的字段建了索引,排序的字段建了索引,但是分开建的,以为会走索引,其实这样的话排序的字段不会使用索引的,除非建复合索引,切记。
like语句操作
一般情况下不鼓励使用like操作,如果非使用不可,注意正确的使用方式。like ‘%aaa%’不会使用索引,而like ‘aaa%’可以使用索引。
不要在列上进行运算
不使用NOT IN 、<>、!=操作,但<,<=,=,>,>=,BETWEEN,IN是可以用到索引的。
索引要建立在经常进行select操作的字段上。
这是因为,如果这些列很少用到,那么有无索引并不能明显改变查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。
索引要建立在值比较唯一的字段上。
对于那些定义为text、image和bit数据类型的列不应该增加索引。因为这些列的数据量要么相当大,要么取值很少。
在where和join中出现的列需要建立索引。
where的查询条件里有不等号(where column != …),mysql将无法使用索引。
如果where字句的查询条件里使用了函数(如:where DAY(column)=…),mysql将无法使用索引。
在join操作中(需要从多个数据表提取数据时),mysql只有在主键和外键的数据类型相同时才能使用索引,否则即使建立了索引也不会使用。这一点很容易忽略,切记,切记,切记!
在进行联表查询时,建立关联的表的字段类型最好一样且长度一致,这样能更好的发挥索引的作用。
组合索引时切记此条约束:组合索引中有多个字段,其中一个字段是有范围判断,则需将此字段在最后面
。如
1 | ALTER TABLE USER_DEMO ADD INDEX name_age (NAME,AGE); |
因为age会有范围判断,则建组合索引时将AGE字段放在后面。
简单总结这一条规则就是范右无索
(范围查询右边无法使用索引)。
字符集字段比较,UTF8与UTF-BIN联合查询是不能走索引的。
如某张表的order_no字段类型为varchar(50),另一张表的order_no字段类型为varchar(50) COLLATE utf8_BIN。则此时联合查询时不能走索引的,切记。
即两张表的字段类型如下:
1 | `order_no` varchar(50) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT '订单号'; |
以下几种情况不适合
建索引:
更新非常频繁的字段不适合创建索引
给表创建主键,对于没有主键的表,在查询和索引定义上有一定的影响。
避免表字段为null,建议设置默认值(如int类型设置默认值为0),这样在索引查询上,效率会高很多。
关于order by的索引问题重点说下:
无条件查询如果只有order by create_time,即便create_time上有索引,也不会使用到。
因为优化器认为走二级索引再去回表成本比全表扫描排序更高,所以选择走全表扫描。
无条件查询但是order by create_time limit m,如果m值较小,是可以走索引的。
因为优化器认为根据索引有序性去回表查数据,然后得到m条数据,就可以终止循环,
那么成本比全表扫描小,则选择走二级索引。
即便没有二级索引,mysql针对order by limit也做了优化,采用堆排序。
order by排序分为file sort和index,index的效率更高。但以下情况不会使用index排序:
优化子查询
尽量使用join语句来替代子查询。因为子查询是嵌套查询,而嵌套查询会新建一张临时表,而临时表的创建和销毁会占用一定的系统资源,也花费一定的时间。但join语句并不会创建临时表,因此性能会高一点。
留意查询结果集
尽量使用小表驱动大表
的方式进行查询。也就是如果B表的数据小于A表的数据,那执行的顺序就是先查B表再查A表。
适当增加冗余字段
增加冗余字段可以减少大量的连表查询,因为多张表的连表查询性能很低,所以可以适当的增加冗余字段,以减少多张表的关联查询,这就是用空间换时间
的优化策略。
参考:
]]>new
或反射
或clone
或反序列化
的方法创建,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由java虚拟机通过垃圾回收机制完成的。GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,java会使用有向图的方法来管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题。java使用有向图的方式进行内存管理,示例如下:
在有向图中,我们叫做obj1是可达的,obj2就是不可达的,显然不可达的可以被清理。
释放对象的根本原则就是对象不会再被使用。在java语言中,判断一个内存空间是否符合垃圾收集标准有两个:
接下来介绍内存泄漏与内存溢出:
内存泄漏(memory leak)
:是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄漏。内存溢出(out of memory)
:指程序运行过程中无法申请到足够的内存而导致的一种错误。即程序在申请内存时,没有足够的内存空间供其使用。内存泄漏是内存溢出的一种诱因,不是唯一因素,那么,java内存泄漏根本原因是什么?长生命周期的对象持有短生命周期对象的引用,就很可能发生内存泄漏
。尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄漏的发生场景。
memory leak会最终导致out of memory,内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
在java中出现内存泄漏的场景,主要有如下几个大类:
如HashMap、Vector等的使用最容易出险内存泄漏,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
1 | Static Vector v = new Vector(10); |
在这个例子中,循环申请Object对象,并将所申请的对象放入一个Vector中,如果仅仅释放引用本身(o=null),那么Vector仍然引用该对象,所以这个对象对GC来说是不可回收。因此,如果对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为null。
1 | Set<Person> set = new HashSet<Person>(); |
就Set而言,remove()方法是通过equals()方法来删除匹配的元素的,如果一个对象确实提供了正确的equals()方法,但是切忌不要在修改这个对象后使用remove(),这可能会发生内存泄漏。
比如数据库连接(dataSource.getConnection())、网络连接(socket)和io连接,以及使用其它框架的时候,除非显示的调用了其close()方法(或类似方法)将其连接关闭,否则是不会自动被GC回收的。其实原因就是长生命周期对象持有短生命周期对象的引用。
对于Resultset和Statement对象可以不进行显示回收,但Connection一定要显示回收,因为Connection在任何时候都无法自动回收,而Connection一旦回收,Resultset和Statement对象就会立即为null。
但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
内部类的引用是比较容易遗忘的一种,而且一旦没释放,可能导致一系列的后继承类对象没有释放。此外还要小心外部模块不经意的引用。
不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被JVM正常回收,导致内存泄漏。
单例模式很多时候我们可以把它的生命周期与整个程序的生命周期看做差不多的,所以是一个长生命周期的对象。如果这个对象持有其他对象的引用,也很容易发生内存泄漏。
说明:此篇文章的内容绝大部分来源于《极客时间》专栏。
以下讨论是基于InnoDB引擎。
至于分析性能差别的时候,可以记住以下几个原则:
对于count(主键id)
来说,InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。
对于count(1)
来说,InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。
单看这两个用法的差别的话,你就能对比出来,count(1)执行的要比count(主键id)快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。
对于count(字段)
来说:
但是count(*)
是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是null,按行累加。
看到这里,你一定会说,优化器就不能自己判断一下吗,主键id肯定非空啊,为什么不能按照count(*)来处理,多么简单的优化啊。
当然,mysql专门针对这个语句进行优化,也不是不可以。但是这种需要专门优化的情况太多了,而且mysql已经优化过count(*)了,直接使用这种用法就可以了。
所以结论是:按照效率排序的话,count(字段) < count(主键id) < count(1) ≈ count(*)
。所以建议尽量使用count(*)
。
用法:left(str, length),即:left(被截取字符串, 截取长度)
1 | SELECT LEFT('www.yuanrengu.com',8); |
结果为:www.yuan
用法:right(str, length),即:right(被截取字符串, 截取长度)
1 | SELECT RIGHT('www.yuanrengu.com',6); |
结果为:gu.com
用法:
1 | SELECT SUBSTRING('www.yuanrengu.com', 9); |
结果为:rengu.com
1 | SELECT SUBSTRING('www.yuanrengu.com', 9, 3); |
结果为:ren
1 | SELECT SUBSTRING('www.yuanrengu.com', -6); |
结果为:gu.com
1 | SELECT SUBSTRING('www.yuanrengu.com', -6, 2); |
结果为:gu
用法:substring_index(str, delim, count),即:substring_index(被截取字符串,关键字,关键字出现的次数)
1 | SELECT SUBSTRING_INDEX('www.yuanrengu.com', '.', 2); |
结果为:www.yuanrengu
1 | SELECT SUBSTRING_INDEX('www.yuanrengu.com', '.', -2); |
结果为:yuanrengu.com
1 | SELECT SUBSTRING_INDEX('www.yuanrengu.com', 'sprite', 1); |
举例如下:
1 | 表A记录如下: |
sql语句如下:
1 | select * from A |
结果如下:
1 | aID aNum bID bName |
left join是以A表的记录为基础的,A可以看成左表,B可以看成右表,left join是以左表为准的。
换句话说,左表(A)的记录将会全部表示出来,而右表(B)只会显示符合搜索条件的记录(例子中为: A.aID = B.bID),B表记录不足的地方均为NULL。
sql语句如下:
1 | select * from A |
结果如下:
1 | aID aNum bID bName |
仔细观察一下,就会发现,和left join的结果刚好相反,这次是以右表(B)为基础的,A表不足的地方用NULL填充。
sql语句如下:
1 | select * from A |
结果如下:
1 | aID aNum bID bName |
很明显,这里只显示出了 A.aID = B.bID的记录.这说明inner join并不以谁为基础,它只显示符合条件的记录.
LEFT JOIN操作用于在任何的 FROM 子句中,组合来源表的记录。使用 LEFT JOIN 运算来创建一个左边外部联接。左边外部联接将包含了从第一个(左边)开始的两个表中的全部记录,即使在第二个(右边)表中并没有相符值的记录。
语法:FROM table1 LEFT JOIN table2 ON table1.field1 compopr table2.field2
说明:table1, table2参数用于指定要将记录组合的表的名称。
field1, field2参数指定被联接的字段的名称。且这些字段必须有相同的数据类型及包含相同类型的数据,但它们不需要有相同的名称。
compopr参数指定关系比较运算符:”=”, “<”, “>”, “<=”, “>=” 或 “<>”。
运行状态
中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制
。反射是java中一种强大的工具,能够使我们很方便的创建灵活的代码,这些代码可以在运行时装配。在实际的业务中,可能会动态根据属性去获取值。
工具类如下:
1 | package com.yaoguang.common.utils.field; |
测试用例如下:
1 | /** |
还有一种将字符串转换成java代码并执行的方法:Java Expression Language (JEXL)
是一个表达式语言引擎,可以用来在应用或者框架中使用。
JEXL受Velocity和JSP 标签库 1.1 (JSTL) 的影响而产生的,需要注意的是,JEXL 并不是 JSTL 中的表达式语言的实现。
需要先添加jar包,maven配置如下:
1 | <dependency> |
1 | public class DyMethodUtil { |
测试示例如下:
1 | /** |
转自:https://blog.csdn.net/jason0539/article/details/23297037/
概念:单例模式指的是,保证一个类只有一个实例,并且提供一个可以全局访问的入口
。java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例
、饿汉式单例
、登记式单例
。
单例模式有以下特点:
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
1 | //懒汉式单例类.在第一次调用的时候实例化自己 |
Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。
(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)
但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全,如果你第一次接触单例模式,对线程安全不是很了解,可以先跳过下面这三小条,去看饿汉式单例,等看完后面再回头考虑线程安全的问题.
1 | public static synchronized Singleton getInstance() { |
1 | public class Singleton { |
两次if(singleton==null)检查,这就是双重检查锁
这个名字的由来。这种写法是可以保证线程安全的,假设有两个线程同时到达synchronized语句块,那么实例化代码只会由其中先抢到锁的线程执行一次,而后抢到锁的线程会在第二个if判断中发现singleton不为null,所以跳过创建实例的语句。再后面的其他线程再来调用getInstance方法时,只需判断第一次的if(singleton==null),然后会跳过整个if块,直接return实例化后的对象。
在双重检查锁模式中,给singleton这个对象加了volatile
关键字,那为什么要用volatile呢?主要就在于singleton=newSingleton(),它并非是一个原子操作。
这种写法的优点是不仅线程安全
,而且延迟加载、效率也更高。
1 | public class Singleton { |
这种比上面两种都好一些,既实现了线程安全,又避免了同步带来的性能影响
。
1 | //饿汉式单例类。在类初始化时,已经自行实例化 |
饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全
的。
1 | //类似Spring里面的方法,将类名注册,下次从里面直接获取。 |
登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。
这里我对登记式单例标记了可忽略,我的理解来说,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。
饿汉
就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了;懒汉
比较懒,只有当调用getInstance的时候,才会去初始化这个单例。饿汉式天生就是线程安全的
,可以直接用于多线程而不会出现问题;懒汉式本身是非线程安全的
,为了实现线程安全有几种写法,分别是上面的1.1,1.2,1.3,这三种实现在资源加载和性能方面有些区别。延迟加载
,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。classloader的机制
来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全
的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。
以下是一个单例类使用的例子,以懒汉式为例,这里为了保证线程安全,使用了双重检查锁定的方式:
1 | public class TestSingleton { |
可以看到里面加了volatile
关键字来声明单例对象,既然synchronized已经起到了多线程下原子性、有序性、可见性的作用,为什么还要加volatile呢,原因已经在下面评论中提到:http://www.iteye.com/topic/652440和http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
1 | public class TMain { |
运行结果:
1 | the name is 0539 |
结论
:由结果可以得知单例模式为一个面向对象的应用程序提供了对象唯一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象。
对于单例模式的几种实现方式,知道饿汉式和懒汉式的区别,线程安全,资源加载的时机,还有懒汉式为了实现线程安全的3种方式的细微差别。
]]>该篇文章写于2016年10月21日
一直在使用Mybatis这个ORM框架,都是使用mybatis里的一些常用功能。今天在项目开发中有个业务是需要限制各个用户对某些表里的字段查询以及某些字段是否显示,如某张表的某些字段不让用户查询到。这种情况下,就需要构建sql来动态传入表名、字段名了。现在对解决方法进行下总结,希望对遇到同样问题的伙伴有些帮助。
动态SQL是mybatis的强大特性之一,mybatis在对sql语句进行预编译之前,会对sql进行动态解析,解析为一个BoundSql对象,也是在此处对动态sql进行处理。下面让我们先来熟悉下mybatis里#{}
与${}
的用法。
在动态sql解析过程,#{}
与${}
效果是不一样的:
#{}
解析为一个 JDBC 预编译语句(prepared statement)的参数标记符。
如以下sql语句:
1 | select * from user where name = #{name}; |
会被解析为:
1 | select * from user where name = ?; |
可以看到#{}被解析为一个参数占位符?
。
${ }
仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换
如以下sql语句:
1 | select * from user where name = ${name}; |
当我们传递参数“yuanrengu”时,sql会解析为:
1 | select * from user where name = "yuanrengu"; |
可以看到预编译之前的sql语句已经不包含变量name了。
综上所得, ${ }
的变量的替换阶段是在动态 SQL 解析阶段
,而 #{ }
的变量的替换是在 DBMS 中。
#{}
与${}
的区别可以简单总结如下:
${}
将传入的参数直接显示生成在sql中,不会添加引号${}
无法防止sql注入${}
在预编译之前已经被变量替换了,这会存在sql注入的风险。如下sql
1 | select * from ${tableName} where name = ${name} |
如果传入的参数tableName为user; delete user; —
,那么sql动态解析之后,预编译之前的sql将变为:
1 | select * from user; delete user; -- where name = ?; |
—
之后的语句将作为注释不起作用,顿时我和我的小伙伴惊呆了!!!看到没,本来的查询语句,竟然偷偷的包含了一个删除表数据的sql,是删除,删除,删除!!!重要的事情说三遍,可想而知,这个风险是有多大。
${}
一般用于传输数据库的表名、字段名等${}
通过上面的分析,相信大家可能已经对如何动态调用表名和字段名有些思路了。示例如下:
1 | <select id="getUser" resultType="java.util.Map" parameterType="java.lang.String" statementType="STATEMENT"> |
要实现动态调用表名和字段名,就不能使用预编译了,需添加statementType=”STATEMENT”。
1 | statementType:STATEMENT(非预编译),PREPARED(预编译)或CALLABLE中的任意一个,这就告诉 MyBatis 分别使用Statement,PreparedStatement或者CallableStatement。默认:PREPARED。这里显然不能使用预编译,要改成非预编译。 |
其次,sql里的变量取值是${xxx}
,不是#{xxx}。
因为${}
是将传入的参数直接显示生成sql,如${xxx}传入的参数为字符串数据,需在参数传入前加上引号,如:
1 | String name = "sprite"; |
mybatis动态调用表名和字段名,还可以应用于日志的收集上,如数据库的日志表,每隔一个月动态建一个日志表,表名前缀相同(如log_201610,log_201611等),这样实现日志的分月分表存储,方便日志的分析。
使用PrepareStatement执行SQL的好处主要有两个:
预处理
,提前生成执行计划,当给定占位符参数,真正执行SQL的时候,执行引擎可以直接执行,效率更好一点。防止SQL注入攻击
。因为一开始构造PrepareStatement的时候就已经提交了SQL,并被数据库预先生成了执行计划
,后面不管提交什么样的字符串,都只能交给这个执行计划去执行,不可能再生成一个新的SQL了,也就不会被攻击了。本篇文章写于2017年3月14日。虽然写的比较早了,但很多东西依然适用于如今的职场。
近一年技术团队在不断扩充成员,一直忙于高级java工程师、Android工程师、iOS工程师的面试,很想写一篇和招聘程序猿相关的文章,特别是看到“酷壳”里皓哥写的一篇《我是怎么招聘程序员的》文章后,产生很多共鸣。
虽然工作年限还不够长,但也经历过很多大大小小的面试,即被面试过,也面试过很多人。经历过很多很专业的面试,也经历过一些非常BT和令人不怎么舒服的面试。一个好的面试体验,公司的考核流程和面试官就显得非常重要了,如果考核流程非常繁琐,会让面试者内心没有任何好感,如面试时在前台莫名其妙的被晾置一两个小时,笔试初试复试得跑三趟公司。面试官就更重要了,不要刚开始面试,就弄的像别人欠你钱或别人在挑战的技术能力似的,或者尽问些非常冷门而且工作中完全用不到的技术来彰显自己的博学。
公司招聘不要弄成一种买卖关系,应该是寻找一些志同道合和跟公司“气场”匹配的伙伴,这里的气场是指跟公司所需要的技能有一定的匹配度、跟公司的理念有一定程度的吻合。公司行政经常给我说:“技术人员是不是很矫情啊?说好来面试的,最后都不来,说好要入职的,入职前一天又说有公司给更高的待遇。”我只能微微一笑的给她说:“你见,或者不见我,我就在那里,不悲不喜。你来入职,或者不来入职,offer反正在你手里,不增只减”。因为我也曾经矫情过,研究生快毕业时,做了一堆的算法和数据结构方面的题,准备了一堆面试常问的题和一些面试技巧,就到处投简历面试(其实当时手里已经有几个不错的offer了),不为找工作,只为刷存在感,回想这年少无知的举动,倍感惭愧。技术圈说小不小,说大其实也不大,毕竟江湖不远,有缘再见,程序猿平时还是要多给自己攒攒口碑的。
如何去考核一个程序猿是否可以给offer?我在面试应聘者的时候,最主要是要弄清如下几件事:
我相信绝大部分的公司在考核应聘者时都会围绕上述四个问题来进行,可能有人会对第一个问题产生疑问,难道还有不能正常对话的人?还真别说,我就遇到几个性格比较“鲜明”的应聘者,面试时头抬的高高的,眼瞅着天花板,一副老子天下第一的表情,技术人员有傲骨可以理解,可是问几个技术问题,却一问三不知,问他之前团队如何协作的,只回答说公司安排的任务不喜欢就离职了,让人真的很难正常对话。对于第二个问题就更重要了,公司当然希望应聘者的技术越牛越好,梦想是美好的,能招到大神这肯定是公司之幸,现实终究还是很骨感,只希望能找到跟公司所需的技能契合度尽可能高的伙伴。
技术圈比较流行一句话:面试造火箭,工作拧螺丝。这种情况确实比较常见,从某个层面来看是大部分公司程序猿能力的美好愿景,也催促自己去不断完善自己的知识体系。对于第三个问题,程序猿最基本的技能就是要通过网络解决工作中的一些难题,多问度娘,多问谷歌,程序猿比较忌讳的一点就是“拿来主义”,遇到问题不动脑思考张口就问别人。第四个问题,基本就是考查人的社交能力和情商了,个人人为,团队氛围的整体和谐是做好所有项目的前提。毕竟如今的项目都是靠团战,一个人即使能力再强,也很难独立完成整个项目。
之前我经历过的一些比较传统的面试流程,基本是下面这样的:
个人觉得这种面试形式不是太合适,可能会错过很多适合公司的程序猿。其实我个人不论是面试别人还是被面试时,都非常讨厌第一个问题,拿着别人简历难道不知道别人叫什么名字?技术面试,这种形式上的东西能少就尽量少即可。但应聘者一进来,总得有个关于介绍的开场。我面试应聘者时,别人一进来时我会先问好,给个微笑,让应聘者不要太紧张。让别人做下技术方面的简单介绍,如工作中主要处理哪里方面的业务(电商、金融等等)啊?主要用哪些编程语言?主要用哪些开发架构(dubbo、SOA等)?主要用哪些框架(Spring、mybatis等)?这样也方便对这个人有比较全面的了解,交流时也好针对性的问些问题,做偏技术方面的介绍也好了解这个人的沟通交流能力。毕竟很难从一个人的简历或自我阐述上来考核这个人是否合适。
我绝不会在面试应聘者时问一些非常细节的问题,我曾经就经历一个非常BT的面试,面试官据称刚从华为出来,一上来就问我是否用过mybatis,我说用过。接下来这哥们问,mybatis是哪一年被开源的?接手的是哪个开发团队?mybatis的升级历史?当然我觉得我可能听错了,我说记不清楚哪一年被开源的,是apache开源的项目,讲解了下mybatis对比hibernate的优势和缺点。这哥们说是都不知道哪一年开源的,也不知道是apache的哪个团队接手,真的用过mybatis?说是我肯定没用过mybatis。当时内心就千万只骏马奔腾而过啊,我真的是在应聘程序猿吗?敢情我用了几年假的mybatis?
还有一种面试流程是很多朋友都给我说过非常不喜欢的,就是跟hr的面聊,hr的小姐姐们一般都是聊家庭啊,聊什么让应聘者说自己目前做过最骄傲的事情是什么?最失败的事情是什么?还有问应聘者如何处理跟女票的矛盾,陪女票逛街时接到要加班的电话怎么办?还有非常多很奇怪的问题,其实程序猿虽然得到的评价是木讷,但都是很聪明的,这类问题都可以用些标准答案来应付,但不是内心真实的答案啊。例如陪女票逛街的问题,程序猿大都是很有责任心的,如果是项目非常忙,都会主动加班,如果项目没那么忙,好容易陪女票逛街要加啥班啊,毕竟程序猿有个女票还是非常不容易的,人艰不拆啊。
我在面试时一般会根据应聘者自己的项目描述来提问,考核下他自己说的技能的熟练程度。也遇到一些技术确实够菜,简历写的无比高大上,问他自己说的问题都回答不上来,你问东他答西,完全不在同一频道对话。碰到这种情况,我就会问笔试题里的SpringMVC工作原理(笔试只是公司要求的形式,我一般不会太看重笔试的成绩),这个问题非常简单,只要用过这个框架的人都能说出个一二三来。这道题也是所有人都答的非常好的,因为网上一搜,答案一大推,问这个问题也是让应聘者放松些不要太紧张,毕竟自己刚写过。但有些人笔试题上答案写了好大一堆,但口述却一点都说不出来,知道什么问题了吧?我真的不介意你笔试时抄网上的东西,只要你能复述出来讲清楚我都算你掌握了这个问题,但假如是抄的东西连复述都说不出来,那面试还有什么可问的?碰到这种情况,我也不能直接打发别人,还得照顾应聘者的自尊心啊,我会跟应聘者聊聊人生聊聊理想,然后面试就愉快的结束了。
对于这里还得说一点,公司hr小姐姐要求即使面试遇到不合适的应聘者,说话也要照顾对方的面子,因为网络暴力真的很害人,但凡看一些招聘网站的面试分享,说公司好话的基本都是拿到offer了的,说不好话都是没拿到offer的,如果面试体验不好,就尽发些诋毁公司的言论,对后面要来公司应聘的伙伴来说起到非常不好的影响。网络暴力,确实。。。。。。
如果没有一起工作过,没有一些实际的项目做背景,单靠半个小时或一个多小时的面试,是比较难全面的了解一个人的。个人觉得在应聘程序猿职务时需要做好如下几个方面:
所有的面试技巧都敌不过自己知识体系的深度、广度!不断提升自己,基础扎实,对某一个或几个业务有比较深入的熟悉,这样的小伙伴无论在哪家公司都是非常受欢迎的。
]]>官方介绍如下:
Project Lombok makes java a spicier language by adding ‘handlers’ that know how to build and compile simple, boilerplate-free, not-quite-java code.
大致意思是Lombok通过增加一些“处理程序”,可以让java变得简洁、快速。
Lombok能以简单的注解形式来简化java代码,提高开发人员的开发效率。例如开发中经常需要写的javabean,都需要花时间去添加相应的getter/setter,也许还要去写构造器、equals等方法,而且需要维护,当属性多时会出现大量的getter/setter方法,这些显得很冗长也没有太多技术含量,一旦修改属性,就容易出现忘记修改对应方法的失误。
Lombok能通过注解的方式,在编译时自动为属性生成构造器、getter/setter、equals、hashcode、toString方法。出现的神奇就是在源码中没有getter和setter方法,但是在编译生成的字节码文件中有getter和setter方法。这样就省去了手动重建这些代码的麻烦,使代码看起来更简洁些。
Lombok的使用跟引用jar包一样,可以在官网下载jar包,也可以使用maven添加依赖:
1 | <dependency> |
接下来我们来分析Lombok中注解的具体用法。
@Data注解在类上,会为类的所有属性自动生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。
官方实例如下:
1 | import lombok.AccessLevel; |
如不使用Lombok,则实现如下:
1 | import java.util.Arrays; |
如果觉得@Data太过残暴(因为@Data集合了@ToString、@EqualsAndHashCode、@Getter/@Setter、@RequiredArgsConstructor的所有特性)不够精细,可以使用@Getter/@Setter注解,此注解在属性上,可以为相应的属性自动生成Getter/Setter方法,示例如下:
1 | import lombok.AccessLevel; |
如果不使用Lombok:
1 | public class GetterSetterExample { |
该注解用在属性或构造器上,Lombok会生成一个非空的声明,可用于校验参数,能帮助避免空指针。
示例如下:
1 | import lombok.NonNull; |
不使用Lombok:
1 | import lombok.NonNull; |
该注解能帮助我们自动调用close()方法,很大的简化了代码。
示例如下:
1 | import lombok.Cleanup; |
如不使用Lombok,则需如下:
1 | import java.io.*; |
默认情况下,会使用所有非静态(non-static)和非瞬态(non-transient)属性来生成equals和hasCode,也能通过exclude注解来排除一些属性。
示例如下:
1 | import lombok.EqualsAndHashCode; |
类使用@ToString注解,Lombok会生成一个toString()方法,默认情况下,会输出类名、所有属性(会按照属性定义顺序),用逗号来分割。
通过将includeFieldNames参数设为true,就能明确的输出toString()属性。这一点是不是有点绕口,通过代码来看会更清晰些。
使用Lombok的示例:
1 | import lombok.ToString; |
不使用Lombok的示例如下:
1 | import java.util.Arrays; |
无参构造器、部分参数构造器、全参构造器。Lombok没法实现多种参数构造器的重载。
Lombok示例代码如下:
1 | import lombok.AccessLevel; |
不使用Lombok的示例如下:
1 | public class ConstructorExample<T> { |
会发现在Lombok使用的过程中,只需要添加相应的注解,无需再为此写任何代码。自动生成的代码到底是如何产生的呢?
核心之处就是对于注解的解析上。JDK5引入了注解的同时,也提供了两种解析方式。
运行时能够解析的注解,必须将@Retention设置为RUNTIME,这样就可以通过反射拿到该注解。java.lang,reflect反射包中提供了一个接口AnnotatedElement,该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,对反射熟悉的朋友应该都会很熟悉这种解析方式。
编译时解析有两种机制,分别简单描述下:
1)Annotation Processing Tool
apt自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,apt被替换主要有2点原因:
JSR 269自JDK6加入,作为apt的替代方案,它解决了apt的两个问题,javac在执行的时候会调用实现了该API的程序,这样我们就可以对编译器做一些增强,这时javac执行的过程如下:
Lombok本质上就是一个实现了“JSR 269 API”的程序。在使用javac的过程中,它产生作用的具体流程如下:
拜读了Lombok源码,对应注解的实现都在HandleXXX中,比如@Getter注解的实现时HandleGetter.handle()。还有一些其它类库使用这种方式实现,比如Google Auto、Dagger等等。
Lombok虽然有很多优点,但Lombok更类似于一种IDE插件,项目也需要依赖相应的jar包。Lombok依赖jar包是因为编译时要用它的注解,为什么说它又类似插件?因为在使用时,eclipse或IntelliJ IDEA都需要安装相应的插件,在编译器编译时通过操作AST(抽象语法树)改变字节码生成,变向的就是说它在改变java语法。它不像spring的依赖注入或者mybatis的ORM一样是运行时的特性,而是编译时的特性。这里我个人最感觉不爽的地方就是对插件的依赖!因为Lombok只是省去了一些人工生成代码的麻烦,但IDE都有快捷键来协助生成getter/setter等方法,也非常方便。
知乎上有位大神发表过对Lombok的一些看法:
这是一种低级趣味的插件,不建议使用。JAVA发展到今天,各种插件层出不穷,如何甄别各种插件的优劣?
能从架构上优化你的设计的,能提高应用程序性能的 ,实现高度封装可扩展的…,
像lombok这种,像这种插件,已经不仅仅是插件了,改变了你如何编写源码,
事实上,少去了代码你写上去又如何?
如果JAVA家族到处充斥这样的东西,那只不过是一坨披着金属颜色的屎,迟早会被其它的语言取代。
虽然话糙但理确实不糙,试想一个项目有非常多类似Lombok这样的插件,个人觉得真的会极大的降低阅读源代码的舒适度。
虽然非常不建议在属性的getter/setter写一些业务代码,但在多年项目的实战中,有时通过给getter/setter加一点点业务代码,能极大的简化某些业务场景的代码。所谓取舍,也许就是这时的舍弃一定的规范,取得极大的方便。
我现在非常坚信一条理念,任何编程语言或插件,都仅仅只是工具而已,即使工具再强大也在于用的人,就如同小米加步枪照样能赢飞机大炮的道理一样。结合具体业务场景和项目实际情况,无需一味追求高大上的技术,适合的才是王道。
Lombok有它的得天独厚的优点,也有它避之不及的缺点,熟知其优缺点,在实战中灵活运用才是王道。
参考:
https://projectlombok.org/features/
https://github.com/rzwitserloot/lombok?spm=a2c4e.11153940.blogcont59972.5.2aeb6d32hayLHv
https://www.zhihu.com/question/42348457
https://blog.csdn.net/ghsau/article/details/52334762
java虽然宣称一切都是对象,但原始数据类型是例外。int是整形数字,是java的9个原始数据类型(Primitive Types)(boolean、byte、short、char、int、float、double、long、void)之一。Integer是int对应的包装类,它有一个int类型的字段存储数据,并且提供了基本操作,比如数学运算、int和字符串之间转换等。在java 5中引入了自动装箱和自动拆箱功能(boxing/unboxing),java可以根据上下文,自动进行转换,极大地简化了相关编程。javac自动把装箱转换为Integer.valueOf(),把拆箱替换为Integer.intValue()。
自动装箱实际上算是一种语法糖。什么是语法糖?可以简单理解为java平台为我们自动进行了一些转换,保证不同的写法在运行时等价,他们发生在编译阶段
,也就是生产的字节码是一致的。(此句摘自极客时间专栏)
原始数据类型的变量,需要使用并发相关手段才能保证线程安全。如果有线程安全的计算需要,建议考虑使用类似AtomicInteger、AtomicLong这样的线程安全类。
原始数据类型和java泛型并不能配合使用
。因为java的泛型某种程度上可以算作伪泛型,它完全是一种编译期
的技巧,java编译期会自动将类型转换为对应的特定类型。这就决定了使用泛型,必须保证相应类型可以转换为Object。
废话不多说,直接来demo,这样效果更直接。
1 | public class Test { |
需要明确的一点是,包装型(Integer)和基本型(int)比较会自动拆箱(jdk1.5以上)。
在这里很多人比较容易迷惑的是如下情况:
1 | Integer a1 = 6; |
如果研究过jdk源码,你就会发现Integer a3 = 128;在java编译时会被翻译成 Integer a3 = Integer.valueOf(128); 我们再来看看valueOf()的源码就更清晰了。
1 | public static Integer valueOf(int i) { |
由以上源码就会发现,对于-128到127之间的数,会进行缓存
,Integer a1 = 6时,会将6进行缓存,下次再写Integer a2 = 6;时,就会直接从缓存中取,也就不用new一个对象了,所以a1和a2比较时就为true。但a3和a4是超过范围,会new一个对象,==是进行地址和值比较,是比较两个对象在JVM中的地址
,这时a3和a4虽然值相同但地址是不一样的,所以比较就为false了。
通过上面的分析可知:
所有包装类对象之间值的比较,建议使用equals方法比较
。
==
判断对象是否同一个。
Integer var = ?在缓存区间
的赋值,会复用已有对象,因此这个区间内的Integer使用==进行判断可通过,但是区间之外的所有数据,则会在堆
上新产生,不会通过。
因此如果用== 来比较数值,很可能在小的测试数据中通过,而到了生产环境才出问题。
为了节省内容,对与下列包装对象的两个实例,当他们的基本值相同时,用==判断会为true:
1 | Boolean |
我们也可以看看其它包装型的缓存情况:
1 | Boolean:(全部缓存) |
如果要比较两个Integer对象的值(均为new的对象),可以通过.intValue()
进行转换后来比较,如下:
1 | Integer a3 = 128; |
也可以使用equal()
来进行比较,如下:
1 | Integer a3 = 128; |
Apollo官方文档的介绍其实已经很详细,出一个Apollo系列主要是对自己学习的一个归纳、源码解读以及踩坑的总结。大家还是以阅读官方文档为主!
Apollo系列会分篇介绍环境的搭建、常用场景的配置分析、源码解读等。系列文章均以官方文档为主!!!希望大家能积极讨论,有问题可随时留言,一起学习!
在实际的项目开发中经常会遇到配置信息的场景,常见的有两种配置形式:1.基于本地配置形式(通常有两种做法:将配置信息耦合在业务代码中;将配置信息配置在配置文件);2.适用于大规模分布式场景的集中式配置形式。
本地配置会有非常多的痛点,如修改代码带来的麻烦、修改配置后获取新配置不实时等。集中式配置好处非常多(虽然也会带来麻烦,但相比于好处,这些麻烦还是可以接受的),结合猿人谷多年的实战,在下面几点非常有效果:
Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。
服务端基于Spring Boot和Spring Cloud开发,打包后可以直接运行,不需要额外安装Tomcat等应用容器。
Java客户端不依赖任何框架,能够运行于所有Java运行时环境,同时对Spring/Spring Boot环境也有较好的支持。
本篇介绍如何在本地使用IDE编译、运行Apollo。Talk is cheap,Show me the code. 学习开源最有效的方式就是将项目实实在在的跑起来,边踩坑边学习边总结,这样才能收获更多。
本地开发需要如下组件:
踩坑
:IDEA在2018.1.8之前的版本debug时报程序包com.netflix.servo.util不存在,其实这个包是有的。当时百思不得其解,检查之后再检查,也没发现啥问题,升级IDEA版本后,世界安静了,你说意外不意外!
Apollo配置信息的存储是适用的Mysql
,需要一些初始化的数据库信息。市面上还有一些配置中心的相关开源项目,如百度开源的DisConf(也是适用Mysql存储配置信息)、360开源的QConf(使用的是ZooKeeper)、Spring Cloud的组件Spring Cloud Config等。
apolloportaldb.sql的信息如下:
1 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; |
apolloconfigdb.sql如下:
1 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; |
在本地开发时,一般会同时启动apollo-configservice
和apollo-adminservice
,是基于appollo-assembly来启动的,后面会进行详细介绍。
如下图所示,点击Edit Configurations…
创建Application:
如上图所示:
Name:ConfigAdminApplication
Main class配置:com.ctrip.framework.apollo.assembly.ApolloApplication
如果希望独立启动apollo-configservice和apollo-adminservice,可以把Main Class分别换成 com.ctrip.framework.apollo.configservice.ConfigServiceApplication和 com.ctrip.framework.apollo.adminservice.AdminServiceApplication
VM options配置:username和password要填对。
1 | -Dapollo_profile=github |
可以指定日志文件的路径。
程序默认日志输出为/opt/logs/100003171/apollo-assembly.log,如果需要修改日志文件路径,可以增加logging.file参数,如下:
Dlogging.file=/your-path/apollo-assembly.log
Program arguments配置: –configservice –adminservice
启动完成后,打开http://localhost:8080
可以看到apollo-configservice和apollo-adminservice都已经启动完成并注册到Eureka。
本地启动apollo-portal跟上面启动apollo-configservice和apollo-adminservice很相似。
跟上面一样,创建Application:
配置Application:
如上图所示:
com.ctrip.framework.apollo.portal.PortalApplication
1 | -Dapollo_profile=github,auth |
- 这里指定了apollo_profile是github和auth,其中github是Apollo必须的一个profile,用于数据库的配置,auth是从0.9.0新增的,用来支持使用apollo提供的Spring Security简单认证。
- 程序默认日志输出为/opt/logs/100003173/apollo-portal.log,如果需要修改日志文件路径,可以增加logging.file参数,如下:
- Dlogging.file=/your-path/apollo-portal.log
启动完后,打开 http://localhost:8070 就可以看到Apollo配置中心界面了。
可以在上面做些创建项目的测试。
以创建anna2019项目为例。下图为创建项目后,添加了两个配置。(建议大家一定一定要动手操作下,放心的做测试,不会删库的)
项目中有一个样例客户端的项目:apollo-demo。
跟上面一样,创建Application。
如上图所示:
com.ctrip.framework.apollo.demo.api.SimpleApolloConfigDemo
apollo-demo项目的app.properties文件中:apollo-demo/src/main/resources/META-INF/app.properties会有app.id的配置,这里修改为刚才创建的anna2019.
1 | app.id=anna2019 |
注:
AppId是应用的唯一身份标识
,Apollo客户端使用这个标识来获取应用自己的私有Namespace配置。
对于公共Namespace的配置,没有AppId也可以获取到配置,但是就失去了应用覆盖公共Namespace配置的能力。
运行SimpleApolloConfigDemo,启动成功后,可以看到:
1 | Apollo Config Demo. Please input key to get the value. Input quit to exit. |
在anna2019项目里我们配置了两个配置,Key分别为:yuanrengu、anna999 。
输入:yuanrengu
。可以看到结果如下:
至此完成Apollo的本地运行环境已经搭建完成,并创建项目做了测试。再次重申,大家一定一定要动手操作,这样才能对项目理解更深刻。
见过比较典型的面试场景是这样的:
面试官:请介绍下三次握手
求职者:第一次握手就是客户端给服务器端发送一个报文,第二次就是服务器收到报文之后,会应答一个报文给客户端,第三次握手就是客户端收到报文后再给服务器发送一个报文,三次握手就成功了。
面试官:然后呢?
求职者:这就是三次握手的过程,很简单的。
面试官:。。。。。。
(番外篇:一首凉凉送给你)
记住猿人谷一句话:面试时越简单的问题,一般就是隐藏着比较大的坑,一般都是需要将问题扩展的。上面求职者的回答不对吗?当然对,但距离面试官的期望可能还有点距离。
希望大家能带着如下问题进行阅读,收获会更大。
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小
信息。
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
进行三次握手:
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SENT
状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD
的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED
状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED
状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。
在socket编程中,客户端执行connect()时,将触发三次握手。
用更通俗的语言来解释三次握手过程:
连接请求报文
,其中TCP标志位里SYN=1,ACK=0,选择一个初始的序号x。连接确认报文
,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。发出确认
,确认号为 y+1,序号为 x+1。连接建立
。第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接
。
弄清这个问题,我们需要先弄明白三次握手的目的是什么,能不能只用两次握手来达到同样的目的。
因此,需要三次握手才能确认双方的接收与发送能力是否正常。
试想如果是用两次握手,则会出现下面这种情况:
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。
当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
这里再补充一点关于SYN-ACK 重传次数的问题:
服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s……
关于建连接时
SYN超时
。试想一下,如果server端接到了clien发的SYN后回了SYN-ACK后client掉线了,server端没有收到client回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server端如果在一定时间内没有收到的TCP会重发SYN-ACK。在Linux下,默认重试次数为5次
,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 =63s
,TCP才会把断开这个连接。
当一端为建立连接而发送它的SYN时,它为连接选择一个初始序号。ISN随时间而变化,因此每个连接都将具有不同的ISN。ISN可以看作是一个32比特的计数器,每4ms加1 。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它做错误的解释。
三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。
对于连接的3次握手,主要是要初始化Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)。
这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序
(TCP会用这个序号来拼接数据)。
其实第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据。
为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。
检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态
时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstat 命令来检测 SYN 攻击。
1 | netstat -n -p TCP | grep SYN_RECV |
常见的防御 SYN 攻击的方法有如下几种:
关于SYN Flood攻击。一些恶意的人就为此制造了SYN Flood攻击——给服务器发了一个SYN后,就下线了,于是服务器需要默认等
63s
才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。于是,Linux下给了一个叫tcp_syncookies
的参数来应对这个事——当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。请注意,请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为,synccookies是妥协版的TCP协议,并不严谨。对于正常的请求,你应该调整三个TCP参数可供你选择,第一个是:tcp_synack_retries
可以用他来减少重试次数;第二个是:tcp_max_syn_backlog
,可以增大SYN连接数;第三个是:tcp_abort_on_overflow
处理不过来干脆就直接拒绝连接了。
当第三次握手失败时,服务器并不会重传ack报文,而是直接发送RST报文段,进入CLOSED状态
。这样做的目的是为了防止SYN洪泛攻击
。
三次握手建立连接的首要目的是同步序列号
。只有同步了序列号才有可靠的传输,TCP 协议的许多特性都是依赖序列号实现的,比如流量控制、消息丢失后的重发等等,这也是三次握手中的报文被称为 SYN 的原因,因为 SYN 的全称就叫做 Synchronize Sequence Numbers。
客户端发送 SYN 开启了三次握手,之后客户端连接的状态是 SYN_SENT,然后等待服务器回复 ACK 报文。正常情况下,服务器会在几毫秒内返回 ACK,但如果客户端迟迟没有收到 ACK 会怎么样呢?客户端会重发 SYN,重试的次数由 tcp_syn_retries
参数控制,默认是 6 次:
1 | net.ipv4.tcp_syn_retries = 6 |
第 1 次重试发生在 1 秒钟后,接着会以翻倍的方式在第 2、4、8、16、32 秒共做 6 次重试,最后一次重试会等待 64 秒,如果仍然没有返回 ACK,才会终止三次握手。所以,总耗时是 1+2+4+8+16+32+64=127 秒
,超过 2 分钟。
如果这是一台有明确任务的服务器,你可以根据网络的稳定性和目标服务器的繁忙程度修改重试次数,调整客户端的三次握手时间上限。比如内网中通讯时,就可以适当调低重试次数,尽快把错误暴露给应用程序。
当服务器收到 SYN 报文后,服务器会立刻回复 SYN+ACK
报文,既确认了客户端的序列号,也把自己的序列号发给了对方
。此时,服务器端出现了新连接,状态是 SYN_RCVD。这个状态下,服务器必须建立一个 SYN 半连接队列
来维护未完成的握手信息,当这个队列溢出后,服务器将无法再建立新连接。
如果 SYN 半连接队列已满,只能丢弃连接吗?并不是这样,开启 syncookies
功能就可以在不使用 SYN 队列的情况下成功建立连接。syncookies 是这么做的:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功,如下图所示。
Linux 下怎样开启 syncookies 功能呢?修改 tcp_syncookies
参数即可,其中值为 0 时表示关闭该功能,2 表示无条件开启功能,而 1 则表示仅当 SYN 半连接队列放不下时,再启用它。由于 syncookie 仅用于应对 SYN 泛洪攻击
(攻击者恶意构造大量的 SYN 报文发送给服务器,造成 SYN 半连接队列溢出,导致正常客户端的连接无法建立),这种方式建立的连接,许多 TCP 特性都无法使用。所以,应当把 tcp_syncookies 设置为 1,仅在队列满时再启用
。
当客户端接收到服务器发来的 SYN+ACK 报文后,就会回复 ACK 去通知服务器,同时己方连接状态从 SYN_SENT 转换为 ESTABLISHED,表示连接建立成功。服务器端连接成功建立的时间还要再往后,到它收到 ACK 后状态才变为 ESTABLISHED。如果服务器没有收到 ACK,就会一直重发 SYN+ACK 报文。当网络繁忙、不稳定时,报文丢失就会变严重,此时应该调大重发次数。反之则可以调小重发次数。
tcp_synack_retries
的默认重试次数是5 次
,与客户端重发 SYN 类似,它的重试会经历 1、2、4、8、16 秒,最后一次重试后等待 32 秒,若仍然没有收到 ACK,才会关闭连接,故共需要等待 63 秒
。
服务器收到 ACK 后连接建立成功,此时,内核会把连接从 SYN 半连接队列中移出,再移入 accept 队列,等待进程调用 accept 函数时把连接取出来
。如果进程不能及时地调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃。
实际上,丢弃连接只是 Linux 的默认行为,我们还可以选择向客户端发送 RST 复位报文
,告诉客户端连接已经建立失败
。打开这一功能需要将 tcp_abort_on_overflow
参数设置为 1。
建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭(half-close)造成的。所谓的半关闭
,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
TCP 连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务端均可主动发起挥手动作。
刚开始双方都处于ESTABLISHED
状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
FIN_WAIT1
状态。CLOSE_WAIT
状态。LAST_ACK
的状态。TIME_WAIT
状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED
状态。收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。
在socket编程中,任何一方执行close()操作即可产生挥手操作。
通过上面的分析,可以看出四次挥手涉及两种报文:FIN 和 ACK。FIN 就是 Finish 结束连接的意思,谁发出 FIN 报文,就表示它将不再发送任何数据,关闭这一方向的传输通道。ACK 是 Acknowledge 确认的意思,它用来通知对方:你方的发送通道已经关闭
。
四次挥手过程总结:
FIN_WAIT1
。当被动方收到 FIN 报文后,内核自动回复 ACK 报文,连接状态由 ESTABLISHED 变为 CLOSE_WAIT
,顾名思义,它在等待进程调用 close 函数关闭连接。当主动方接收到这个 ACK 报文后,连接状态由 FIN_WAIT1 变为 FIN_WAIT2
,主动方的发送通道就关闭了。close 函数
,进而触发内核发送 FIN 报文,此时被动方连接的状态变为 LAST_ACK
。当主动方收到这个 FIN 报文时,内核会自动回复 ACK,同时连接的状态由 FIN_WAIT2 变为 TIME_WAIT,Linux 系统下大约 1 分钟
后 TIME_WAIT 状态的连接才会彻底关闭。而被动方收到 ACK 报文后,连接就会关闭。这是因为 TCP 不允许连接处于半打开状态
时就单向传输数据,所以在三次握手建立连接时,服务器会把 ACK 和 SYN 放在一起发给客户端,其中,ACK 用来打开客户端的发送通道,SYN 用来打开服务器的发送通道。这样,原本的四次握手就降为三次握手了。
但是当连接处于半关闭状态
时,TCP 是允许单向传输数据的。为便于理解,我们把先关闭连接的一方叫做主动方,后关闭连接的一方叫做被动方。当主动方关闭连接时,被动方仍然可以在不调用 close 函数的状态下,长时间发送数据,此时连接处于半关闭状态。这一特性是 TCP 的双向通道互相独立所致
,却也使得关闭连接必须通过四次挥手才能做到。
TIME_WAIT状态也称为2MSL等待状态
。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。
对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。
这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。
MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器
。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。
保证客户端发送的最后一个ACK报文段能够到达服务端。
这个ACK报文段有可能丢失,使得处于LAST-ACK状态的服务端收不到对已发送的FIN+ACK报文段的确认,服务端超时重传FIN+ACK报文段,而客户端能在2MSL时间内收到这个重传的FIN+ACK报文段,接着客户端重传一次确认,重新启动2MSL计时器,最后客户端和服务端都进入到CLOSED状态,若客户端在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到服务端重传的FIN+ACK报文段,所以不会再发送一次确认报文段,则服务端无法正常进入到CLOSED状态。
防止“已失效的连接请求报文段”出现在本连接中。
客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
关于 MSL 和 TIME_WAIT。通过上面的ISN的描述,相信你也知道MSL是怎么来的了。我们注意到,在TCP的状态图中,从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是 2*MSL(RFC793定义了MSL为2分钟,Linux设置成了30s)为什么要这有TIME_WAIT?为什么不直接给转成CLOSED状态呢?主要有两个原因:1)TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发
被动端重发Fin,一来一去正好2个MSL
,2)有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)。
TIME_WAIT 状态的连接,在主动方看来确实已经关闭了。然而,被动方没有收到 ACK 报文前,连接还处于 LAST_ACK 状态。如果这个 ACK 报文没有到达被动方,被动方就会重发
FIN 报文。
如果主动方不保留 TIME_WAIT 状态,会发生什么呢?
此时连接的端口恢复了自由身,可以复用于新连接了
。然而,被动方的 FIN 报文可能再次到达,这既可能是网络中的路由器重复发送,也有可能是被动方没收到 ACK 时基于 tcp_orphan_retries 参数重发。这样,正常通讯的新连接就可能被重复发送的 FIN 报文误关闭
。保留 TIME_WAIT 状态,就可以应付重发的 FIN 报文,当然,其他数据报文也有可能重发,所以 TIME_WAIT 状态还能避免数据错乱。
理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
TIME-WAIT状态如果过多,会占用系统资源。Linux下有几个参数可以调整TIME-WAIT状态时间:
在socket的TIME_WAIT状态结束之前,该socket所占用的本地端口号将一直无法释放。高TCP并发并且采用短连接方式进行通讯的通讯系统在高并发高负载下运行一段时间后,就常常会出现做为客户端的程序无法向服务端建立新的socket连接的情况。
此时用“netstat -tanlp”命令查看系统将会发现机器上存在大量处于TIME_WAIT状态的socket连接,并且占用大量的本地端口号
。最后,当该机器上的可用本地端口号被占完(或者达到用户可使用的文件句柄上限),而旧的大量处于TIME_WAIT状态的socket尚未被系统回收时,就会出现无法向服务端创建新的socket连接的情况。此时系统几乎停转,空有再好的性能也发挥不出来。
解决TIME-WAIT状态过多的情况,一般做法是打开系统的TIMEWAIT重用和快速回收
。然而,主动进行关闭的链接才会进入TIME-WAIT状态
,所以最好的办法:尽量不要让服务器主动关闭链接,除非一些异常情况,如客户端协议错误、客户端超时等等。
解决方法?
《TCP/IP详解 卷1:协议》有一张TCP状态变迁图,很具有代表性,有助于大家理解三次握手和四次挥手的状态变化。如下图所示,粗的实线箭头表示正常的客户端状态变迁,粗的虚线箭头表示正常的服务器状态变迁。
以后面试官再问你三次握手和四次挥手,直接把这一篇文章丢给他就可以了,他想问的都在这里。
参考:
《TCP/IP详解 卷1:协议》
状态转换伪代码
https://coolshell.cn/articles/11564.html
极客时间:系统性能调优必知必会
这里给小伙伴要再次说明下,任何知识点,先抓主干,再摸细节。对于面试来说,能把各个主干捋清楚,只要面试官要求不是太高,都是能过关的。毕竟jvm参数那么多,难不成面试官揪着各个参数的作用不放?如果真遇到这种太过揪细节的,只能说江湖路远,有缘再见!
对象的内存分配,往大方向上讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下可能会直接分配在老年代中。
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(前面篇章中有介绍过Minor GC)。但也有一种情况,在内存担保机制下,无法安置的对象会直接进到老年代。
大对象时指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。目的就是避免在Eden区及两个Survivor区之间发生大量的内存复制。
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1 。对象在Survivor区中没经过一次Minor GC,年龄就加1岁,当年龄达到15岁(默认值),就会被晋升到老年代中。
对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
真的不是为什么不能是其它数(除了15),着实是臣妾做不到啊!
事情是这样的,HotSpot虚拟机的对象头其中一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark word”。
例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的32bit空间中25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0 。
明白是什么原因了吗?对象的分代年龄占4位,也就是0000,最大值为1111也就是最大为15,而不可能为16,20之类的了。
为了能更好的适应不同程序的内存状况,虚拟机并不是永远地要求兑现过的年龄必须达到了MaxTenuringThreshold才能晋升老年代。
很多文章都只是注意到了上面描述的情况(包括阿里中间件公众号发的一篇文章里也只是这么简单的介绍),但如果只是这么认识的话,会发现在实际的内存回收中有悖于此条规定。
举个小栗子,如对象年龄5的占34%,年龄6的占36%,年龄7的占30%,按那两个标准,对象是不能进入老年代的,但Survivor都已经100%了啊?
大家可以关注这个参数TargetSurvivorRatio,目标存活率,默认为50%。大致意思就是说年龄从小到大累加,如加入某个年龄段(如栗子中的年龄6)后,总占用超过Survivor空间TargetSurvivorRatio的时候,从该年龄段开始及大于的年龄对象就要进入老年代(即栗子中的年龄6,7对象)。动态对象年龄判断,主要是被TargetSurvivorRatio这个参数来控制。而且算的是年龄从小到大的累加和,而不是某个年龄段对象的大小。
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC 。
上面说的风险是什么呢?我们知道,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。
总结脑图:
CMS和G1作为垃圾收集器里的大杀器,是需要好好弄明白的,而且面试中也经常被问到。
希望大家带着下面的问题进行阅读,有目标的阅读,收获更多:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。这是因为CMS收集器工作时,GC工作线程与用户线程可以并发
执行,以此来达到降低收集停顿时间的目的。
CMS收集器仅作用于老年代的收集,是基于标记-清除算法
的,它的运作过程分为4个步骤:
其中,初始标记
、重新标记
这两个步骤仍然需要Stop-the-world。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。
CMS以流水线方式拆分了收集周期,将耗时长的操作单元保持与应用线程并发执行。只将那些必需STW才能执行的操作单元单独拎出来,控制这些单元在恰当的时机运行,并能保证仅需短暂的时间就可以完成。这样,在整个收集周期内,只有两次短暂的暂停(初始标记和重新标记),达到了近似并发的目的。
CMS收集器优点:并发收集、低停顿。
CMS收集器缺点:
CMS收集器之所以能够做到并发,根本原因在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。前面篇章介绍过标记-清除算法将产生大量的内存碎片这对新生代来说是难以接受的,因此新生代的收集器并未提供CMS版本。
另外要补充一点,JVM在暂停的时候,需要选准一个时机。由于JVM系统运行期间的复杂性,不可能做到随时暂停,因此引入了安全点的概念。
CMS在JDK9中已经被标记deprecated,目前被广泛使用的垃圾回收器是 G1
,通过很少的参数配置,内存即可高效回收。CMS 垃圾回收器已经在 Java 14 中被移除,由于它的 GC 时间不可控,有条件应该尽量避免使用。
安全点,即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。
安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。只要不离开这个安全点,Java虚拟机便能够在垃圾回收的同时,继续运行这段本地代码。
程序运行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
对于安全点,另一个需要考虑的问题就是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。
两种解决方案:
抢先式中断(Preemptive Suspension)
抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。
主动式中断(Voluntary Suspension)
主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
指在一段代码片段中,引用关系不会发生变化。在这个区域中任意地方开始GC都是安全的。也可以把Safe Region看作是被扩展了的Safepoint。
默认收集器:
G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测
的收集模型:用户可以指定收集操作在多长时间内完成。即G1提供了接近实时的收集特性。G1 的主要关注点在于达到可控的停顿时间,在这个基础上尽可能提高吞吐量。
G1 使用了停顿预测模型来满足用户指定的停顿时间目标,并基于目标来选择进行垃圾回收的区块数量。G1 采用增量回收
的方式,每次回收一些区块,而不是整堆回收。要清楚 G1 不是一个实时收集器(只是接近实时),它会尽力满足我们的停顿时间要求,但也不是绝对的,它基于之前垃圾收集的数据统计,估计出在用户指定的停顿时间内能收集多少个区块。
G1与CMS的特征对比如下:
特征 | G1 | CMS |
---|---|---|
并发和分代 | 是 | 是 |
最大化释放堆内存 | 是 | 否 |
低延时 | 是 | 是 |
吞吐量 | 高 | 低 |
压实 | 是 | 否 |
可预测性 | 强 | 弱 |
新生代和老年代的物理隔离 | 否 | 是 |
G1具备如下特点:
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。在堆的结构设计时,G1打破了以往将收集范围固定在新生代或老年代的模式,G1收集器将整个Java堆划分为多个大小相等的独立区域(Region)。Region是一块地址连续的内存空间,G1模块的组成如下图所示:
虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。Region的大小是一致的,数值是在1M到64M
字节之间的一个2的幂值数,JVM会尽量划分2048
个左右(默认情况下,它期望堆中区域的数量在2048到4095之间,如果不在,它会调整区域的大小来实现这个目标)、同等大小的Region,这一点可以参看如下源码。其实这个数字既可以手动调整,G1也会根据堆大小自动进行调整。
1 | #ifndef SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP |
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Regions作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。
对于打算从CMS或者ParallelOld收集器迁移过来的应用,按照官方 的建议,如果发现符合如下特征,可以考虑更换成G1收集器以追求更佳性能:
原文如下:
Applications running today with either the CMS or the ParallelOld garbage collector would benefit switching to G1 if the application has one or more of the following traits.
- More than 50% of the Java heap is occupied with live data.
- The rate of object allocation rate or promotion varies significantly.
- Undesired long garbage collection or compaction pauses (longer than 0.5 to 1 second)
G1收集的运作过程大致如下:
停顿线程
,但耗时很短。停顿线程
,但是可并行执行。全局变量和栈中引用的对象是可以列入根集合的,这样在寻找垃圾时,就可以从根集合出发扫描堆空间。在G1中,引入了一种新的能加入根集合的类型,就是记忆集
(Remembered Set)。Remembered Sets(也叫RSets)用来跟踪对象引用。G1的很多开源都是源自Remembered Set,例如,它通常约占Heap大小的20%或更高。并且,我们进行对象复制的时候,因为需要扫描和更改Card Table的信息,这个速度影响了复制的速度,进而影响暂停时间。
G1 比 ParallelOld 和 CMS 会需要更多的内存消耗,那是因为有部分内存消耗于簿记(accounting)上,如以下两个数据结构:
有个场景,老年代的对象可能引用新生代的对象,那标记存活对象的时候,需要扫描老年代中的所有对象。因为该对象拥有对新生代对象的引用,那么这个引用也会被称为GC Roots。那不是得又做全堆扫描?成本太高了吧。
HotSpot给出的解决方案是一项叫做卡表
(Card Table)的技术。该技术将整个堆划分为一个个大小为512字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。
在进行Minor GC的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成所有脏卡的扫描之后,Java虚拟机便会将所有脏卡的标识位清零。
想要保证每个可能有指向新生代对象引用的卡都被标记为脏卡,那么Java虚拟机需要截获每个引用型实例变量的写操作,并作出对应的写标识位操作。
卡表能用于减少老年代的全堆空间扫描,这能很大的提升GC效率。
我们可以看下官方文档对G1的展望(这段英文描述比较简单,我就不翻译了):
Future:
G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). Comparing G1 with CMS, there are differences that make G1 a better solution. One difference is that G1 is a compacting collector. G1 compacts sufficiently to completely avoid the use of fine-grained free lists for allocation, and instead relies on regions. This considerably simplifies parts of the collector, and mostly eliminates potential fragmentation issues. Also, G1 offers more predictable garbage collection pauses than the CMS collector, and allows users to specify desired pause targets.
在G1的情况下,只要收集器能跟上分配率,那么增量压缩
就可能完全避免并发模式失败。对于分配率高且稳定,而且大部分对象寿命很短的应用程序,我们可以通过如下方式进行调优:
将新生代设置得较大
以这种方式配置Eden和Survivor空间,可以尽可能使真正的短寿命对象不被晋升。
ZGC(Z Garbage Collector)作为一种比较新的收集器,目前还没有得到大范围的关注。作为一款低延迟的垃圾收集器,它有如下几个亮点:
去年看官网文档时,写的还是停顿时间不超10ms,现在竟然将停顿时间控制在亚毫秒级别,大写的服气!!!
在ZGC中,连逻辑上的也是重新定义了堆空间(不区分年轻代和老年代),只分为一块块的page,每次进行GC时,都会对page进行压缩操作,所以没有碎片问题。虽然ZGC属于很新的GC技术, 但优点不一定真的出众,ZGC只在特定情况下具有绝对的优势, 如巨大的堆和极低的暂停需求
。而实际上大多数开发在这两方面都不太成问题(尤其是在服务器端), 而对GC的性能/效率更在意。也有一种观点认为ZGC是为大内存、多cpu而生,它通过分区的思路来降低STW。
ZGC在JDK14前只支持Linux, 从JDK14开始支持Mac和Windows。
可以从官网看下ZGC的Change Log:
可以看出ZGC未来可期,让我们拭目以待吧。
目前被广泛使用的垃圾回收器是 G1
,通过很少的参数配置,内存即可高效回收。CMS在JDK9中已经被标记deprecated,更高版本中(Java 14 )已经被彻底移除,由于它的 GC 时间不可控,有条件应该尽量避免使用。
查了下度娘有关G1的文章,绝大部分文章对G1的介绍都是停留在JDK7或更早期的实现很多结论已经存在较大偏差了,甚至一些过去的GC选项已经不再推荐使用。举个例子,JDK9中JVM和GC日志进行了重构,如PrintGCDetails已经被标记为废弃,而PrintGCDateStamps已经被移除,指定它会导致JVM无法启动。
本文对CMS和G1的介绍绝大部分内容也是基于JDK7,新版本中的内容有一点介绍,倒没做过多介绍(本人对新版本JVM还没有深入研究),后面有机会可以再出专门的文章来重点介绍。
《深入理解Java虚拟机》
《HotSpot实战》
https://wiki.openjdk.java.net/display/zgc/Main
https://plumbr.io/handbook/garbage-collection-algorithms-implementations#g1
https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
有几个小伙伴提出了希望出一篇介绍对象的创建及访问,猿人谷向来是没有原则的,小伙们要求啥,咱就尽力满足,毕竟文章就是对自己学习的一个总结及和各位小伙伴交流学习的机会。话不多说,直接开撸!
在Java程序运行过程中无时无刻都有对象被创建出来,java中对象可以采用new或反射或clone或反序列化的方法创建。接下来我们我们介绍在虚拟机中,对象(限于普通Java对象,不包括数组和Class对象等)的创建过程。
字节码new表示创建对象,虚拟机遇到该指令时,从栈顶取得目标对象在常量池中的索引,接着定位到目标对象的类型。接下来,虚拟机将根据该类的状态,采取相应的内存分配技术,在内存中分配实例空间,并完成实例数据和对象头的初始化。这样,一个对象就在JVM中创建好了。
实例的创建过程,首先根据从类常量池中获取对象类型信息并验证类是否已被解析过,若确保该类已被加载和正确解析,使用快速分配(fast allocation)技术为该类分配对象空间;若该类尚未解析过,则只能通过慢速分配(slow allocation)方式分配实例对象。实例的创建流程如下图所示。
对象创建的基本流程:
如果在实例分配之前已经完成了类型的解析,那么分配操作仅仅是在内存空间中划分可用内存,因此能以较高效率实现内存分配,这就是快速分配。
根据分配空间是来自于线程私有区域还是共享的堆空间,快速分配可以分为两种空间选择策略。HotSpot通过线程局部分配缓存技术(Thread-Local Allocation Buffers,即TLABs)可以在线程私有区域实现空间的分配。
可以通过VM选项UseTLAB来开启或关闭TLAB功能。
根据是否使用TLAB,快速分配方式有两种选择策略:
实例空间分配成功以后,将对实例进行初始化。待完成对象的空间分配和初始化后,就可以设置栈顶对象引用。当然,对象的空间分配和初始化操作都是基于从类常量池中获取对象类型并确保该类已被加载和正确解析的前提下进行的,如果类未被解析,则需要进行慢速分配。
之所以成为慢速分配,正是因为在分配实例前需要对类进行解析,确保类及依赖类已得到正确的解析和初始化。慢速分配是调用InterpreterRuntime模块_new()进行的,实现代码如下。
1 | // 确保要初始化的类不是抽象类型 |
对象分配流程大致如下
:首先如果开启栈上分配,JVM会先进行栈上分配,如果没有开启栈上分配或不符合条件则会进行TLAB分配,如果TLAB分配不成功,再尝试在eden区分配,如果对象满足了直接进入老年代的条件,那就直接分配在老年代。在eden区和老年代分配主要通过“指针碰撞”和“空闲列表”两种方式实现,通过CAS解决堆上“非TLAB方式分配”的并发问题。
建立对象是为了使用对象,Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。
目前主流的访问方式有使用句柄和直接指针两种:
如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如下图所示。
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。即使用直接指针访问在对象被移动时reference本身需要被修改,reference存储的就是对象地址。如下图所示。
这两种对象访问方式各有优势:
HotSpot就是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
]]>