0%

makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cc = gcc
prom = multisum
deps = $(shell find ./ -name "*.h")
src = $(shell find ./ -name "*.c")
obj = $(src:%.c=%.o)

$(prom): $(obj)
$(cc) -o $(prom) $(obj) -lpthread

%.o: %.c $(deps)
$(cc) -c $< -o $@

clean:
rm -rf $(obj) $(prom)

线程

使用

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
#include<stdio.h>
#include <pthread.h>

void thread(void) {
int i;
for(i = 0; i < 30; i++) {
printf("This is a pthread.\n");
}
}

int main(int argc, char const *argv[]) {
pthread_t id;
int i, ret;
ret = pthread_create(&id, NULL, (void *) thread, NULL); // 成功返回0,错误返回错误编号
if(ret != 0) {
printf ("Create pthread error!\n");
return -1;
}
pthread_join(id, NULL);
for(i = 0; i < 30; i++) {
printf("This is the main process.\n");
}

return 0;
}
  • pthread_t id:

  • int pthread_create(&id, NULL, (void *) thread, (void *)arg):

    1. 第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
    2. 多个参数:必须申明一个结构体来包含所有的参数,然后再传入线程函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct  parameter {
int size,
int count;
};

// 然后在main函数将这个结构体指针,作为void *形参的实际参数传递
struct parameter arg;
pthread_create(&ntid, NULL, fn,& (arg));

// 函数中需要定义一个parameter类型的结构指针来引用这个参数
void thr_fn(void *arg) {
struct parameter *pstru;
pstru = ( struct parameter *) arg;
// 然后在这个函数中就可以使用指针来使用相应的变量的值了。
}
  • int pthread_join(id, NULL);

    函数pthread_join用来等待一个线程的结束。第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是像上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。

  • void pthread_exit(arg);

    唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。

  • 在主线程中初始化锁为解锁状态

    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);

  • 在编译时初始化锁为解锁状态

    锁初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

  • 访问对象时的加锁操作与解锁操作

    加锁 pthread_mutex_lock(&mutex)
    释放锁 pthread_mutex_unlock(&mutex)

信号量

信号量本质上是一个非负数的整数计数器,它也被用来控制对公共资源的访问。当公共资源增加的时候,调用信号量增加函数sem_post()对其进行增加,当公共资源减少的时候,调用函数sem_wait()来减少信号量。其实,我们是可以把锁当作一个0-1信号量的。

在使用semaphore之前,需要先引入头文件semaphore.h

  • 初始化信号量: int sem_init(sem_t *sem, int pshared, unsigned int value);

    • 成功返回0,失败返回-1
    • sem:指向信号量结构的一个指针
    • pshared: 不是0的时候,该信号量在进程间共享,否则只能为当前进程的所有线程们共享
    • value:信号量的初始值
  • 信号量减1操作,当sem=0的时候该函数会堵塞 int sem_wait(sem_t *sem);

    • 成功返回0,失败返回-1
    • 参数
    • sem:指向信号量的一个指针
  • 信号量加1操作 int sem_post(sem_t *sem);

    • 参数与返回同上
  • 销毁信号量 int sem_destroy(sem_t *sem);

    • 参数与返回同上
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
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

#define MAXSIZE 10

int stack[MAXSIZE];
int size = 0;
sem_t sem;

// 生产者
void provide_data(void) {
int i;
for (i=0; i< MAXSIZE; i++) {
stack[i] = i;
sem_post(&sem); //为信号量加1
}
}

// 消费者
void handle_data(void) {
int i;
while((i = size++) < MAXSIZE) {
sem_wait(&sem);
printf("乘法: %d X %d = %d\n", stack[i], stack[i], stack[i]*stack[i]);
sleep(1);
}
}

int main(void) {

pthread_t provider, handler;

sem_init(&sem, 0, 0); //信号量初始化
pthread_create(&provider, NULL, (void *)handle_data, NULL);
pthread_create(&handler, NULL, (void *)provide_data, NULL);
pthread_join(provider, NULL);
pthread_join(handler, NULL);
sem_destroy(&sem); //销毁信号量

return 0;
}

实例

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>

static int N = 0;
static long M = 0;
static long sum = 0;
static long count = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

const static char *fileInput = "input.txt";
const static char *fileOutput = "output.txt";

void getInput() {
FILE *pFile = fopen(fileInput, "r");
char line[20];
if (pFile == NULL) {
perror("Error opening file");
} else {
fseek(pFile, 2, SEEK_SET);
if (fgets(line, 20, pFile) != NULL) {
N = atoi(line);
}

fseek(pFile, ftell(pFile)+2, SEEK_SET);
if (fgets(line, 20, pFile) != NULL) {
M = atol(line);
}
fclose(pFile);
}
}

void setOutput() {
FILE *pFile = fopen(fileOutput, "w");
if (pFile == NULL) {
perror("Error opening file");
} else {
fprintf(pFile,"%ld", sum);
fclose(pFile);
}
}

void threadSum() {
while (count <= M) {
pthread_mutex_lock(&mutex);
sum += count++;
printf("the thread result = %ld\n", sum);
pthread_mutex_unlock(&mutex);
}
}

int main(int argc, char const *argv[]) {
clock_t start = clock();

getInput();

printf("N = %d\n", N);
printf("M = %ld\n", M);

int ret;
pthread_t thread[N];
for (int i = 0; i < N; i++) {
ret = pthread_create(&thread[i], NULL, (void *)threadSum, NULL);

if(ret != 0) {
printf("create pthread-%d error!\n", i);
return -1;
}
}

for (int i = 0; i < N; i++) {
pthread_join(thread[i], NULL);
}

setOutput();

printf("the final result = %ld\n", sum);
printf("the tatol time = %ld\n", clock()-start);

return 0;
}

线程

fork

fork()会产生一个和父进程完全相同的子进程,可以认为fork后,这两个相同的虚拟地址指向的是不同的物理地址。但实际上,Linux为了提高 fork 的效率,采用了 copy-on-write 技术(写时复制),fork后,这两个虚拟地址实际上指向相同的物理地址(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不同的物理地址(新的物理地址的内容从原物理地址中复制得到)。

共享内存

1
2
3
4
5
#include <sys/shm.h>
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
int shmdt(const void *shm_addr);
int shmget(key_t key, size_t size, int shmflg);

同步互斥

  • 信号量(PV)
  • 管程
  1. #include <semaphore.h>

  2. sem_init函数

    • 函数作用:初始化信号量
    • 函数原型:int sem_init(sem_t *sem,int pshared, unsigned int value)
    • 参数:sem:信号量指针
    • Pshared:决定信号量能否在几个进程间共享,一般取0
    • Value:信号量的初始值
  3. 信号的操作

    • int sem_wait(sem_t *sem); P操作
    • int sem_try_wait(sem_t *sem);
    • int sempost(sem_t *sem); V操作
    • int sem_getvalue(sem_t *sem);
    • int sem_destroy(sem_t *sem); 销毁信号

实例

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/shm.h>
#include <semaphore.h>

#define PERM S_IRUSR|S_IWUSR

static int N = 0;
static long M = 0;

const static char *fileInput = "input.txt";
const static char *fileOutput = "output.txt";

typedef struct {
long sum;
long count;
sem_t S;
} Process;

void getInput() {
FILE *pFile = fopen(fileInput, "r");
char line[20];
if (pFile == NULL) {
perror("Error opening file");
} else {
fseek(pFile, 2, SEEK_SET);
if (fgets(line, 20, pFile) != NULL) {
N = atoi(line);
}

fseek(pFile, ftell(pFile)+2, SEEK_SET);
if (fgets(line, 20, pFile) != NULL) {
M = atol(line);
}
fclose(pFile);
}
}

void setOutput(long result) {
FILE *pFile = fopen(fileOutput, "w");
if (pFile == NULL) {
perror("Error opening file");
} else {
fprintf(pFile,"%ld", result);
fclose(pFile);
}
}

int main(int argc, char const *argv[]) {
clock_t start = clock();

getInput();

printf("N = %d\n", N);
printf("M = %ld\n", M);

int shm_id;
Process *pProcess;

if((shm_id = shmget(IPC_PRIVATE, sizeof(Process), PERM)) == -1){
printf("Create Share Memory Error.\n");
return -1;
}

pProcess = (Process *)shmat(shm_id, NULL, 0);
pProcess->sum = 0;
pProcess->count = 0;

sem_init(&pProcess->S, 0, 1);

pid_t pid;
int status;

for (int i = 0; i < N; i++) {
pid =fork();
if (pid == 0 || pid == -1) {
break;
}
}

if(pid==-1) {
printf("fail to fork!\n");
return -1;
} else if(pid == 0) {
while (pProcess->count <= M) {
sem_wait(&pProcess->S);
pProcess->sum += pProcess->count++;
printf("the process result = %ld\n", pProcess->sum);
sem_post(&pProcess->S);
}
} else {
wait(&status);

sem_destroy(&pProcess->S);
setOutput(pProcess->sum);

printf("the final result = %ld\n", pProcess->sum);
printf("the tatol time = %ld\n", clock()-start);
}

return 0;
}

常用命令

查看文件夹大小

1
2
$ du -h --max-depth=1
$ du -h -d 1

ulimit

ulimit命令用来限制系统用户对shell资源的访问。

假设有这样一种情况,当一台 Linux 主机上同时登陆了 10 个人,在系统资源无限制的情况下,这 10 个用户同时打开了 500 个文档,而假设每个文档的大小有 10M,这时系统的内存资源就会受到巨大的挑战。

而实际应用的环境要比这种假设复杂的多,例如在一个嵌入式开发环境中,各方面的资源都是非常紧缺的,对于开启文件描述符的数量,分配堆栈的大小,CPU 时间,虚拟内存大小,等等,都有非常严格的要求。资源的合理限制和分配,不仅仅是保证系统可用性的必要条件,也与系统上软件运行的性能有着密不可分的联系。这时,ulimit可以起到很大的作用,它是一种简单并且有效的实现资源限制的方式。

ulimit 用于限制 shell 启动进程所占用的资源,支持以下各种类型的限制:所创建的内核文件的大小、进程数据块的大小、Shell 进程创建文件的大小、内存锁住的大小、常驻内存集的大小、打开文件描述符的数量、分配堆栈的最大大小、CPU 时间、单个用户的最大线程数、Shell 进程所能使用的最大虚拟内存。同时,它支持硬资源和软资源的限制。

作为临时限制,ulimit 可以作用于通过使用其命令登录的 shell 会话,在会话终止时便结束限制,并不影响于其他 shell 会话。而对于长期的固定限制,ulimit 命令语句又可以被添加到由登录 shell 读取的文件中,作用于特定的 shell 用户。

  • 语法

    1
    ulimit option
  • 选项

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    -a:显示目前资源限制的设定;
    -c <core文件上限>:设定core文件的最大值,单位为区块;
    -d <数据节区大小>:程序数据节区的最大值,单位为KB;
    -f <文件大小>:shell所能建立的最大文件,单位为区块;
    -H:设定资源的硬性限制,也就是管理员所设下的限制;
    -m <内存大小>:指定可使用内存的上限,单位为KB;
    -n <文件数目>:指定同一时间最多可开启的文件数;
    -p <缓冲区大小>:指定管道缓冲区的大小,单位512字节;
    -s <堆叠大小>:指定堆叠的上限,单位为KB;
    -S:设定资源的弹性限制;
    -t <CPU时间>:指定CPU使用时间的上限,单位为秒;
    -u <程序数目>:用户最多可开启的程序数目;
    -v <虚拟内存大小>:指定可使用的虚拟内存上限,单位为KB。
  • 实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [root@localhost ~]# ulimit -a
    core file size (blocks, -c) 0 #core文件的最大值为100 blocks。
    data seg size (kbytes, -d) unlimited #进程的数据段可以任意大。
    scheduling priority (-e) 0
    file size (blocks, -f) unlimited #文件可以任意大。
    pending signals (-i) 98304 #最多有98304个待处理的信号。
    max locked memory (kbytes, -l) 32 #一个任务锁住的物理内存的最大值为32KB。
    max memory size (kbytes, -m) unlimited #一个任务的常驻物理内存的最大值。
    open files (-n) 1024 #一个任务最多可以同时打开1024的文件。
    pipe size (512 bytes, -p) 8 #管道的最大空间为4096字节。
    POSIX message queues (bytes, -q) 819200 #POSIX的消息队列的最大值为819200字节。
    real-time priority (-r) 0
    stack size (kbytes, -s) 10240 #进程的栈的最大值为10240字节。
    cpu time (seconds, -t) unlimited #进程使用的CPU时间。
    max user processes (-u) 98304 #当前用户同时打开的进程(包括线程)的最大个数为98304。
    virtual memory (kbytes, -v) unlimited #没有限制进程的最大地址空间。
    file locks (-x) unlimited #所能锁住的文件的最大个数没有限制。

curl

检查url有效性

1
2
3
4
5
6
urlstatus=$(curl -s -m 5 -IL $url|grep 200)
if [ "$urlstatus" != "" ];then
wget $url
else
echo "illegal url: $url"
fi

jq

可以使用jq解析json。

1
2
3
4
# Usage: jq [options] <jq filter> [file...] 
$ sudo apt install jq
$ curl -s url | jq -r '.result.page_size'
$ echo $json|jq -r '.result.data[0]' # 数组

实例:分页请求服务器,每页请求返回page_size条数据,下载每条数据中的视频/图片

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
#!/bin/bash

echo "Begin to download..."
urlPattern="http://url?version=3&page="

for page in `seq 1 20`
do
url="$urlPattern$page"
echo "fetch content: $url"

urlstatus=$(curl -s -m 5 -IL $url|grep 200)
if [ "$urlstatus" != "" ];then
json=$(curl -s $url)
pageSize=`expr $(echo $json|jq -r '.result.page_size') - 1`
echo "the page size is $pageSize"
for index in `seq 0 $pageSize`
do
item=$(echo $json|jq -r ".result.data[$index]")
urlVideo=$(echo $item|jq -r '.url')
urlImg=$(echo $item|jq -r '.url_img')
echo "download video: $urlVideo"
wget $urlVideo>/dev/null 2>&1
echo "download image: $urlImg"
wget $urlImg>/dev/null 2>&1
done
else
echo "illegal url: $url"
fi
done

proxy

1
2
3
4
5
6
7
8
9
10
11
12
# 设置代理
$ export http_proxy=ip:port

# 取消代理
$ unset http_proxy

# 也可以
$ export https_proxy=https://127.0.0.1:1080
# 设置socks代理
$ export http_proxy=socks://127.0.0.1:1080
$ export http_proxy=sock4://127.0.0.1:1080
$ export http_proxy=sock5://127.0.0.1:1080

变量

定义变量

定义变量时,变量名不加美元符号,变量名和等号之间不能有空格,已定义的变量,可以被重新定义,如:

1
your_name="runoob.com"

除了显式地直接赋值,还可以用语句给变量赋值,如:

1
2
3
for file in `ls /etc`

for file in $(ls /etc)

使用变量

使用一个定义过的变量,只要在变量名前面加美元符号即可,如:

1
2
3
your_name="qinjx"
echo $your_name
echo ${your_name}

只读变量

使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

1
2
3
4
#!/bin/bash
myUrl="http://www.google.com"
readonly myUrl
myUrl="http://www.runoob.com"

删除变量

使用 unset 命令可以删除变量。语法:

1
unset variable_name

输出为变量

1
arr=$(command)

字符串

字符串可以用单引号,也可以用双引号,也可以不用引号。

单引号

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的;
  • 单引号字串中不能出现单引号(对单引号使用转义符后也不行)。

双引号

  • 双引号里可以有变量
  • 双引号里可以出现转义字符

拼接字符串

1
2
3
4
your_name="qinjx"
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting $greeting_1

获取字符串长度

1
2
string="abcd"
echo ${#string} #输出 4

提取子字符串

1
2
string="runoob is a great site"
echo ${string:1:4} # 输出 unoo

查找子字符串

查找字符 “i 或 s” 的位置(输出index从1开始),如果i存在则输出i的位置,不存在则输出s的位置,若都不存在则输出0:

1
2
string="runoob is a great company"
echo `expr index "$string" is` # 输出 8

注意: 以上脚本中 ` 是反引号,而不是单引号 ‘,不要看错了哦。

Shell 数组

bash支持一维数组(不支持多维数组),并且没有限定数组的大小。数组元素的下标由 0 开始编号。获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于或等于0。

定义数组

在 Shell 中,用括号来表示数组,数组元素用”空格”符号分割开。定义数组的一般形式为:

数组名=(值1 值2 … 值n)

1
2
3
4
5
6
7
8
array_name=(value0 value1 value2 value3)

array_name=(
value0
value1
value2
value3
)

还可以单独定义数组的各个分量:

1
2
3
array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen

可以不使用连续的下标,而且下标的范围没有限制。

读取数组

读取数组元素值的一般格式是:

${数组名[下标]}

使用 @ 符号可以获取数组中的所有元素:

echo ${array_name[@]}

获取数组的长度

获取数组长度的方法与获取字符串长度的方法相同:

1
2
3
4
5
6
#取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}

注释

以”#”开头的行就是注释,会被解释器忽略。sh里没有多行注释,只能每一行加一个#号。
如果在开发过程中,遇到大段的代码需要临时注释起来,过一会儿又取消注释,可以把这一段要注释的代码用一对花括号括起来,定义成一个函数,没有地方调用这个函数,这块代码就不会执行,达到了和注释一样的效果。

多行注释还可以使用以下格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
:<<EOF
注释内容...
注释内容...
注释内容...
EOF

:<<'
注释内容...
注释内容...
注释内容...
'

:<<!
注释内容...
注释内容...
注释内容...
!

传递参数

可以在执行 Shell 脚本时,向脚本传递参数,脚本内获取参数的格式为:$n。n 代表一个数字,1 为执行脚本的第一个参数,2 为执行脚本的第二个参数,以此类推……,$0 为执行的文件名:

1
2
3
4
5
echo "Shell 传递参数实例!";
echo "执行的文件名:$0";
echo "第一个参数为:$1";
echo "第二个参数为:$2";
echo "第三个参数为:$3";
参数处理 说明
$# 传递到脚本的参数个数
$* 以一个单字符串显示所有向脚本传递的参数。如”$*“用「”」括起来的情况、以”$1 $2 … $n“的形式输出所有参数。
$@ $*相同,但是使用时加引号,并在引号中返回每个参数。如”$@“用「”」括起来的情况、以"$1" "$2" … "$n" 的形式输出所有参数。
$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$- 显示Shell使用的当前选项,与set命令功能相同。
$? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。
1
2
3
4
5
6
7
8
9
10
echo "Shell 传递参数实例!";
echo "第一个参数为:$1";

echo "参数个数为:$#";
echo "传递的参数作为一个字符串显示:$*";

Shell 传递参数实例!
第一个参数为:1
参数个数为:3
传递的参数作为一个字符串显示:1 2 3

运算符

算术运算符

原生bash不支持简单的数学运算,但是可以通过其他命令来实现,例如 awk 和 expr,expr 最常用。expr 是一款表达式计算工具,使用它能完成表达式的求值操作。

1
2
val=`expr 2 + 2`
echo "两数之和为 : $val"

注意:表达式和运算符之间要有空格,例如 2+2 是不对的,必须写成 2 + 2.

关系运算符

关系运算符只支持数字,不支持字符串,除非字符串的值是数字。

运算符 说明 举例
-eq 检测两个数是否相等,相等返回 true。 [ $a -eq $b ] 返回 false。
-ne 检测两个数是否不相等,不相等返回 true。 [ $a -ne $b ] 返回 true。
-gt 检测左边的数是否大于右边的,如果是,则返回 true。 [ $a -gt $b ] 返回 false。
-lt 检测左边的数是否小于右边的,如果是,则返回 true。 [ $a -lt $b ] 返回 true。
-ge 检测左边的数是否大于等于右边的,如果是,则返回 true。 [ $a -ge $b ] 返回 false。
-le 检测左边的数是否小于等于右边的,如果是,则返回 true。 [ $a -le $b ] 返回 true。

布尔运算符

运算符 说明 举例
! 非运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。
-o 或运算,有一个表达式为 true 则返回 true。 [ $a -lt 20 -o $b -gt 100 ] 返回 true。
-a 与运算,两个表达式都为 true 才返回 true。 [ $a -lt 20 -a $b -gt 100 ] 返回 false。

逻辑运算符

运算符 说明 举例
&& 逻辑的 AND [[ $a -lt 100 && $b -gt 100 ]] 返回 false
|| 逻辑的 OR [[ $a -lt 100 || $b -gt 100 ]] 返回 true

字符串运算符

运算符 说明 举例
= 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。
!= 检测两个字符串是否相等,不相等返回 true。 [ $a != $b ] 返回 true。
-z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。
-n 检测字符串长度是否为0,不为0返回 true。 [ -n “$a” ] 返回 true。
str 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。

文件测试运算符

操作符 说明 举例
-r file 检测文件是否可读,如果是,则返回 true。 [ -r $file ] 返回 true。
-w file 检测文件是否可写,如果是,则返回 true。 [ -w $file ] 返回 true。
-x file 检测文件是否可执行,如果是,则返回 true。 [ -x $file ] 返回 true。
-s file 检测文件是否为空(文件大小是否大于0),不为空返回 true。 [ -s $file ] 返回 true。
-e file 检测文件(包括目录)是否存在,如果是,则返回 true。 [ -e $file ] 返回 true。
-d file 检测文件是否是目录,如果是,则返回 true。 [ -d $file ] 返回 false。
-f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 [ -f $file ] 返回 true。
-b file 检测文件是否是块设备文件,如果是,则返回 true。 [ -b $file ] 返回 false。
-c file 检测文件是否是字符设备文件,如果是,则返回 true。 [ -c $file ] 返回 false。
-g file 检测文件是否设置了 SGID 位,如果是,则返回 true。 [ -g $file ] 返回 false。
-k file 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 [ -k $file ] 返回 false。
-p file 检测文件是否是有名管道,如果是,则返回 true。 [ -p $file ] 返回 false。
-u file 检测文件是否设置了 SUID 位,如果是,则返回 true。 [ -u $file ] 返回 false。

echo, printf和read

echo

显示普通字符串:

1
echo "It is a test"

这里的双引号完全可以省略,以下命令与上面实例效果一致:

显示转义字符

1
2
echo "\"It is a test\""
"It is a test"

同样,双引号也可以省略

显示变量

read 命令从标准输入中读取一行,并把输入行的每个字段的值指定给 shell 变量

1
2
read name 
echo "$name It is a test"

显示换行

1
2
echo -e "OK! \n" # -e 开启转义
echo "It it a test"

显示不换行

1
2
echo -e "OK! \c" # -e 开启转义 \c 不换行
echo "It is a test"

显示结果定向至文件

1
echo "It is a test" > myfile

不转义或取变量

1
echo '$name\"'

显示命令执行结果

1
2
3
echo `date`

->Thu Jul 24 10:08:46 CST 2014

注意: 这里使用的是反引号 `, 而不是单引号 ‘。

printf

用法

printf format-string [arguments…]

参数说明:

format-string: 为格式控制字符串
arguments: 为参数列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg  
printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234
printf "%-10s %-8s %-4.2f\n" 杨过 男 48.6543
printf "%-10s %-8s %-4.2f\n" 郭芙 女 47.9876

#执行脚本,输出结果如下所示:
姓名 性别 体重kg
郭靖 男 66.12
杨过 男 48.65
郭芙 女 47.99

# 没有引号也可以输出
printf %s abcdef

# 格式只指定了一个参数,但多出的参数仍然会按照该格式输出,format-string 被重用
printf %s abc def

# 如果没有 arguments,那么 %s 用NULL代替,%d 用 0 代替
printf "%s and %d \n"

%-10s 指一个宽度为10个字符(-表示左对齐,没有则表示右对齐),任何字符都会被显示在10个字符宽的字符内,如果不足则自动以空格填充,超过也会将内容全部显示出来。

%-4.2f 指格式化为小数,其中.2指保留2位小数。

read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
echo -n "Enter you name:"   #echo -n 让用户直接在后面输入 
read name #输入的多个文本将保存在一个变量中
echo "Hello $name, welcome to my program."

read -p "Please enter your age: " age #在read命令行指定提示符
days=$[ $age * 365 ]
echo "That makes you over $days days old!"

read -p "Enter your name:" first last #指定多个变量
echo "Checking data for $last, $first"

if read -t 5 -p "Please enter your name: " name #超时, 等待输入的秒数
then
echo "Hello $name, welcome to my script"
else
echo
echo "Sorry, too slow!"
fi

read -s -p "Enter your passwd: " pass #-s 参数使得read读入的字符隐藏
echo
echo "Is your passwd readlly $pass?"

流程控制

if else

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
if condition
then
command1
command2
...
commandN
else
command
fi

if condition; then
command1
command2
...
commandN
else
command
fi

if condition1
then
command1
elif condition2
then
command2
else
commandN
fi

for

1
2
3
4
5
6
7
8
9
for var in item1 item2 ... itemN
do
command1
command2
...
commandN
done

for var in item1 item2 ... itemN; do command1; command2… done;

in列表可以包含替换、字符串和文件名。

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
for loop in 1 2 3 4 5
do
echo "The value is: $loop"
done
# 输出结果:

The value is: 1
The value is: 2
The value is: 3
The value is: 4
The value is: 5

for str in 'This is a string'
do
echo $str
done
# 输出结果:

This is a string

for i in `seq 1 4`
do
echo "The value is: $i"
done
# 输出结果:
The value is: 1
The value is: 2
The value is: 3
The value is: 4

for (( i = 1; i < 5; i++ )); do
echo "The value is: $i"
done
# 输出结果:
The value is: 1
The value is: 2
The value is: 3
The value is: 4

while

1
2
3
4
while condition
do
command
done
1
2
3
4
5
6
int=1
while(( $int<=5 ))
do
echo $int
let "int++"
done

使用中使用了 Bash let 命令,它用于执行一个或多个表达式,变量计算中不需要加上 $ 来表示变量,具体可查阅:Bash let 命令。

while循环可用于读取键盘信息。下面的例子中,输入信息被设置为变量FILM,按结束循环。

1
2
3
4
5
6
7
8
9
10
11
12
echo '按下 <CTRL-D> 退出'
echo -n '输入你最喜欢的网站名: '
while read FILM
do
echo "是的!$FILM 是一个好网站"
done

# 运行脚本,输出类似下面:

按下 <CTRL-D> 退出
输入你最喜欢的网站名:菜鸟教程
是的!菜鸟教程 是一个好网站

无限循环

1
2
3
4
5
6
7
8
9
10
11
12
13
while :
do
command
done
# 或者

while true
do
command
done
# 或者

for (( ; ; ))

until

until 循环执行一系列命令直至条件为 true 时停止。until 循环与 while 循环在处理方式上刚好相反。一般 while 循环优于 until 循环,但在某些时候—也只是极少数情况下,until 循环更加有用。

1
2
3
4
5
6
7
8
9
10
11
until condition
do
command
done

a=0
until [ ! $a -lt 10 ]
do
echo $a
a=`expr $a + 1`
done

case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case 值 in
模式1)
command1
command2
...
commandN
;;
模式2)
command1
command2
...
commandN
;;
esac

case工作方式如上所示。取值后面必须为单词in,每一模式必须以右括号结束。取值可以为变量或常数。匹配发现取值符合某一模式后,其间所有命令开始执行直至 ;;。

取值将检测匹配的每一个模式。一旦模式匹配,则执行完匹配模式相应命令后不再继续其他模式。如果无一匹配模式,使用星号 * 捕获该值,再执行后面的命令。

下面的脚本提示输入1到4,与每一种模式进行匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
echo '输入 1 到 4 之间的数字:'
echo '你输入的数字为:'
read aNum
case $aNum in
1) echo '你选择了 1'
;;
1) echo '你选择了 2'
;;
1) echo '你选择了 3'
;;
1) echo '你选择了 4'
;;
*) echo '你没有输入 1 到 4 之间的数字'
;;
esac

跳出循环

break

1
2
3
4
5
6
7
8
9
10
11
12
while :
do
echo -n "输入 1 到 5 之间的数字:"
read aNum
case $aNum in
1|2|3|4|5) echo "你输入的数字为 $aNum!"
;;
*) echo "你输入的数字不是 1 到 5 之间的! 游戏结束"
break
;;
esac
done

continue

1
2
3
4
5
6
7
8
9
10
11
12
13
while :
do
echo -n "输入 1 到 5 之间的数字: "
read aNum
case $aNum in
1|2|3|4|5) echo "你输入的数字为 $aNum!"
;;
*) echo "你输入的数字不是 1 到 5 之间的!"
continue
echo "游戏结束"
;;
esac
done

函数

函数定义

1
2
3
4
[ function ] funname [()] {
action;
[return int;]
}
  1. 可以带function fun() 定义,也可以直接fun() 定义,不带任何参数。
  2. 参数返回,可以显示加:return 返回,如果不加,将以最后一条命令运行结果,作为返回值。 return后跟数值n(0-255
1
2
3
4
demoFun(){
echo "这是我的第一个 shell 函数!"
}
demoFun

注意:所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至shell解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可。

函数参数

在Shell中,调用函数时可以向其传递参数。在函数体内部,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数…

1
2
3
4
5
6
7
8
9
10
funWithParam(){
echo "第一个参数为 $1 !"
echo "第二个参数为 $2 !"
echo "第十个参数为 $10 !"
echo "第十个参数为 ${10} !"
echo "第十一个参数为 ${11} !"
echo "参数总数有 $# 个!"
echo "作为一个字符串输出所有参数 $* !"
}
funWithParam 1 2 3 4 5 6 7 8 9 34 73

注意,$10 不能获取第十个参数,获取第十个参数需要${10}。当n>=10时,需要使用${n}来获取参数。另外,还有几个特殊字符用来处理参数:

参数处理 说明
$# 传递到脚本的参数个数
$* 以一个单字符串显示所有向脚本传递的参数
$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$@ 与$*相同,但是使用时加引号,并在引号中返回每个参数。
$- 显示Shell使用的当前选项,与set命令功能相同。
$? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

输入/输出重定向

概述

大多数 UNIX 系统命令从终端接受输入并将所产生的输出发送回​​到终端。一个命令通常从一个叫标准输入的地方读取输入,默认情况下是终端。同样,一个命令通常将其输出写入到标准输出,默认情况下也是终端。

重定向命令列表如下:

命令 说明
command > file 将输出重定向到 file。
command < file 将输入重定向到 file。
command >> file 将输出以追加的方式重定向到 file。
n > file 将文件描述符为 n 的文件重定向到 file。
n >> file 将文件描述符为 n 的文件以追加的方式重定向到 file。
n>&m 把输出到文件符n的信息重定向到文件描述符m。
n<&m 将输入文件 m 和 n 合并。
<< tag 将开始标记 tag 和结束标记 tag 之间的内容作为输入。
cmd >&n 把输出送到文件描述符n
cmd m>&n 把输出 到文件符m的信息重定向到文件描述符n
cmd >&- 关闭标准输出
cmd <&n 输入来自文件描述符n
cmd m<&n m来自文件描述符n
cmd <&- 关闭标准输入
cmd <&n- 移动输入文件描述符n而非复制它。(需要解释)
cmd >&n- 移动输出文件描述符 n而非复制它。(需要解释)
cmd 2>file 把文件描述符2重定向到file,即把错误输出存到file中。
cmd > file 2>&1 把标准错误重定向到标准输出,再重定向到file,即stderr和stdout都被输出到file中
cmd &> file 功能与上一个相同,更为简便的写法。
cmd >& file 功能仍与上一个相同。
cmd > f1 2>f2 把stdout重定向到f1,而把stderr重定向到f2
tee files 把stdout原样输出的同时,复制一份到files中。
tee files 把stderr和stdout都输出到files中,同时输出到屏幕。

>&实际上复制了文件描述符,这使得ls > dirlist 2>&1与ls 2>&1 > dirlist的效果不一样。man bash的Redirection节中提及了这段内容。

需要注意的是文件描述符 0 通常是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。

输出重定向

1
2
3
4
5
6
7
$ echo "菜鸟教程:www.runoob.com" > users
$ cat users

$ echo "菜鸟教程:www.runoob.com" >> users
$ cat users
菜鸟教程:www.runoob.com
菜鸟教程:www.runoob.com

输入重定向

和输出重定向一样,Unix 命令也可以从文件获取输入,这样,本来需要从键盘获取输入的命令会转移到文件读取内容。

1
2
3
4
5
6
command1 < file1

$ cat > test.txt liujiadong

$ cat test.txt
liujiadong

重定向深入讲解

一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件:

  • 标准输入文件(stdin):stdin的文件描述符为0,Unix程序默认从stdin读取数据。
  • 标准输出文件(stdout):stdout 的文件描述符为1,Unix程序默认向stdout输出数据。
  • 标准错误文件(stderr):stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。

默认情况下,command > file 将 stdout 重定向到 file,command < file 将stdin 重定向到 file。

  • 如果希望 stderr 重定向到 file,可以这样写:
1
2
$ command 2 > file
$ command 2 >> file
  • 如果希望将 stdout 和 stderr 合并后重定向到 file,可以这样写:
1
2
$ command > file 2>&1
$ command >> file 2>&1
  • 如果希望对 stdin 和 stdout 都重定向,可以这样写:
1
$ command < file1 >file2

Here Document

Here Document 是 Shell 中的一种特殊的重定向方式,用来将输入重定向到一个交互式 Shell 脚本或程序。

1
2
3
command << delimiter
document
delimiter

它的作用是将两个 delimiter 之间的内容(document) 作为输入传递给 command。

  • 结尾的delimiter 一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和 tab 缩进。
  • 开始的delimiter前后的空格会被忽略掉。
1
2
3
4
5
6
7
8
9
10
11
cat << EOF
欢迎来到
菜鸟教程
www.runoob.com
EOF

执行以上脚本,输出结果:
欢迎来到
菜鸟教程
www.runoob.com
/dev/null 文件
  • 如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null, /dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到”禁止输出”的效果。

  • 如果希望屏蔽 stdout 和 stderr,可以这样写:

1
$ command > /dev/null 2>&1

文件包含

和其他语言一样,Shell 也可以包含外部脚本。这样可以很方便的封装一些公用的代码作为一个独立的文件。Shell 文件包含的语法格式如下:

1
2
. filename   # 注意点号(.)和文件名中间有一空格
source filename
  • test1.sh 代码如下:
1
url="http://www.runoob.com"
  • test2.sh 代码如下:
1
2
3
4
5
6
7
#使用 . 号来引用test1.sh 文件
. ./test1.sh

# 或者使用以下包含文件代码
# source ./test1.sh

echo "菜鸟教程官网地址:$url"

注:被包含的文件 test1.sh 不需要可执行权限。

OSI七层模型

由于OSI(Open System Interconnect)是一个理想的模型,因此一般网络系统只涉及其中的几层,很少有系统能够具有所有的7层,并完全遵循它的规定。

在7层模型中,每一层都提供一个特殊的网络功能。从网络功能的角度观察:下面4层(物理层、数据链路层、网络层和传输层)主要提供数据传输和交换功能,即以节点到节点之间的通信为主;第4层作为上下两部分的桥梁,是整个网络体系结构中最关键的部分;而上3层(会话层、表示层和应用层)则以提供用户与应用程序之间的信息和数据处理功能为主。

简言之,下4层主要完成通信子网的功能,上3层主要完成资源子网的功能.OSI下3层的主要任务是数据通信,上3层的任务是数据处理。

Socket:是应用层与传输层之间的桥梁.

协议

OSI七层网络模型 TCP/IP四层概念模型 对应网络协议
应用层(Application) 应用层 HTTP、TFTP, FTP, NFS, WAIS、SMTP
表示层(Presentation) 应用层 Telnet, Rlogin, SNMP, Gopher
会话层(Session) 应用层 SMTP, DNS
传输层(Transport) 传输层 TCP, UDP
网络层(Network) 网络层 IP, ICMP, ARP, RARP, AKP, UUCP
数据链路层(Data Link) 数据链路层 FDDI, Ethernet, Arpanet, PDN, SLIP, PPP
物理层(Physical) 数据链路层 IEEE 802.1A, IEEE 802.2到IEEE 802.11

物理层

  • 利用传输介质为数据链路层提供物理连接,实现比特流的传输(比特Bit).

  • 物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的

  • 把二进制转换成电流,把电流转换成二进制(单位是bit比特)

数据链路层

  • 将比特组装成帧和点到点的传递(帧Frame)
  • 通过各种控制协议,将有差错的物理信道变为无差错的、能可靠传输数据帧的数据链路.
  • 在计算机网络中由于各种干扰的存在,物理链路是不可靠的。因此,这一层的主要功能是在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路,即提供可靠的通过物理介质传输数据的方法
  • 数据链路层的具体工作是:接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层
  • 将二进制数据转换成标准帧格式(起始位、数据、地址、校验、结束位)
  • 与数据链路层有关的设备:交换机,也就是大家常说的猫(为数据帧从一个端口到另一个任意端口的转发提供了低时延、低开销的通路)
  • 如果把电脑比如成客户,数据链路比喻成物流,那么快递小哥通过电脑MAC地址(MAC地址由网卡决定)找到客户地址

网络层

  • 数据链路层的数据在这一层被转换为数据包(package).

  • 其主要任务是:通过路由选择算法,为报文或分组通过通信子网选择最适当的路径。该层控制数据链路层与传输层之间的信息转发,建立、维持和终止网络的连接。具体地说,数据链路层的数据在这一层被转换为数据包,然后通过路径选择、分段组合、顺序、进/出路由等控制,将信息从一个网络设备传送到另一个网络设备

  • 寻址:数据链路层中使用的物理地址(如MAC地址)仅解决网络内部的寻址问题。在不同子网之间通信时,为了识别和找到网络中的设备,每一子网中的设备都会被分配一个唯一的地址。由于各子网使用的物理技术可能不同,因此这个地址应当是逻辑地址(如IP地址)。

  • 交换:规定不同的信息交换方式。常见的交换技术有:线路交换技术和存储转发技术,后者又包括报文交换技术和分组交换技术。

  • 路由算法:当源节点和目的节点之间存在多条路径时,本层可以根据路由算法,通过网络为数据分组选择最佳路径,并将信息从最合适的路径由发送端传送到接收端。

  • 连接服务:与数据链路层流量控制不同的是,前者控制的是网络相邻节点间的流量,后者控制的是从源节点到目的节点间的流量。其目的在于防止阻塞,并进行差错检测

  • 网络层有关的设备:路由器(一个作用是连通不同的网络,另一个作用是选择信息传送的线路)

  • 网络层主要有两个作用

    选择数据传输的最优路径,解决网络阻塞问题(网络阻塞的原因主要是CPU需要处理数据有一定延迟)

    将大的数据切割成小的数据包,根据不同时间段的不同最优路径进行传输(可以联想看片时候的断点续传)

  • 互联网通过ip地址识别电脑.

传输层

  • 传输层是通信子网和资源子网的接口和桥梁,起到承上启下的作用
  • 该层的主要任务是:定义了一些传输数据的协议和端口号(如HTTP的端口80等),TCP,UDP。 主要是从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做报文段(Segment)
  • 电脑通过通过端口号识别某一个应用程序,每一个应用程序都有很多的服务,每一个服务对应着一个端口号

会话层

  • 建立、管理和终止会话(会话协议数据单元SPDU)
  • 会话层是用户应用程序和网络之间的接口,主要任务是:向两个实体的表示层提供建立和使用连接的方法。将不同实体之间的表示层的连接称为会话。因此会话层的任务就是组织和协调两个会话进程之间的通信,并对数据交换进行管理
  • 通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)
  • 数据的传输是在会话层完成的,而不是传输层,传输层只是定义了数据传输的协议

表示层

  • 对数据进行翻译、加密和压缩(表示协议数据单元PPDU)
  • 表示层对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。其主要功能是“处理用户信息的表示问题,如编码、数据格式转换和加密解密”等
  • 可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换
  • 表示层的任务:数据格式转换(可以理解成iOS中将c语言的char字符转换成OC语言的NSString)

应用层

  • 允许访问OSI环境的手段(应用协议数据单元APDU)
  • 应用层是计算机用户,以及各种应用程序和网络之间的接口,其功能是直接向用户提供服务,完成用户希望在网络上完成的各种工作
  • 是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务

单工,半双工,全双工

  • 单工:单工就是指A只能发信号,而B只能接收信号,通信是单向的。
  • 半双工:指一个时间段内只有一个动作发生,举个简单例子,一天窄窄的马路,同时只能有一辆车通过,当目前有两量车对开,这种情况下就只能一辆先过,等到头儿后另一辆再开,这个例子就形象的说明了半双工的原理。早期的对讲机、以及早期集线器等设备都是实行半双工的产品。随着技术的不断进步,半双工会逐渐退出历史舞台。
  • 全双工:指交换机在发送数据的同时也能够接收数据,两者同步进行,这好像我们平时打电话一样,说话的同时也能够听到对方的声音。目前的交换机都支持全双工。

IP地址

IP地址

IP地址:<网络号><主机号>.

  • A类地址:以0开头,第一个字节范围:0~127(1.0.0.0 - 126.255.255.255);
  • B类地址:以10开头,第一个字节范围:128~191(128.0.0.0 - 191.255.255.255);
  • C类地址:以110开头,第一个字节范围:192~223(192.0.0.0 - 223.255.255.255);

私有(保留)地址

  • A类:10.0.0.0——10.255.255.255
  • B类:172.16.0.0——172.31.255.255
  • C类:192.168.0.0——192.168.255.255

子网划分

三级ip地址:<网络号><子网号><主机号>

TCP&UDP(传输层)

TCP三次握手与四次挥手

主要因为这是一次全双工的,双方都需要证明自己有发送和接收的能力。握手只需要三次是因为服务端的SYN和ACK可以一次发送给客户端。

为什么TCP客户端最后还要发一次确认呢?

防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

  1. 客户端进程发出连接释放报文,并且停止发送数据。
  2. 服务器收到连接释放报文,发出确认报文,服务端就进入了CLOSE-WAIT(关闭等待)状态。
  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文。
  5. 客户端收到服务器的连接释放报文后,必须发出确认。此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗ *∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。

为什么客户端最后还要等待2MSL?

  1. 保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
  2. 防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

为什么建立连接是三次握手,关闭连接确是四次挥手呢?

  1. 当主机甲想要释放连接时会发FIN给主机B。
  2. 主机乙收到甲发送的FIN,表示收到了,就会发送ACK回复。
  3. 但这是乙可能还在发送数据,没有想要关闭数据口的意思,所以FIN与ACK不是同时发送的,而是等到乙数据发送完了,才会发送FIN给主机A.
  4. A收到B发来的FIN,知道B的数据也发送完了,回复ACK,A等待2MSL以后,没有收到B传来的任何消息,知道B已经收到自己的ACK了,A就关闭链接,B也关闭链接了。

TCP和UDP

UDP

  • 面向无连接
  • 有单播,多播,广播的功能:支持一对一,一对多,多对多,多对一的方式。
  • 面向报文:发送方对应用程序的报文添加首部后就向下交付IP层,既不合并,也不拆分,因此应用程序必须选择合适大小的报文。
  • 不可靠性:不需建立连接,不关心接收端是否接收到信息。适合实时性要求高的场景(比如电话会议)。
  • 头部开销小,传输数据报文时是很高效的。

TCP

  • 面向连接:三次握手四次挥手。
  • 仅支持单播传输:点对点。
  • 面向字节流
  • 可靠传输:给每个包一个序列号,接收端按序接收,然后发送确认ACK,发送端决定是否重传。
  • 拥塞控制

TCP流量控制

所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。利用滑动窗口实现流量控制。

发送窗口在连接建立时由双方商定。但在通信的过程中,接收端可根据自己的资源情况,随时动态地调整对方的发送窗口上限值(可增大或减小)。

TCP拥塞控制

所谓拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。

慢开始和拥塞避免

  1. 在主机刚刚开始发送报文段时可先将拥塞窗口 cwnd 设置为一个最大报文段 MSS 的数值。
  2. 在每收到一个对新的报文段的确认后,将拥塞窗口增加至多一个 MSS 的数值。
  3. 用这样的方法逐步增大发送端的拥塞窗口 cwnd,可以使分组注入到网络的速率更加合理。

快重传和快恢复

  • 快重传:发送端只要一连收到三个重复的 ACK 即可断定有分组丢失了,就应立即重传丢失的报文段而不必继续等待为该报文段设置的重传计时器的超时
  • 快恢复:
    1. 当发送端收到连续三个重复的 ACK 时,就重新设置慢开始门限 ssthresh。
    2. 与慢开始不同之处是 swnd 不是设置为 1,而是设置为 ssthresh + 3 * MSS。
    3. 若收到的重复的 ACK 为 n 个(n > 3),则将 cwnd 设置为 ssthresh + n * MSS。
    4. 若发送窗口值还容许发送报文段,就按拥塞避免算法继续发送报文段。
    5. 若收到了确认新的报文段的 ACK,就将 swnd 缩小到 ssthresh。

http&https

Http码

  • 1xx (临时响应)请求正在处理,可能需要请求者执行某些操作。
  • 2xx (成功)表示成功处理了请求的状态代码。
  • 3xx (重定向) 表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。
  • 4xx (客户端错误) 这些状态代码表示客户端请求可能出错,服务器无法处理。
  • 5xx (服务器错误)这些状态代码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错。

http和https的区别与联系

HTTP协议传输的数据都是明文,因此使用HTTP协议传输隐私信息非常不安全。为了保证这些隐私数据能加密传输,于是通过 SSL 协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。

HTTPS和HTTP的区别主要如下:

  1. https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
  2. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  3. http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
  4. http是无状态(每个请求都是完全独立的,每个请求包含了处理这个请求所需的完整的数据,发送请求不涉及到状态变更,也不依赖之前的状态)的;HTTPS协议是由SSL+HTTP协议构建的,http是无状态的,ssl/tls是有状态的。

https

Http1.0, 1.1, 2.0

  • HTTP 1.1支持长连接,使用长连接的情况下,连接成功后客户端和服务器之间用于的TCP连接不会关闭,一段时间内如果客户端再次访问这个服务器,会继续使用这一条已经建立的连接(Connection:keep-alive)。HTTP 1.0规定浏览器与服务器只保持短暂的连接,每次请求都需要与服务器建立TCP连接,完成处理后立即断开TCP连接。
  • HTTP 1.0认为每台服务器都绑定一个唯一的IP地址,虚拟主机发展后一台物理服务器上可以存在多个虚拟主机,它们共享一个IP地址,因此添加了主机host参数。
  • HTTP 1.1在1.0的基础上加入了一些cache的新特性。HTTP 1.0使用Expires头标识缓存的有效期,其值是一个绝对时间,依赖客户端的本地时间。从HTTP 1.1 开始使用Cache-Control头表示缓存状态。
  • 新增了24个状态响应码,如 410 表示服务器上的某个资源被永久性的删除。
  • http 1.1在请求头引入了range头,支持断点续传。

HTTP 2.0:

  • HTTP 1.x的解析基于文本,HTTP 2.0在应用层和传输层之间增加一个二进制分帧层,它把原来 HTTP 1.x的header和body用二进制重新封装了一层。
  • HTTP 2.0多路复用基于二进制分帧,在同一域名下所有访问都从同一个tcp连接中走,http消息被分解为独立的帧,乱序发送,服务端根据帧id重新组装起来。
  • 头部压缩:HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0要求客户端和服务器同时维护和更新一个包含之前见过的头字段的索引列表(cache),之后传输的头信息会基于此表编码和解码。
  • 服务器推送:服务器可以额外的向客户端推送资源,而无需客户端明确的请求

HTTP是不保存状态的协议,如何保存用户状态?

HTTP 协议自身不对请求和响应之间的通信状态进行保存,使用Session可以服务端记录用户的状态。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个Session)。

  • Cookie: 浏览器的一种数据存储方式。
  • Session: 会话。服务器使用Session把用户的信息临时保存在服务器上,用户离开网站后Session会被销毁。客户端得到这个Session Id后可以存储在Cookie里。这里有个缺陷,如果Web服务器有许多台,那么下次请求如果请求到了另一个服务器上的时候就验证不了Session了。
  • Token: 也用来做身份验证,但是服务器不需要存储Token信息,服务器会在用户登录成功后返回一个签名后的Token回来,下次请求带上,服务器再解密验证即可。

浏览器访问http网址

  1. 客户端浏览器输入URL后,检查本地是否有DNS缓存,缓存的查找顺序为:浏览器缓存–>系统缓存–>路由器缓存,缓存中没有则查找系统的hosts文件中是否有记录,如果没有则查询DNS服务器。
  2. 应用层客户端浏览器根据ip及相应的端口号,构造一个http请求,包括请求头和请求体,封装在TCP包里。
  3. 然后传输层TCP通过三次握手建立连接(SYN–>ACK/SYN–>ACK),TCP进行分割数据包,输入到网络层。
  4. 网络层ip协议通过查找路由表决定通过哪个路径到达服务器,把数据包发送给服务器。
  5. 数据到达数据链路层,将数据发送给指定mac地址的服务器。

交换机、路由器、网关

交换机

交换机工作于数据链路层。交换机内部的CPU会在每个端口成功连接时,通过ARP协议学习它的MAC地址,保存成一张ARP表。在今后的通讯中,发往该MAC地址的数据包将仅送往其对应的端口,而不是所有的端口。因此,交换机可用于划分数据链路层广播,即冲突域;但它不能划分网络层广播,即广播域。

交换机被广泛应用于二层网络交换,俗称“二层交换机”。

交换机的种类有:二层交换机、三层交换机、四层交换机、七层交换机分别工作在OSI七层模型中的第二层、第三层、第四层和第七层,并因此而得名。

路由器

路由器(Router)是一种计算机网络设备,提供了路由与转送两种重要机制,可以决定数据包从来源端到目的端所经过的路由路径(host到host之间的传输路径),这个过程称为由;将路由器输入端的数据包移送至适当的路由器输出端(在路由器内部进行),这称为转 送。路由工作在OSI模型的第三层——即网络层,例如网际协议。

路由器的一个作用是连通不同的网络,另一个作用是选择信息传送的线路。

路由器与交换器的差别,路由器是属于OSI第三层(网络层)的产品,交换器是OSI第二层的产品(这里特指二层交换机)。

网关

网关(Gateway),网关顾名思义就是连接两个网络的设备,区别于路由器(由于历史的原因,许多有关TCP/IP 的文献曾经把网络层使用的路由器(Router)称为网关,在今天很多局域网采用都是路由来接入网络,因此现在通常指的网关就是路由器的IP),经常在家 庭中或者小型企业网络中使用,用于连接局域网和Internet。 网关也经常指把一种协议转成另一种协议的设备,比如语音网关。

在现代网络术语中,网关(gateway)与路由器(router)的定义不同。网关(gateway)能在不同协议间移动数据,而路由器(router)是在不同网络间移动数据,相当于传统所说的IP网关(IP gateway)。

网关是连接两个网络的设备,对于语音网关来说,他可以连接PSTN网络和以太网,这就相当于VOIP,把不同电话中的模拟信号通过网关而转换成数字信号,而且加入协议再去传输。在到了接收端的时候再通过网关还原成模拟的电话信号,最后才能在电话机上听到。

对于以太网中的网关只能转发三层以上数据包,这一点和路由是一样的。而不同的是网关中并没有路由表,他只能按照预先设定的不同网段来进行转发。网关最重要的一点就是端口映射,子网内用户在外网看来只是外网的IP地址对应着不同的端口,这样看来就会保护子网内的用户。

网桥

简单的说网桥就是个硬件网络协议翻译器,假设你有2台电脑,一台兼容机安装windows,一台是Apple安装OS2,那么两台电脑之间是默认网络协议是不同的,兼容机可能只会说TCP/IP,苹果机只会说Apple talk,就好象两个外国人都不会说对方的语言,怎么办?找个翻译,网桥就是翻译。

在386、486时代网桥可能是一台安装了协议转换程序的电脑,如今交换机也包含这个功能。今天的操作系统之间为了互相交流,支持更多的协议,操作系统自己就可以是网桥,现在网桥这个概念已经淡出了。更多是所谓的桥接、转发、协议二次封装。

网桥也可以说相当一个端口少的二层交换机,再者网桥主要由软件实现,交换机主要由硬件实现!

网络接口卡(网卡)

  1. 进行串行/并行转换。
  2. 对数据进行缓存。
  3. 在计算机的操作系统安装设备驱动程序。
  4. 实现以太网协议。

路由表

路由表是用来决定如何将一个数据包从一个子网传送到另一个子网的,换句话说就是用来决定从一个网卡接收到的包应该送到哪一个网卡上去。

路由表的每一行至少有目标网络号、子网掩码、到这个子网应该使用的网卡这三条信息。当路由器从一个网卡接收到一个包时,它扫描路由表的每一行,用里面的子网掩码与数据包中的目标IP地址做逻辑与运算(&)找出目标网络号。如果得出的结果网络号与这一行的网络号相同,就将这条路由表六下来作为备用路由。如果已经有备用路由了,就载这两条路由里将网络号最长的留下来,另一条丢掉(这是用无分类编址CIDR的情况才是匹配网络号最长的,其他的情况是找到第一条匹配的行时就可以进行转发了)。如此接着扫描下一行直到结束。如果扫描结束仍没有找到任何路由,就用默认路由。确定路由后,直接将数据包送到对应的网卡上去。在具体的实现中,路由表可能包含更多的信息为选路由算法的细节所用。

ssh

概述

SSH的英文全称为Secure Shell,是IETF(Internet Engineering Task Force)的Network Working Group所制定的一族协议,其目的是要在非安全网络上提供安全的远程登录和其他安全网络服务。用于在主机之间建立起安全连接, 并加密传输内容, 以达到安全的远程访问, 操作以及数据传输的目的。它只是一种协议,存在多种实现,既有商业实现,也有开源实现。比较常用的是OpenSSH,它是自由软件,应用非常广泛。

SSH协议目前有SSH1和SSH2两个主流版本,SSH2协议兼容SSH1,强烈建议使用SSH2版本。目前实现SSH1和SSH2协议的主要软件有OpenSSH 和SSH Communications Security Corporation 公司的SSH Communications 软件。前者是OpenBSD组织开发的一款免费的SSH软件,后者是商业软件,因此在linux、FreeBSD、OpenBSD 、NetBSD等免费类UNIX系统种,通常都使用OpenSSH作为SSH协议的实现软件。

SSH主要有两个特点: 1. 安全性 2. 传输速度快。与FTP、POP 和 Telnet 等传统网络服务使用明文传输数据、命令和口令不同,SSH可以对所有传输的数据进行加密,能够防止 DNS 欺骗和 IP 欺骗。

如果一个用户从本地计算机,使用SSH协议登录另一台远程计算机,我们就可以认为,这种登录是安全的,即使被中途截获,密码也不会泄露。

SSH支持两种认证方式:密码认证和密钥认证。

中间人攻击

SSH之所以能够保证安全,原因在于它采用了公钥加密。

整个过程是这样的:(1)远程主机收到用户的登录请求,把自己的公钥发给用户。(2)用户使用这个公钥,将登录密码加密后,发送回来。(3)远程主机用自己的私钥,解密登录密码,如果密码正确,就同意用户登录。

这个过程本身是安全的,但是实施的时候存在一个风险:如果有人截获了登录请求,然后冒充远程主机,将伪造的公钥发给用户,那么用户很难辨别真伪。因为不像https协议,SSH协议的公钥是没有证书中心(CA)公证的,也就是说,都是自己签发的。

可以设想,如果攻击者插在用户与远程主机之间(比如在公共的wifi区域),用伪造的公钥,获取用户的登录密码。再用这个密码登录远程主机,那么SSH的安全机制就荡然无存了。这种风险就是著名的”中间人攻击”(Man-in-the-middle attack)。

密码认证

  1. 客户端向服务端发起登录请求,服务端将自己的公钥返回给客户端
  2. 客户端输入登录口令,口令经服务端公钥加密后发送到服务端
  3. 服务端接收到加密口令后使用私钥解密,如果密码正确则登录成功

第一次登录对方主机时,系统会出现下面的提示:

1
2
3
4
$ ssh user@host
The authenticity of host 'host (12.18.429.21)' can't be established.
RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d.
Are you sure you want to continue connecting (yes/no)?

这段话的意思是,无法确认host主机的真实性,只知道它的公钥指纹,问你还想继续连接吗?

所谓”公钥指纹”,是指公钥长度较长(这里采用RSA算法,长达1024位),很难比对,所以对其进行MD5计算,将它变成一个128位的指纹。上例中是98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d,再进行比较,就容易多了。

远程主机必须在自己的网站上贴出公钥指纹,以便用户自行核对。

当远程主机的公钥被接受以后,它就会被保存在文件$HOME/.ssh/known_hosts之中。下次再连接这台主机,系统就会认出它的公钥已经保存在本地了,从而跳过警告部分,直接提示输入密码。

每个SSH用户都有自己的known_hosts文件,此外系统也有一个这样的文件,通常是/etc/ssh/ssh_known_hosts,保存一些对所有用户都可信赖的远程主机的公钥。

密钥认证

  1. 客户端发起密钥连接请求,并上传身份信息
  2. 服务端收到请求后,在可信列表中查询客户端,若无此客户端则断开连接,否则发送一串随机问询码,该问询码使用此客户端公钥加密处理
  3. 客户端收到加密问询码后,使用私钥解密出问询码再用通信session对问询码加密并传送给服务端
  4. 服务端解密问询码并判定客户端身份安全与否,安全则建立连接

登录的时候,远程主机会向用户发送一段随机字符串,用户用自己的私钥加密后,再发回来。远程主机用事先储存的公钥进行解密,如果成功,就证明用户是可信的,直接允许登录shell,不再要求密码。

因此,密钥认证首要要将客户端的公钥放置在服务端的授权登录列表中。密钥认证一般不需要密码,但客户端可在生成密钥时指定密钥加密密码,这样在与服务端建立连接时需要输入加密密码来解密私钥,防止因私钥泄露带来的安全问题。

1
2
3
4
5
# 生成密钥对
$ ssh-keygen

# 将公钥传送到远程主机host
$ ssh-copy-id user@host

以后再登录,就不需要输入密码了。

如果还是不行,就打开远程主机的/etc/ssh/sshd_config这个文件,检查下面几行前面”#”注释是否取掉。

1
2
3
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

然后,重启远程主机的ssh服务。

1
2
3
4
5
# ubuntu系统
$ service ssh restart

# debian系统
$ /etc/init.d/ssh restart

authorized_keys文件

远程主机将用户的公钥,保存在登录后的用户主目录的$HOME/.ssh/authorized_keys文件中。公钥就是一段字符串,只要把它追加在authorized_keys文件的末尾就行了。

这里不使用上面的ssh-copy-id命令,改用下面的命令,解释公钥的保存过程:

1
$ ssh user@host 'mkdir -p .ssh && cat >> .ssh/authorized_keys' < ~/.ssh/id_rsa.pub
  1. $ ssh user@host表示登录远程主机;
  2. mkdir .ssh && cat >> .ssh/authorized_keys表示登录后在远程shell上执行的命令:
  3. $ mkdir -p .ssh的作用是,如果用户主目录中的.ssh目录不存在,就创建一个;
  4. cat >> .ssh/authorized_keys' < ~/.ssh/id_rsa.pub的作用是,将本地的公钥文件~/.ssh/id_rsa.pub,重定向追加到远程文件authorized_keys的末尾。

写入authorized_keys文件后,公钥登录的设置就完成了。

SSL、TLS、SSH&HTTPS

SSL(Secure Socket Layer)

  • SSL是传输层之上,对Socket连接的加密协议。
  • SSL多用于Internet上,在浏览器和服务器之间的安全传输。
  • OpenSSL是SSL/TLS的开源实现。

TLS(Transport Layer Security)

  • TLS也是传输层之上的加密协议。
  • TLS可用于任何两个应用程序之间的安全传输。
  • OpenSSL是SSL/TLS的开源实现。

SSH(Secure Shell)

  • SSH是应用层的通信加密协议,往往用于远程登录的会话。
  • OpenSSH是SSH的开源实现。

HTTPS

  • 在传输层使用SSL/TLS加密的HTTP
  • 与HTTP类似,HTTPS本身是应用层的协议。
  • 同一台Web服务器,往往同时支持HTTP和HTTPS,这是分别通过80端口和443端口实现的。

安装

  1. 安装gcc g++的依赖库
1
2
apt-get install build-essential
apt-get install libtool
  1. 安装pcre依赖库
1
2
sudo apt-get update
sudo apt-get install libpcre3 libpcre3-dev
  1. 安装zlib依赖库
1
apt-get install zlib1g-dev
  1. 安装ssl依赖库
1
apt-get install openssl
  1. 安装nginx
1
2
3
4
5
6
7
8
9
10
11
12
./configure --prefix=/usr/local/nginx
make
# 注意:这里可能会报错,提示“pcre.h No such file or directory”,具体详见:http://stackoverflow.com/questions/22555561/error-building-fatal-error-pcre-h-no-such-file-or-directory
# 需要安装 libpcre3-dev,命令为:sudo apt-get install libpcre3-dev
sudo make install
#启动nginx:
sudo /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
# 注意:-c 指定配置文件的路径,不加的话,nginx会自动加载默认路径的配置文件,可以通过 -h查看帮助命令。
#查看nginx进程:
ps -ef|grep nginx
#重启
sudo ./sbin/nginx -s reload
  1. 配置

在conf目录下新建一个ihasy.conf文件

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
upstream ihasy  {
server 127.0.0.1:9001; #Tornado
}

## Start www.ihasy.com ##
server {
listen 80;
server_name www.ihasy.com ihasy.com;

#root html;
#index index.html index.htm index.py index;

## send request back to Tornado ##
location / {
proxy_pass http://ihasy;

#Proxy Settings
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_max_temp_file_size 0;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}
}
## End www.ihasy.com ##

在nginx.conf中添加include ihasy.conf,保存,重启nginx,即可实现反向代理。

nginx.conf中默认配置

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
user  root;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;

# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;

# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;

# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;

# location / {
# root html;
# index index.html index.htm;
# }
#}

}

概述

正向代理和反向代理

正向代理:是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理。

正向代理的用途:

  • 访问原来无法访问的资源,如google
  • 可以做缓存,加速访问资源
  • 对客户端访问授权,上网进行认证
  • 代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息

反向代理(Reverse Proxy)实际运行方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。

反向代理的作用:

  • 保证内网的安全,可以使用反向代理提供WAF功能,阻止web攻击
  • 负载均衡,通过反向代理服务器来优化网站的负载

事件驱动

Nginx之所以能同时处理大量的请求,原因在于它采用了十分巧妙的事件驱动机制。

作为一个Web服务器,要同时处理多个请求,不可避免地要面对这么一个问题,如何同时处理像磁盘和网络等等的I/O请求,即如何实现I/O复用。为了解决该问题,操作系统在很久之前就开始提供诸如“select”、“poll”等系统调用。Apache HTTP的多处理模块(MPM,multi-processing module)就会用到这些系统调用。但是,select/poll为了识别出哪些文件或者socket已经准备就绪,必须将所有已注册的文件描述符(fd)一个个地检查一遍。如果注册列表越长,那么每次的扫描所需的时间也越长。而Nginx的I/O复用机制使用的是“epoll”这个基于事件驱动的系统调用。因为epoll会在系统内核管理和监听这些文件描述符(fd),并自动把就绪的加入到Ready队列当中。所以,程序只需在需要时往Ready队列中取出一个进行处理即可,而不用切换到内核态,然后一个个地检查,然后又切换回用户态。这样,无论需要注册监听的I/O有多少,都不会影响程序的运行效率。为了避免select/poll带来的线性增长的负担,Apache HTTP必须将这些IO分散到各个进程/线程中处理,这样势必会造成占用内存的增长。但是,Nginx可以通过利用“epoll”,保证可以使用一个进程/线程完成所有请求的处理,这样可以大大减少内存的占用,从而使应对上万并发请求成为可能。

配置

location

语法规则: location [=||*|^~] /uri/ { … }

  • = 开头表示精确匹配
  • ^~ 开头表示uri以某个常规字符串开头,理解为匹配 url路径即可。nginx不对url做编码,因此请求为/static/20%/aa,可以被规则^~ /static/ /aa匹配到(注意是空格)。一旦匹配成功,那么 Nginx 就停止去寻找其他的 Location 块进行匹配了(其他可能没有这个特性)
  • ~ 开头表示区分大小写的正则匹配
  • ~* 开头表示不区分大小写的正则匹配
  • !和!*分别为区分大小写不匹配及不区分大小写不匹配 的正则
  • / 通用匹配,任何请求都会匹配到。
  • 多个location配置的情况下匹配顺序为:首先匹配 =,其次匹配^~, 其次是按文件中顺序的正则匹配,最后是交给 / 通用匹配。当有匹配成功时候,停止匹配,按当前匹配规则处理请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
location = / {  
#规则A
}
location = /login {
#规则B
}
location ^~ /static/ {
#规则C
}
location ~ \.(gif|jpg|png|js|css)$ {
#规则D
}
location ~* \.png$ {
#规则E
}
location !~ \.xhtml$ {
#规则F
}
location !~* \.xhtml$ {
#规则G
}
location / {
#规则H
}

那么产生的效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#直接匹配网站根,通过域名访问网站首页比较频繁,使用这个会加速处理,官网如是说。  
#这里是直接转发给后端应用服务器了,也可以是一个静态首页
# 第一个必选规则
location = / {
proxy_pass http://tomcat:8080/index
}

# 第二个必选规则是处理静态文件请求,这是nginx作为http服务器的强项
# 有两种配置模式,目录匹配或后缀匹配,任选其一或搭配使用
location ^~ /static/ {
root /webroot/static/;
}
location ~* \.(gif|jpg|jpeg|png|css|js|ico)$ {
root /webroot/res/;
}

#第三个规则就是通用规则,用来转发动态请求到后端应用服务器
#非静态文件请求就默认是动态请求,自己根据实际把握
#毕竟目前的一些框架的流行,带.php,.jsp后缀的情况很少了
location / {
proxy_pass http://tomcat:8080/
}

handler

filter

upstream

基本配置

负载均衡配置:

1
2
3
4
5
6
7
8
9
10
upstream linuxidc {
server 10.0.6.108:7080;
server 10.0.0.85:8980;
}

location / {
root html;
index index.html index.htm;
proxy_pass http://linuxidc;
}

weight(权重)

1
2
3
4
upstream linuxidc {
server 10.0.0.77 weight=5;
server 10.0.0.88 weight=10;
}

ip_hash(访问ip)

每一个请求按訪问ip的hash结果分配。这样每一个訪客固定訪问一个后端服务器,能够解决session的问题。

1
2
3
4
5
upstream favresin {
ip_hash;
server 10.0.0.10:8080;
server 10.0.0.11:8080;
}

url_hash(第三方)

按访问url的hash结果来分配请求,使每一个url定向到同一个后端服务器。后端服务器为缓存时比較有效。注意:在upstream中加入hash语句。server语句中不能写入weight等其他的參数,hash_method是使用的hash算法。

1
2
3
4
5
6
upstream resinserver {
server 10.0.0.10:7777;
server 10.0.0.11:8888;
hash $request_uri;
hash_method crc32;
}

fair(第三方)

按后端服务器的响应时间来分配请求。响应时间短的优先分配,与weight分配策略相似。

1
2
3
4
5
upstream favresin {
server 10.0.0.10:8080;
server 10.0.0.11:8080;
fair;
}

其他

upstream还能够为每一个设备设置状态值,这些状态值的含义分别例如以下:

  • down 表示单前的server临时不參与负载.
  • weight 默觉得1.weight越大,负载的权重就越大。
  • max_fails :同意请求失败的次数默觉得1.当超过最大次数时,返回proxy_next_upstream 模块定义的错误.
  • fail_timeout : max_fails次失败后。暂停的时间。
  • backup: 其他全部的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
1
2
3
4
5
6
7
upstream bakend{ #定义负载均衡设备的Ip及设备状态 
ip_hash;
server 10.0.0.11:9090 down;
server 10.0.0.11:8080 weight=2;
server 10.0.0.11:6060;
server 10.0.0.11:7070 backup;
}

虚拟主机配置

概述

通常情况下,为了使每个服务器可以供更多用户使用,可以将一个服务器分为很多虚拟的子服务器,每个子服务器是相互独立的.这些服务器是根据虚拟化技术分出来的,这样,一台服务器可以虚拟成很多台子服务器,叫做虚拟主机.nginx下,一个server标签就是一个虚拟主机。

配置方法:

  1. 基于域名的虚拟主机,通过域名来区分虚拟主机——应用:外部网站
  2. 基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站,外部网站的管理后台
  3. 基于ip的虚拟主机,几乎不用。

基于域名

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name www.yong.com;
index index.html;
root /data/www;
}
server {
listen 80;
server_name bbs.yong.com;
index index.html;
root /data/bbs;
}

基于端口

1
2
3
4
5
6
7
8
9
10
server {
listen 8000;
server_name www.yong.com;
root /data/www;
}
server {
listen 8001;
server_name www.yong.com;
root /data/bbs;
}

基于ip地址

1
2
3
4
5
6
7
8
9
10
server {
listen 192.168.20.20:80;
server_name www.yong.com;
root /data/www;
}
server {
listen 192.168.20.21:80;
server_name www.yong.com;
root /data/bbs;
}

正向代理

nginx实现代理上网,有三个关键点必须注意,其余的配置跟普通的nginx一样\

  1. 增加dns解析resolver
  2. 增加无server_name名的server
  3. proxy_pass指令
  4. 电脑配置配置文件中的地址和端口为代理地址和端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
user www;
worker_processes 1;
error_log /var/log/nginx/error.log debug;

events {
use epoll;
worker_connections 1024;
}

http {
resolver 8.8.8.8;
server {
listen 8088;
location / {
proxy_pass http://$http_host$request_uri;
}
}
}

反向代理

反向代理,外部机器通过网关访问网关后面服务器上的内容,网关起到了反向代理的功能,我们平时通过浏览器访问远程的web服务器大都是这样实现的。

nginx反向代理的指令不需要新增额外的模块,默认自带proxy_pass指令,只需要修改配置文件就可以实现反向代理。配置前的准备工作:后端run apache服务的ip和端口,确保可以通过http://ip:port能访问到自己的网站.

添加配置文件:

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
upstream apachephp  {
server ip:8080; #Apache
}

server {
listen 80;
server_name www.quancha.cn;

access_log logs/quancha.access.log main;
error_log logs/quancha.error.log;
root html;
index index.html index.htm index.php;

## send request back to apache ##
location / {
proxy_pass http://apachephp;

#Proxy Settings
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_max_temp_file_size 0;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}
}

部署SpringBoot

Nginx配置:

1
2
3
4
5
6
location / {
proxy_pass http://localhost:8080;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
}

SpringBoot的配置文件中配置:

1
2
3
4
5
6
7
server:
tomcat:
remote-ip-header: x_forwarded_for
protocol-header: x-forwarded-proto
port-header: X-Forwarded-Port
use-forward-headers: true
address: 127.0.0.1 #8080端口只能被本机访问

其他

映射本地文件夹

配置如下:

1
2
3
4
location /hearing {
autoindex on;
root /home/hearing/Downloads/;
}

通过nginx -s reload重新载入配置,可以在localhost:80/hearing/访问到/home/hearing/Downloads/hearing/下的文件。

*文/苍耳*

走在昏黄的路灯下,一步一步地,走过去了,也未曾留下什么脚印。树影斑驳的小路上依旧残留着些许枯叶,无论什么季节,它们始终静静地躺在那里,等待着变腐烂,守候着诗人低吟。死如秋叶,也无人问津。

回到宿舍忽然很想重温火影,他完结的那天总有种怅然若失的感觉,那个傻乎乎的鸣人,总是有一种让人情不自禁地想要去亲近他的气质。相伴十余年,尚记得曾在小摊上找到一张有好几百集的盗版CD的时候,兴奋地可以看上一整天,如痴如醉,如梦难醒。

当然自然没看成,明天还要上课吧。躺在床上想听会歌,却循环到了久石让。刹那间便有种难以言明的伤感,所有曾迷恋过宫崎骏的动画和久石让的音乐的人大概都能理解这种感觉吧。当音乐响起,不禁有种恍若隔世的感觉,童年,少年,以及所谓的青春,都被衔接成一段段的旋律,每个音符,都藏着某些不为人知的往事。那曾是所有青春年少的日子里,与我朝夕相伴的挚友,在流金岁月里,如一杯清水,一曲平静。

如今渐渐少有写东西的时间,很长一段时间里都迷上了小说,大概是觉得自己的生活里缺少了许多的东西吧,说白了是贪心,看过了这么多的故事,我如一个傻子般沉浸在别人的幸福里,却从未想过,我自己的生活,究竟是什么。披着一层伪装,然后戴上一套面具,推开门,都是一天。

天涯海角的邂逅,陌生人之间亦可相谈甚欢,曾以为相逢即是缘,而今想来,世上哪会有如此多的缘分,许多,大抵都是相逢一笑,擦肩路人而已。

或许是从小孤独惯了,所以对许多东西都太过在意,结果自是相反的,在人生这条路上,都被我越拉越远。

成长的代价,便是越来越少的时间去做喜欢的事情,而且必须毫无怨言,因为活着已不仅是为了自己。如今也只有在夜深人静难以入眠的时候,或者是独自一人找个地方独饮的时候,久违地矫情一把,当一个破落诗人,举杯饮酒,附庸风雅,好好地犒劳一下自己心中潜藏已久的小情绪,清一下往事,倒一碗矫情,一饮而尽。

想起一个朋友,和两杯酒,许是人声鼎沸的小店,或者静若可闻的深夜,一碟小菜,久饮不散。杯中那倒影,荡漾着,击碎了流光。

一曲循环,也终有尽时。爱深了这些音乐,半随忧伤,半入流年。这一路走来,虽然不长,却也都有故事。一直不擅长说话,也讲不清故事,即便相识,也总是重复着相逢又陌路的遗憾,随着汹涌的浪潮,消失在时间海里。

但你的故事,我都听着。

一直都喜欢这句话:”不困于心,不乱于情。不畏将来,不念过往。”

如此,安好。

内容来源于网上

同步/异步,阻塞/非阻塞

概念

同步/异步主要针对C端.

  • 同步

    所谓同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。同步,就是我客户端(c端调用者)调用一个功能,该功能没有结束前,我(c端调用者)死等结果。

  • 异步

    异步的概念和同步相对。当c端一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。就是我(c端调用者)调用一个功能,不需要知道该功能结果,该功能有结果后通知我(c端调用者)即回调通知。

阻塞/非阻塞主要针对S端.

  • 阻塞

    阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。就是调用我(s端被调用者,函数),我(s端被调用者,函数)没有接收完数据或者没有得到结果之前,我不会返回。

  • 非阻塞

    非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。就是调用我(s端被调用者,函数),我(s端被调用者,函数)立即返回,通过select通知调用者.

同步异步是作用在程序外部的,线程/进程是一直激活是否完成后才返回;阻塞和非阻塞是作用在IO网络函数内部的,外部的线程/进程进入睡眠或马上返回尽管得不到结果.

Node.js中的描述:

线程在执行中如果遇到磁盘读写或网络通信(统称为I/O 操作),通常要耗费较长的时间,这时操作系统会剥夺这个线程的CPU控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为阻塞。当I/O 操作完毕时,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种I/O 模式就是通常的同步式I/O(Synchronous I/O)或阻塞式I/O (Blocking I/O)。

相应地,异步式I/O (Asynchronous I/O)或非阻塞式I/O (Non-blocking I/O)则针对所有I/O 操作不采用阻塞的策略。当线程遇到I/O 操作时,不会以阻塞的方式等待I/O 操作的完成或数据的返回,而只是将I/O 请求发送给操作系统,继续执行下一条语句。当操作系统完成I/O 操作时,以事件的形式通知执行I/O 操作的线程,线程会在特定时候处理这个事件。为了处理异步I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理。

阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程所使用的CPU 核心利用率永远是100%,I/O 以事件的方式通知。

在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞时还有其他线程在工作,多线程可以让CPU 资源不被阻塞中的线程浪费。而在非阻塞模式下,线程不会被I/O 阻塞,永远在利用CPU。多线程带来的好处仅仅是在多核CPU 的情况下利用更多的核,而Node.js的单线程也能带来同样的好处。这就是为什么Node.js 使用了单线程、非阻塞的事件编程模式。

I/O模型

  1. 阻塞I/O(blocking I/O)

  2. 非阻塞I/O (nonblocking I/O)

  3. I/O复用(select 和poll) (I/O multiplexing)

  4. 信号驱动I/O (signal driven I/O (SIGIO))

  5. 异步I/O (asynchronous I/O (the POSIX aio_functions))

    前四种都是同步,只有最后一种才是异步IO。

阻塞I/O模型

简介:进程会一直阻塞,直到数据拷贝完成

应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。如果数据没有准备好,一直等待.数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。

当使用socket()函数和WSASocket()函数创建套接字时,默认的套接字都是阻塞的。这意味着当调用Windows Sockets API不能立即完成时,线程处于等待状态,直到操作完成。

将可能阻塞套接字的Windows Sockets API调用分为以下四种:

  1. 输入操作: recv()、recvfrom()、WSARecv()和WSARecvfrom()函数。以阻塞套接字为参数调用该函数接收数据。如果此时套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。
  2. 输出操作: send()、sendto()、WSASend()和WSASendto()函数。以阻塞套接字为参数调用该函数发送数据。如果套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。
  3. 接受连接:accept()和WSAAcept()函数。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。
  4. 外出连接:connect()和WSAConnect()函数。对于TCP连接,客户端以阻塞套接字为参数,调用该函数向服务器发起连接。该函数在收到服务器的应答前,不会返回。这意味着TCP连接总会等待至少到服务器的一次往返时间。

使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,使用阻塞模式来开发网络程序比较合适。

应对多客户机的网络应用,最简单的解决方式是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。

具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以,如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的 CPU 资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。通常,使用 pthread_create () 创建新线程,fork() 创建新进程。

多线程/进程服务器同时为多个客户机提供应答服务。模型如下:

主线程持续等待客户端的连接请求,如果有连接,则创建新线程,并在新线程中提供为前例同样的问答服务。

如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。

由此可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如apache,mysql数据库等。

但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

非阻塞IO模型

简介:非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;

我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。

把SOCKET设置为非阻塞模式,即通知系统内核:在调用Windows Sockets API时,不要让线程睡眠,而应该让函数立即返回。在返回时,该函数返回一个错误代码。图所示,一个非阻塞模式套接字多次调用recv()函数的过程。前三次调用recv()函数时,内核数据还没有准备好。因此,该函数立即返回WSAEWOULDBLOCK错误代码。第四次调用recv()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。

IO复用模型

简介:主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听;

I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

信号驱动IO

简介:两次调用,两次返回;

首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

异步IO模型

简介:数据拷贝的时候进程无需阻塞。

当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作

5种IO模型的比较

select、poll、epoll简介

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现

select

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

  1. 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

  2. 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

    当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

  3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

  1. 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
  2. poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll

epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知

epoll的优点:

  1. 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
  2. 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
  3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

总结

  1. 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
  2. select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善.

CAP

  • Consistency
  • Availability
  • Partition tolerance

CAP定理: CAP三个指标不可能同时做到.

Partition tolerance

Partition tolerance 中文叫做”分区容错”。大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。

G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。

Consistency

Consistency 中文叫做”一致性”。意思是,写操作之后的读操作,必须返回该值。

举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。接下来,用户的读操作就会得到 v1。这就叫一致性。问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。这样的话,用户向 G2 发起读操作,也能得到 v1。

Availability

Availability 中文叫做”可用性”,意思是只要收到用户的请求,服务器就必须给出回应。

用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性。

C和A的矛盾

一致性和可用性因为可能通信失败(即出现分区容错)所以会产生矛盾。

  • 如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性。
  • 如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。

综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。

高性能并发

概述

  • 业务数据库:数据水平分割(分区分表分库)、读写分离
  • 业务应用:逻辑代码优化(算法优化)、公共数据缓存
  • 应用服务器:反向静态代理、配置优化、负载均衡(apache分发,多tomcat实例),集群,分布式,微服务.
  • 系统环境:JVM调优
  • 页面优化:减少页面连接数、页面尺寸瘦身

通用措施:

  1. 动态资源和静态资源分离;
  2. CDN;
  3. 负载均衡;
  4. 分布式缓存;
  5. 数据库读写分离或数据切分(垂直或水平);
  6. 服务分布式部署。

动静分离

动静分离是将网站静态资源(HTML,JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用访问。

动静分离的一种做法是将静态资源部署在nginx上,后台项目部署到应用服务器上,根据一定规则静态资源的请求全部请求nginx服务器,达到动静分离的目标。

跨域

浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域.

域名:

端口:

协议:

备注:

  • 端口和协议的不同,只能通过后台来解决
  • localhost和127.0.0.1虽然都指向本机,但也属于跨域

JVM内存结构

JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;

Java虚拟机管理的内存包括几个运行时数据内存:方法区、堆、虚拟机栈、本地方法栈、程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区。

没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。

程序计数器

程序计数器是一块较小的内存,他可以看做是当前线程所执行的行号指示器。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemotyError情况的区域。

  • 线程私有的内存

    由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,在任何一个确定的时间,一个处理器(对多核处理器来说是一个内核)只会执行一条线程中的指令。因此为了为了线程切换能够恢复到正确的执行位置上,每条线程都有一个独立的线程计数器,各条线程之间计数器互不影响,独立存储,我们叫这类内存区域线程私有的内存

Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

栈内存就是虚拟机栈,或者说是虚拟机栈中局部变量表的部分。

局部变量表存放了编辑期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(refrence)类型和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用两个局部变量空间,其余的数据类型只占用1个。

Java虚拟机规范对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机扩展时无法申请到足够的内存,就会跑出OutOfMemoryError异常。

本地方法栈

本地方法栈和虚拟机栈发挥的作用是非常类似的,他们的区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务

本地方法栈区域也会抛出StackOverflowError和OutOfMemoryErroy异常

Java堆

堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存(基本数据类型除外)。所有的对象实例和数组都在堆上分配。

Java堆是垃圾收集器管理的主要区域。Java堆细分为新生代和老年代

不管怎样,划分的目的都是为了更好的回收内存,或者更快地分配内存

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有完成实例分配,并且堆也无法在扩展时将会抛出OutOfMemoryError异常。

Native堆的回收不收 java gc 的影响,一般需要手工进行回收。如果大量的使用非Java堆,则丢失了 Java 自动垃圾回收的特点。

方法区

当程序运行时,首先通过类装载器加载字节码文件,经过解析后装入方法区,方法区它用于储存已被虚拟机加载的类信息、用final修饰的常量、用static修饰的静态变量、String对象(常量池)和方法等数据。对于同一个方法的调用,同一个类的不同实例调用的都是存在方法区的同一个方法。类变量的生命周期从程序开始运行时创建,到程序终止运行时结束!

除了Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。它有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

当方法区无法满足内存分配需求时,将抛出OutOfMemoryErroy异常。

运行时常量池:

它是方法区的一部分。Class文件中除了有关的版本、字段、方法、接口等描述信息外、还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放.

Java语言并不要求常量一定只有编辑期才能产生,也就是可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法.

当常量池无法再申请到内存时会抛出OutOfMemoryError异常.

实例

  1. 当程序运行时,首先通过类装载器加载字节码文件,经过解析后装入方法区!在方法区中存了类的各种信息,包括类静态变量、常量及方法。对于同一个方法的调用,同一个类的不同实例调用的都是存在方法区的同一个方法。类变量的生命周期从程序开始运行时创建,到程序终止运行时结束!
  2. 当程序中new一个对象时,这个对象存在堆中,对象的变量存在栈中,指向堆中的引用!对象的成员变量都存在堆中,当对象被回收时,对象的成员变量随之消失!
  3. 当方法调用时,JVM会在栈中分配一个栈桢,存储方法的局部变量。当方法调用结束时,局部变量消失!
  • 类变量:属于类的属性信息,与类的实例无关,多个实例共用同一个类变量,存在与方法区中。类变量用static修饰,包括静态变量和常量。静态变量有默认初始值,常量必须声明同时初始化。
  • 成员变量:属于实例的变量,只与实例有关,写在类下面,方法外,非static修饰。成员变量会随着成员的创建而生存,随着成员的回收而销毁。
  • 局部变量:声明在方法中,没有默认初始值,随着方法的调用而创建,存储于栈中,随着方法调用的结束而销毁。
1
2
3
4
5
6
7
8
public class Main{
int a = 1; // a 和1 都在堆里
Student s = new Student();// s 和new d的Student()都在 堆里
public void XXX(){
int b = 1;//b 和 1 栈里面
Student s2 = new Student();// s2 在栈里, new的 Student() 在堆里
}
}

hotspot虚拟机对象

HotSpot VM是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。

对象的创建

1.检查

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程

2.分配内存

接下来将为新生对象分配内存,为对象分配内存空间的任务等同于把一块确定的大小的内存从Java堆中划分出来。

假设Java堆中内存是绝对规整的,所有用过的内存放在一遍,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离,这个分配方式叫做“指针碰撞”

如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

在分配内存的时候会出现并发的问题,比如在给A对象分配内存的时候,指针还没有来得及修改,对象B又同时使用了原来的指针进行了内存的分片。

有两个解决方案:

  • 对分配的内存进行同步处理:CAS配上失败重试的方式保证更新操作的原子性
  • 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中分配一块小内存,称为本地缓冲区,那个线程需要分配内存,就需要在本地缓冲区上进行,只有当缓冲区用完并分配新的缓冲区的时候,才需要同步锁定,

3.Init

执行new指令之后会接着执行Init方法,进行初始化,这样一个对象才算产生出来

对象的内存布局

在HotSpot虚拟机中,对象在内存中储存的布局可以分为3块区域:对象头、实例数据和对齐填充

对象头包括两部分:

1.markword

第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。

2.klass

对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.

3.数组长度(只有数组对象有)

如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.

实例数据:

是对象正常储存的有效信息,也是程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。

对齐填充:

不是必然存在的,仅仅是起到占位符的作用。对象的大小必须是8字节的整数倍,而对象头刚好是8字节的整数倍(1倍或者2倍),当实例数据没有对齐的时候,就需要通过对齐填充来补全

对象的访问定位

  • 使用句柄访问

    Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址

    优势:reference中存储的是稳点的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改

  • 使用直接指针访问

    Java堆对象的布局就必须考虑如何访问类型数据的相关信息,而refreence中存储的直接就是对象的地址

    优势:速度更快,节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本

垃圾回收

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了,而方法区和堆是线程共享的,不会随线程死亡而消失

栈中的栈帧随着方法的进入和退出就有条不紊的执行者出栈和入栈的操作,每一个栈分配多少个内存基本都是在类结构确定下来的时候就已经确定了,这几个区域内存分配和回收都具有确定性

而堆和方法区则不同,一个接口的实现是多种多样的,多个实现类需要的内存可能不一样,一个方法中多个分支需要的内存也不一样,我们只能在程序运行的期间知道需要创建那些对象,分配多少内存,这部分的内存分配和回收都是动态的。

判断对象存活

  1. 引用计数器法

    给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

    这样的代码会产生如下引用情形:objA指向objB,而objB又指向objA,这样当其他所有的引用都消失了之后,objA和objB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。

  2. 可达性分析算法

    通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象是不可用的

  • Java语言中GC Roots的对象包括下面几种:

    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
    2. 方法区中类静态属性引用的对象
    3. 方法区中常量引用的对象
    4. 本地方法栈JNI(Native方法)引用的对象

GC时机和finalize方法

下列情况会出发对象的回收:

  1. 对象没有引用
  2. 作用域发生未捕获异常
  3. 程序在作用域正常执行完毕
  4. 程序执行了System.exit()
  5. 程序发生意外终止(被杀进程等)

不可达的对象并不会马上就会被直接回收,而是至少要经过两次标记的过程。

  • 第一次标记:当可达性分析确认该对象没有引用链与GC Roots相连,则对其进行第一次标记和筛选,筛选的条件是重写了finalize()方法并没有执行过,对于重写了且并没有执行finalize()方法的对象这将其放置在一个F-Queue队列中,并在稍后由一个由虚拟机自动建立的低优先级的Finalizer线程去执行它。此处执行只保证执行该方法,但是不保证等待该方法执行结束,之所以这样子设计是为了系统的稳定性和健壮性考虑,以免该方法执行时间较长或者死循环导致系统崩溃。
  • 第二次标记:在此之后,系统会对对象进行第二次标记,如果在第一次标记之后的对象在执行finalize()方法时没有被引用到一个新的变量,这该对象将被回收掉。finalize方法只能被执行一次,并且一般不推荐也不建议重写Object的该方法,如果需要关闭外部资源,比如数据库,I/O等完全可在finally块中完成。

回收方法区

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类

  • 废弃常量:

    假如一个字符串abc已经进入了常量池中,如果当前系统没有任何一个String对象abc,也就是没有任何Stirng对象引用常量池的abc常量,也没有其他地方引用的这个字面量,这个时候发生内存回收这个常量就会被清理出常量池

  • 无用的类:

    1.该类所有的实例都已经被回收,就是Java堆中不存在该类的任何实例

    2.加载该类的ClassLoader已经被回收

    3.该类对用的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

引用

强引用

就是在程序代码之中普遍存在的,类似Object obj = new Object() 这类的引用, obj对象是对后面new Object的一个强引用。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test  
public void strongReference() {
Object referent = new Object();

/**
* 通过赋值创建 StrongReference
*/
Object strongReference = referent;

assertSame(referent, strongReference);

referent = null;
System.gc();

/**
* StrongReference 在 GC 后不会被回收
*/
assertNotNull(strongReference);
}

SoftReference

SoftReference 与 WeakReference 的特性基本一致, 最大的区别在于 SoftReference 会尽可能长的保留引用直到 JVM 内存不足时才会被回收(虚拟机保证), 这一特性使得 SoftReference 非常适合缓存应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test  
public void softReference() {
Object referent = new Object();
SoftReference<Object> softRerference = new SoftReference<Object>(referent);

assertNotNull(softRerference.get());

referent = null;
System.gc();

/**
* soft references 只有在 jvm OutOfMemory 之前才会被回收, 所以它非常适合缓存应用
*/
assertNotNull(softRerference.get());
}

WeakReference & WeakHashMap

WeakReference

用来描述非必须对象的,但是它的强度比软引用更弱一些,被引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够都会回收掉只被弱引用关联的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void weakReference() {
Object referent = new Object();
WeakReference<Object> weakRerference = new WeakReference<Object>(referent);

assertSame(referent, weakRerference.get());

referent = null;
System.gc();

/**
* 一旦没有指向 referent 的强引用, weak reference 在 GC 后会被自动回收
*/
assertNull(weakRerference.get());
}

弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器。

注意:当使用WeakReference来解决匿名内部类内存泄漏的问题时,可能会出现weakRerference.get()返回为null的问题,如下验证:

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
// 声明一个回调接口
public interface Callback {
void call();
}

// 测试类
public class WeakClass {
private WeakReference<Callback> mWeakReference;

public WeakReference<Callback> getWeakReference() {
return mWeakReference;
}

public void run() {
mWeakReference = new WeakReference<>(new Callback() {
@Override
public void callback() {
// do something
}

@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
super.finalize();
}
});
}
}

// 测试
public class Main {
public static void main(String[] args) {
WeakClass weakClass = new WeakClass();
weakClass.run();
System.gc();
System.out.println(weakClass.getWeakReference().get());
}
}

// -->output
finalize
null

这里因为run方法内部的变量会被垃圾回收,如果将它移到类成员变量级别,类成员变量级的强引用在类销毁的时候才会失效。在这之前的整个过程,由于强引用的存在,实例不会被回收,弱应用 WeakReference 也将一直有数据,故最容易的解决方案就是指定一个类成员变量强引用它。即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class WeakClass {
private WeakReference<Callback> mWeakReference;
private Callback mCallback = new Callback() {
@Override
public void callback() {
// do something
}

@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
super.finalize();
}
};

public WeakReference<Callback> getWeakReference() {
return mWeakReference;
}

public void run() {
mWeakReference = new WeakReference<>(mCallback);
}
}

WeakHashMap

WeakHashMap 使用 WeakReference 作为 key, 一旦没有指向 key 的强引用, WeakHashMap 在 GC 后将自动删除相关的 entry.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test  
public void weakHashMap() throws InterruptedException {
Map<Object, Object> weakHashMap = new WeakHashMap<Object, Object>();
Object key = new Object();
Object value = new Object();
weakHashMap.put(key, value);

assertTrue(weakHashMap.containsValue(value));

key = null;
System.gc();

/**
* 等待无效 entries 进入 ReferenceQueue 以便下一次调用 getTable 时被清理
*/
Thread.sleep(1000);

/**
* 一旦没有指向 key 的强引用, WeakHashMap 在 GC 后将自动删除相关的 entry
*/
assertFalse(weakHashMap.containsValue(value));
}

PhantomReference

Phantom Reference(幽灵引用) 与 WeakReference 和 SoftReference 有很大的不同,因为它的 get() 方法永远返回 null, 这也正是它名字的由来

1
2
3
4
5
6
7
8
9
10
@Test
public void phantomReferenceAlwaysNull() {
Object referent = new Object();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(referent, new ReferenceQueue<Object>());

/**
* phantom reference 的 get 方法永远返回 null
*/
assertNull(phantomReference.get());
}

请注意构造 PhantomReference 时的第二个参数 ReferenceQueue(事实上 WeakReference & SoftReference 也可以有这个参数),PhantomReference 唯一的用处就是跟踪 referent 何时被 enqueue 到 ReferenceQueue 中,用于检测对象是否已经从内存中删除。

Reference&ReferenceQueue

Reference 是上面引用的父类,看一下 Reference 的四个状态:

Reference状态

Reference 的构造方法:

1
2
3
4
5
6
7
8
Reference(T referent) {
this(referent, null);
}

Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = queue;
}

对于带 ReferenceQueue 的Reference,GC 会把要回收对象的 Reference 放到 ReferenceQueue 中,后续该 Reference 需要开发者自行处理(poll等)。WeakHashMap 就是利用 ReferenceQueue 来清除 key 已经没有强引用的 Entry 的。

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
@Test  
public void referenceQueue() throws InterruptedException {
Object referent = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
WeakReference<Object> weakReference = new WeakReference<Object>(referent, referenceQueue);

assertFalse(weakReference.isEnqueued());
Reference<? extends Object> polled = referenceQueue.poll();
assertNull(polled);

referent = null;
System.gc();

assertTrue(weakReference.isEnqueued());
Reference<? extends Object> removed = referenceQueue.remove();
assertNotNull(removed);
}

add 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常
remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
offer 添加一个元素并返回true 如果队列已满,则返回false
poll 移除并返问队列头部的元素 如果队列为空,则返回null
peek 返回队列头部的元素 如果队列为空,则返回null
put 添加一个元素 如果队列满,则阻塞
take 移除并返回队列头部的元素

PhantomReference vs WeakReference

PhantomReference 有两个好处:

其一, 它可以让我们准确地知道对象何时被从内存中删除, 这个特性可以被用于一些特殊的需求中(例如 Distributed GC, XWork 和 google-guice 中也使用 PhantomReference 做了一些清理性工作).

其二, 它可以避免 finalization 带来的一些根本性问题, 上文提到 PhantomReference 的唯一作用就是跟踪 referent 何时被 enqueue 到 ReferenceQueue 中, 但是 WeakReference 也有对应的功能, 两者的区别到底在哪呢 ?

这就要说到 Object 的 finalize 方法, 此方法将在 gc 执行前被调用, 如果某个对象重载了 finalize 方法并故意在方法内创建本身的强引用, 这将导致这一轮的 GC 无法回收这个对象并有可能引起任意次 GC, 最后的结果就是明明 JVM 内有很多 Garbage 却 OutOfMemory, 使用 PhantomReference 就可以避免这个问题, 因为 PhantomReference 是在 finalize 方法执行后回收的,也就意味着此时已经不可能拿到原来的引用, 也就不会出现上述问题, 当然这是一个很极端的例子, 一般不会出现.

垃圾回收的比例

我们知道,方法区主要存放类与类之间关系的数据,而这部分数据被加载到内存之后,基本上是不会发生变更的,

Java堆中的数据基本上是朝生夕死的,我们用完之后要马上回收的,而Java栈和本地方法栈中的数据,因为有后进先出的原则,当我取下面的数据之前,必须要把栈顶的元素出栈,因此回收率可认为是100%;而程序计数器我们前面也已经提到,主要用户记录线程执行的行号等一些信息,这块区域也是被认为是唯一一块不会内存溢出的区域。在SunHostSpot的虚拟机中,对于程序计数器是不回收的,而方法区的数据因为回收率非常小,而成本又比较高,一般认为是“性价比”非常差的,所以Sun自己的虚拟机HotSpot中是不回收的!但是在现在高性能分布式J2EE的系统中,我们大量用到了反射、动态代理、CGLIB、JSP和OSGI等,这些类频繁的调用自定义类加载器,都需要动态的加载和卸载了,以保证永久带不会溢出,他们通过自定义的类加载器进行了各项操作,因此在实际的应用开发中,类也是被经常加载和卸载的,方法区也是会被回收的!但是方法区的回收条件非常苛刻,只有同时满足以下三个条件才会被回收!

1、所有实例被回收

2、加载该类的ClassLoader被回收

3、Class对象无法通过任何途径访问(包括反射)

垃圾收集算法

标记—清除算法

算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记整理算法

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

复制算法

将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可.

不足之处:将内存缩小为了原来的一半

实际中我们并不需要按照1:1比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor

当另一个Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代

分代收集算法

根据对象存活周期的不同将内存划分为几块。一般把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清除或者标记整理算法来进行回收。

新生代、老年代、MinorGC、MajorGC、Full GC

新生代

1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1.

  • Eden区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
  • ServivorTo:保留了一次MinorGC过程中的幸存者。
  • ServivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者。

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。

不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

老年代

老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

MajorGC采用标记—清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。

当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

元空间(MetaSpace)

方法区和永久带:一个是标准一个是实现。

元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

  • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

内存分配与回收策略

  • 对象优先在Eden分配
  • 大对象直接进入老年代
    所谓大对象就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。这样做的目的是避免Eden区及两个Servivor之间发生大量的内存复制.
  • 长期存活的对象将进入老年代
  • 动态对象年龄判定
    为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋级到老年代,如果在Servivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入到老年代,无须等到MaxTenuringThreshold中要求的年龄
  • 空间分配担保
    在发生Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor DC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许那么会继续检查老年代最大可用的连续空间是否大于晋级到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次MinorGC 是有风险的:如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC

相关参数

  1. -XX:NewSize和-XX:MaxNewSize

    用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

  2. -XX:SurvivorRatio

    用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

  3. -XX:+PrintTenuringDistribution

    这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

  4. -XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

    用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

HotSpot的算法实现

枚举根节点

从可达性分析来说,逐个寻找分析GC Root是一个很耗时间的过程。另外,对于GC停顿,意思是说这项分析工作必须在一个能确保一致性的快照中进行:这里“一致性”的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这点是导致GC进行时必须停顿所有Java执行线程(Sun将这件事情称为“Stop The World”)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

由于目前的主流Java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。

安全点

安全点(SafePoint):即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
安全点的选定原则:是否具有让程序长时间执行的特征,例如方法调用、循环跳转、异常跳转等。

安全区域

使用Safepoint在实际上不一定能完美解决如何进去GC的问题,Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,当程序“不执行”的时候,即没有分配CPU时间的时候,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

垃圾收集器

Serial收集器

这个收集器是一个单线程的收集器,但它的单线程的意义不仅仅说明它会只使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

它是虚拟机运行在Client模式下的默认新生代收集器,它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

ParNew 收集器

Serial收集器的多线程版本,除了使用了多线程进行收集之外(并行),其余行为和Serial收集器一样。

  • 并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
  • 并发:指用户线程与垃圾收集线程同时执行(不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU上

Parallel Scavenge

收集器是一个新生代收集器,它是使用复制算法的收集器,又是并行的多线程收集器,它关注的是吞吐量。

吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。

Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

Serial Old 收集器

是Serial收集器的老年代版本,是一个单线程收集器,使用标记整理算法

Parallel Old 收集器

Parallel Old是Paraller Seavenge收集器的老年代版本,使用多线程和标记整理算法

CMS收集器

CMS收集器是基于“标记—清除”算法实现的,它的运作过程分为4个步骤:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC RootsTracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

  • 优点:并发收集、低停顿
  • 缺点:
    1. CMS收集器对CPU资源非常敏感,CMS默认启动的回收线程数是(CPU数量+3)/4,
    2. CMS收集器无法处理浮动垃圾,可能出现Failure失败而导致一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
    3. CMS是基于标记清除算法实现的

G1收集器

  1. 并行与并发:利用多CPU缩短STOP-The-World停顿的时间
  2. 分代收集
  3. 空间整合:不会产生内存碎片
  4. 可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

运作方式:初始标记,并发标记(并发),最终标记,筛选回收

常用参数配置

分类

JVM启动参数共分为三类:

  1. 标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容。例如:-verbose:class(输出jvm载入类的相关信息,当jvm报告说找不到类或者类冲突时可此进行诊断);-verbose:gc(输出每次GC的相关情况);-verbose:jni(输出native方法调用的相关情况,一般用于诊断jni调用错误信息)。
  2. 非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容。例如:-Xms512m;-Xmx512m;-Xmn200m;-Xss128k;-Xloggc:file(与-verbose:gc功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。若与verbose命令同时出现在命令行中,则以-Xloggc为准)。
  3. 非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用。例如:-XX:PermSize=64m;-XX:MaxPermSize=512m。

常用参数

  • -Xms:JVM启动时申请的初始Heap值,默认为操作系统物理内存的1/64但小于1G。默认当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过-XX:MaxHeapFreeRation=来指定这个比列。Server端JVM最好将-Xms和-Xmx设为相同值,避免每次垃圾回收完成后JVM重新分配内存;开发测试机JVM可以保留默认值。(例如:-Xms4g)
  • -Xmx:JVM可申请的最大Heap值,默认值为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列。最佳设值应该视物理内存大小及计算机内其他内存开销而定。(例如:-Xmx4g)
  • -Xmn:Java Heap Young区大小。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小(相对于HotSpot 类型的虚拟机来说)。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。(例如:-Xmn2g);程序新创建的对象都是从年轻代分配内存,年轻代由Eden Space和两块相同大小的SurvivorSpace(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定年轻代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及SurvivorSpace的大小。
  • -Xss:Java每个线程的Stack大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。(例如:-Xss1024K)
  • -XX:PermSize:持久代(方法区)的初始内存大小。(例如:-XX:PermSize=64m)
  • -XX:MaxPermSize:持久代(方法区)的最大内存大小。(例如:-XX:MaxPermSize=512m)
  • -XX:+UseSerialGC:串行(SerialGC)是jvm的默认GC方式,一般适用于小型应用和单处理器,算法比较简单,GC效率也较高,但可能会给应用带来停顿。
  • -XX:+UseParallelGC:并行(ParallelGC)是指多个线程并行执行GC,一般适用于多处理器系统中,可以提高GC的效率,但算法复杂,系统消耗较大。(配合使用:-XX:ParallelGCThreads=8,并行收集器的线程数,此值最好配置与处理器数目相等)
  • -XX:+UseParNewGC:设置年轻代为并行收集,JKD5.0以上,JVM会根据系统配置自行设置,所以无需设置此值。
  • -XX:+UseParallelOldGC:设置年老代为并行收集,JKD6.0出现的参数选项。
  • -XX:+UseConcMarkSweepGC:并发(ConcMarkSweepGC)是指GC运行时,对应用程序运行几乎没有影响(也会造成停顿,不过很小而已),GC和app两者的线程在并发执行,这样可以最大限度不影响app的运行。
  • -XX:+UseCMSCompactAtFullCollection:在Full GC的时候,对老年代进行压缩整理。因为CMS是不会移动内存的,因此非常容易产生内存碎片。因此增加这个参数就可以在FullGC后对内存进行压缩整理,消除内存碎片。当然这个操作也有一定缺点,就是会增加CPU开销与GC时间,所以可以通过-XX:CMSFullGCsBeforeCompaction=3 这个参数来控制多少次Full GC以后进行一次碎片整理。
  • -XX:+CMSInitiatingOccupancyFraction=80:代表老年代使用空间达到80%后,就进行Full GC。CMS收集器在进行垃圾收集时,和应用程序一起工作,所以,不能等到老年代几乎完全被填满了再进行收集,这样会影响并发的应用线程的空间使用,从而再次触发不必要的Full GC。
  • -XX:+MaxTenuringThreshold=10:垃圾的最大年龄,代表对象在Survivor区经过10次复制以后才进入老年代。如果设置为0,则年轻代对象不经过Survivor区,直接进入老年代。

虚拟机性能监控与故障处理工具

jps:虚拟机进程状况工具

  • JVM Process Status Tool
  • 命令格式:jps [options] [hostid]

jps可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名。jps的其他常用选项见下表。

选项 作用
-q 只输出LVMID,省略主类名称
-m 输出虚拟机进程启动时传递给主类main函数的参数
-l 输出主类的全名,如果进程执行的时jar包,输出jar包路径
-v 输出虚拟机进程启动时JVM参数

jstat:虚拟机统计信息监视工具

  • JVM Statistics Monitoring Tool
  • 用于监视虚拟机各种运行状态信息的命令行工具
  • 可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据
  • 命令格式:jstat [option vmid[interval[s|ms][count]]]

如果是本地虚拟机进程,VMID与LVMID是一致的,如果是远程虚拟机进程,那VMID的格式应当是:

1
[protocol:][//]lvmid[@hostname[:port]/servername]

参数interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次。假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:

1
jstat-gc 2764 250 20

jstat.png

jinfo:Java配置信息工具

  • Configuration Info for Java
  • 实时地查看和调整虚拟机各项参数
  • 命令格式:jinfo [option] pid
  • jps命令的-v参数可以查看虚拟机启动时显式指定的参数列表,使用jinfo的-flag选项可以查看未被显式指定的参数的系统默认值
  • 使用-sysprops选项把虚拟机进程的System.getProperties()的内容打印出来
  • 使用-flag[+|-]name或者-flag name=value修改一部分运行期可写的虚拟机参数值。

jmap:Java内存映像工具

  • Memory Map for Java
  • 用于生成堆转储快照(一般称为heapdump或dump文件)
  • 命令格式:jmap [option] vmid

jhat:虚拟机堆转储快照分析工具

  • JVM Heap Analysis Tool
  • 与jmap搭配使用,分析jmap生成的堆转储快照
  • 一般都不会去直接使用jhat命令来分析dump文件

jstack:Java堆栈跟踪工具

  • Stack Trace for Java
  • 生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)(当前虚拟机内每一条线程正在执行的方法堆栈的集合)
  • 定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因
  • 命令格式:jstack [option] vmid
  • 在JDK 1.5中,java.lang.Thread类新增了一个getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈

HSDIS:JIT生成代码反汇编

  • Sun官方推荐的HotSpot虚拟机JIT编译代码的反汇编插件,让HotSpot的-XX:+PrintAssembly指令调用它来把动态生成的本地代码还原为汇编代码输出,同时还生成了大量非常有价值的注释

JConsole:Java监视与管理控制台

  • Java Monitoring and Management Console

VisualVM:多合一故障处理工具

  • All-in-One Java Troubleshooting Tool
  • visualVM基于NetBeans平台开发,因此具有插件扩展的功能特性。
  • 生成、浏览堆转储快照
  • 分析程序性能
  • BTrace动态日志跟踪:在不停止目标程序运行的前提下,通过HotSpot虚拟机的HotSwap技术动态加入原本并不存
    在的调试代码。

类文件结构

无关性的基石

实现语言和平台无关性的基础是JVM虚拟机和字节码存储格式。

  • 平台无关性:一次编写,在任意系统上运行
  • 语言无关性:JVM不与包括Java在内的任何语言绑定,只与“Class文件”这种特定的二进制文件格式所关联

Class类文件结构

Class文件是一组以8位字节为基础单位的二进制流,当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。Class文件由无符号数和表构成:

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

class文件结构

可以使用javap -v/verbose classfile查看字节码信息。

Magic Number和文件版本

每个Class文件的头4个字节称为Magic Number,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,其值为0xCAFEBABE。紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。如下为一个Class文件的部分二进制值:

1
cafe babe 0000 0034 0044 0a00 1400 2503

常量池

紧接着主次版本号之后的是常量池入口,它是在Class文件中第一个出现的表类型数据项目。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的,如常量池容量为0x0016,即十进制的22,这就代表常量池中有21项常量,索引值范围为1~21。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

常量池内容

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类加载时解析、翻译到具体的内存地址之中。

由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。

访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。具体的标志位以及标志的含义如下表:

访问标志

类索引、父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

对于接口索引集合,入口的第一项——u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

方法表集合

方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索
引(descriptor_index)、属性表集合(attributes)几项,这些数据项目的含义也非常类似,仅在访问标志和属性表集合的可选项中有所区别。

属性表集合

属性表集合不要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

类的加载

概述

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

JVM主要在程序第一次运行时主动使用类的时候,才会立即去加载。换言之,JVM并不是在运行时就会把所有使用到的类都加载到内存中,而是用到,不得不加载的时候,才加载进来,而且只加载一次!

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。例如,如果编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类;用户可以通过Java预定义的和自定义类加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分,这种组装应用程序的方式目前已广泛应用于Java程序之中。

类加载的时机

类的生命周期

其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

类加载的具体时机可交给虚拟机的具体实现来自由把握,可以通过 ClassLoader 的 findLoadedClass 方法来判断类是否被加载(Hotspot 虚拟机):

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
public class Main {
public static void main(String[] args) throws Exception {
Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class});
m.setAccessible(true);
ClassLoader cl = ClassLoader.getSystemClassLoader();
Object test1 = m.invoke(cl, "Main$Test");
System.out.println(test1 != null);
System.out.println(Test.s); // 1
Object test2 = m.invoke(cl, "Main$Test");
System.out.println(test2 != null);
}

static class Test {
public static final String s = "hearing";
public static final Test instance = new Test();

private Test() {
System.out.println("constructor");
}

static {
System.out.println("static");
}
}
}
  • 1 处代码为 Test.s 时,输出 false hearing false,没触发类的加载。
  • 1 处代码为 Test.instance 时,输出 false constructor static Main$Test@4e25154f true,触发了类的加载(包括初始化)。
  • 1 处代码为 Test.class.getName() 时,输出 false Main$Test true,触发了类的加载(不初始化)。

类加载的过程

加载

查找并加载类的二进制数据加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表这个类的 java.lang.Class对象,作为对方法区中这些数据的访问入口。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个 java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

加载.class文件的方式:

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

连接

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

Java语言本身是相对安全的语言(依然是相对于C/C++来说),使用纯粹的Java代码无法做到诸如访问数组边界以外的数据、将一个对象转型为它并未实现的类型、跳转到不存在的代码行之类的事情,如果这样做了,编译器将拒绝编译。但前面已经说过,Class文件并不一定要求用Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生Class文件。在字节码语言层面上,上述Java代码无法做到的事情都是可以实现的,至少语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

验证阶段大致会完成4个阶段的检验动作:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  4. 符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

准备阶段是正式为类的静态变量分配内存,并将其初始化为默认值,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  2. 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值(初始化阶段)。
  3. 如果类字段的字段属性表中存在 ConstantValue 属性,即同时被final和static修饰(基本类型和String),那么在准备阶段变量value就会被初始化为ConstantValue属性所指定的值

假设上面的类变量value被定义为:public static final int value=3

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。调用这种类型的常量,不会触发所在类的初始化。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

  • 符号引用就是一组符号来描述目标,可以是任何字面量。
  • 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

初始化

前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量时指定初始值
  • 使用静态代码块为类变量指定初始值

JVM初始化步骤

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量(非ConstantValue),或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如 Class.forName(“com.shengsiyuan.Test”))
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类(main),直接使用 java.exe命令来运行某个主类

对于初始化阶段,虚拟机规范规定了在特定情况下才必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始),除此之外,所有引用类的方式都不会触发初始化,称为被动引用。如下实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 通过子类引用父类的静态字段,不会导致子类初始化
**/
public class SuperClass {
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}

public class SubClass extends SuperClass {
static {
System.out.println("SubClass init");
}
}

public static void main(String[]args) {
System.out.println(SubClass.value);
}

上述代码运行之后,只会输出“SuperClass init”,而不会输出“SubClass init”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

当通过SuperClass[] superClasses = new SuperClass[10];调用时,SuperClass依旧不会初始化,因为这段代码里面触发了另外一个名为“[Lorg.fenixsoft.classloading.SuperClass”的类的初始化阶段,对于用户代码来说,这并不是一个合法的类名称,它是一个由虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。

当访问某个类的 ConstantValue 时,不会触发初始化过程。

此外,类变量的初始化和static代码块的执行是按照代码中声明的先后顺序开始的:

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
class SingleTon {
public static int count1;
public static int count2 = 0;

static {
System.out.println("static");
}

private static SingleTon singleTon = new SingleTon();

private SingleTon() {
System.out.println("constructor");
count1++;
count2++;
}

public static SingleTon getInstance() {
return singleTon;
}
}

public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1 = " + singleTon.count1);
System.out.println("count2 = " + singleTon.count2);
}
}

上面会输出: static constructor count1 = 1 count2 = 1, 当把 singleTon 的赋值放在最前面时,会输出: constructor static count1 = 1 count2 = 0

结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

  • 执行了 System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

ConstantValue

static类型变量赋值分两种,在类构造其中赋值,或使用ConstantValue属性赋值。

在实际的程序中,只有同时被final和static修饰的字段才有ConstantValue属性,且限于基本类型和String,因为从常量池中只能引用到基本类型和String类型的字面量。编译时Javac将会为该常量生成ConstantValue属性,在类加载的准备阶段虚拟机便会根据ConstantValue为常量设置相应的值,如果该变量没有被final修饰,或者并非基本类型及字符串,则选择在类构造器中进行初始化。

final、static、static final修饰的字段赋值的区别:

  • static修饰的字段在加载过程中准备阶段被初始化,但是这个阶段只会赋值一个默认的值(0或者null而并非定义变量设置的值)初始化阶段在类构造器中才会赋值为变量定义的值。
  • final修饰的字段在运行时被初始化,可以直接赋值,也可以在实例构造器中赋值,赋值后不可修改。
  • static final修饰的字段在javac编译时生成comstantValue属性,在类加载的准备阶段直接把constantValue的值赋给该字段。可以理解为在编译期即把结果放入了常量池中。

类加载器

类与类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

双亲委派模型

这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

站在Java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;所有其它的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类 java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

  • 启动类加载器: BootstrapClassLoader,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
  • 扩展类加载器: ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器: ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

  1. 在执行非置信代码之前,自动验证数字签名。
  2. 动态地创建符合用户特定需要的定制化构建类。
  3. 从特定的场所取得java class,例如数据库中和网络中。

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

  1. 当 AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  2. 当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成。
  3. 如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载;若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException。

双亲委派优势:

  • 避免重复加载:Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  • 避免 Java 核心类篡改:当通过网络传递一个名为java.lang.Integer的类时,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

ClassLoader

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 双亲委派,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

类的加载方式

类加载有三种方式:

  1. 命令行启动应用时候由JVM初始化加载
  2. 通过Class.forName()方法动态加载
  3. 通过ClassLoader.loadClass()方法动态加载
1
2
3
4
5
6
7
8
9
10
11
12
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println( loader ); //使用ClassLoader.loadClass()来加载类,不会执行初始化块
loader.loadClass("Test2"); //使用Class.forName()来加载类,默认会执行初始化块
//Class.forName("Test2");
//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
//Class.forName("Test2", false, loader);

public class Test2 {
static {
System.out.println("静态初始化块执行了!");
}
}

分别切换加载方式,会有不同的输出结果。

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
  • Class.forName(name,initialize,loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

自定义类加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader类,从上面对 loadClass方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:

  1. 这里传递的文件名需要是类的全限定性名称,即 com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。
  2. 最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
  3. 这类Test 类本身可以被 AppClassLoader类加载,因此我们不能把 com/paddx/test/classloading/Test.class放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader加载,而不会通过我们自定义类加载器来加载.

Java内存模型与线程

重排序

在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序。重排序分成三种类型:

  1. 编译器优化的重排序。在单线程环境下不能改变程序运行的结果.
  2. 指令级并行的重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

as-if-serial

Java遵循as-if-serial语义,即单线程执行程序时,即使发生重排序,程序的执行结果不能被改变。

happens-before

happens-before的前后两个操作不会被重排序且后者对前者内存可见。

举个例子来说明一下。线程Ⅰ执行了操作A:x=3,线程Ⅱ执行了操作B:y=x。如果操作Ahappen-before操作B,线程Ⅱ执行操作B会写入y的值为3。假设线程Ⅲ在操作A和B之间执行了操作C: x=5,并且操作C和操作B之前并没有happen-before关系。这时线程Ⅱ执行操作B后x是3还是5都有可能,这是因为happen-before关系保证一定能够观测到前一个操作施加的内存影响,只有时间上的先后关系而并没有happen-before关系可能但并不保证能观测前一个操作施加的内存影响。

  • 程序次序规则:线程中每个动作A都happens-before于该线程中的每一个动作B。那么在程序中,所有的动作B都能出现在A之后。
  • 监视器锁法则:对一个监视器的解锁happens-before于每个后续对同一监视器锁的加锁
  • volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作
  • 线程启动法则:在一个线程中,对于Thread.start的调用会happens-before于每个启动线程的动作。
  • 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结。
  • 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
  • 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
  • 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C
  1. 同一个线程中,书写在前面的操作happen-before书写在后面的操作。这条规则是说,在单线程中操作间happen-before关系完全是由源代码的顺序决定的,这里的前提“在同一个线程中”是很重要的,这条规则也称为单线程规则。这个规则多少说得有些简单了,考虑到控制结构和循环结构,书写在后面的操作可能happen-before书写在前面的操作,不过我想读者应该明白我的意思。
  2. 对锁的unlock操作happen-before后续的对同一个锁的lock操作。这里的“后续”指的是时间上的先后关系,unlock操作发生在退出同步块之后,lock操作发生在进入同步块之前。这是条最关键性的规则,线程安全性主要依赖于这条规则。但是仅仅是这条规则仍然不起任何作用,它必须和下面这条规则联合起来使用才显得意义重大。这里关键条件是必须对“同一个锁”的lock和unlock。
  3. 如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。这条规则也称为传递规则。

原子性、可见性和有序性

  • 原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。Java内存模型是通过在变量修改后将新值同步会主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。
    • valatile特殊规则保障新值可以立即同步到主内存中。
    • Synchronized是在对一个变量执行unlock之前,必须把变量同步回主内存中(执行store、write操作)。
    • final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去,那在其他线程中就能看见final字段的值。
  • 可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:即程序执行的顺序按照代码的先后顺序执行。

基础

final

  • final类不能被继承,没有子类,final类中的方法默认是final的
  • final方法不能被子类的方法覆盖,但可以被继承
  • final成员变量表示常量,只能被赋值一次,赋值后不能再被改变
  • final不能用于修饰构造方法
  • private不能被子类方法覆盖,private类型的方法默认是final类型的
  • final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
  • final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。
  • final变量可在构造器里面再赋值

static

静态内部类:

  • 不能声明普通外层类或者包为静态的。
  • 只有将某个内部类修饰为静态类,然后才能够在这个类中定义静态的成员变量与成员方法。这是静态内部类都有的一个特性。
  • 不能够从静态内部类的对象中访问外部类的非静态成员(包括成员变量与成员方法)。
  • 在创建静态内部类时不需要将静态内部类的实例绑定在外部类的实例上。
  • 静态内部类可以被继承.

静态方法和属性:

  • 静态方法和属性是属于类的,调用的时候直接通过类名.方法名完成对,不需要继承机制及可以调用。如果子类里面定义了静态方法和属性,那么这时候父类的静态方法或属性称之为”隐藏”。如果你想要调用父类的静态方法和属性,直接通过父类名.方法或变量名完成,至于是否继承一说,子类是有继承静态方法和属性,但是跟实例方法和属性不太一样,存在”隐藏”的这种情况.
  • 多态之所以能够实现依赖于继承、接口和重写、重载(继承和重写最为关键)。有了继承和重写就可以实现父类的引用指向不同子类的对象。重写的功能是:”重写”后子类的优先级要高于父类的优先级,但是“隐藏”是没有这个优先级之分的。
  • 静态属性、静态方法和非静态的属性都可以被继承和隐藏而不能被重写,因此不能实现多态,不能实现父类的引用可以指向不同子类的对象。非静态方法可以被继承和重写,因此可以实现多态。

加载顺序:

  • static用来修饰成员变量和成员方法,也可以形成静态static代码块。
  • 被static修饰的成员变量和成员方法独立于该类的任何对象,只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。

static final

  • 对于变量,表示一旦给值就不可修改,并且通过类名可以访问。
  • 对于方法,表示不可覆盖,并且可以通过类名直接访问。
  • 对于被static和final修饰过的实例常量,实例本身不能再改变了,但对于一些容器类型(比如,ArrayList、HashMap)的实例变量,不可以改变容器变量本身,但可以修改容器中存放的对象。

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性。非static类型的变量的赋值是在实例构造器方法中进行的.

static类型变量赋值分两种,在类构造其中赋值,或使用ConstantValue属性赋值。

在实际的程序中,只有同时被final和static修饰的字段才有ConstantValue属性,且限于基本类型和String,因为从常量池中只能引用到基本类型和String类型的字面量。编译时Javac将会为该常量生成ConstantValue属性,在类加载的准备阶段虚拟机便会根据ConstantValue为常量设置相应的值,如果该变量没有被final修饰,或者并非基本类型及字符串,则选择在类构造器中进行初始化。

final、static、static final修饰的字段赋值的区别:

  • static修饰的字段在加载过程中准备阶段被初始化,但是这个阶段只会赋值一个默认的值(0或者null而并非定义变量设置的值)初始化阶段在类构造器中才会赋值为变量定义的值。
  • final修饰的字段在运行时被初始化,可以直接赋值,也可以在实例构造器中赋值,赋值后不可修改。
  • static final修饰的字段在javac编译时生成comstantValue属性,在类加载的准备阶段直接把constantValue的值赋给该字段。可以理解为在编译期即把结果放入了常量池中。

构造函数

  • 在子类构造对象时,发现,访问子类构造函数时,父类构造函数也运行了。原因是:在子类的构造函数中第一行有一个默认的隐式语句。 super();
  • 如果父类中没有定义空参数构造函数,那么子类的构造函数必须用super明确要调用父类中哪个构造函数。

移位运算符

左移运算符<<:

丢弃左边指定位数,右边补0,符号位可能发生变化。

  • 对于 int 类型,左移位数大于等于32位操作时,会先对 32 求余后再进行左移操作。对于 long 类型,则会对 64 求余后再进行移位操作。
  • 由于 double 和 float 在二进制中的表现比较特殊,因此不能来进行移位操作。
  • 其它几种整形 byte, short 移位前会先转换为int类型(32位)再进行移位。

右移运算符>>:

丢弃右边指定位数,左边补上符号位,符号位不会发生变化。

  • 对于 int 类型,右移位数大于等于32位操作时,会先对 32 求余后再进行右移操作。对于 long 类型,则会对 64 求余后再进行移位操作。
  • 由于 double 和 float 在二进制中的表现比较特殊,因此不能来进行移位操作。
  • 其它几种整形 byte, short 移位前会先转换为int类型(32位)再进行移位。

无符号右移>>>:

丢弃右边指定位数,左边补上0。也就是说,对于正数移位来说等同于>>,负数通过此移位运算符能移位成正数。

原码、反码与补码

  • 原码:符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。
  • 反码:正数的反码是其本身,负数的反码是在其原码的基础上,符号位不变,其余各个位取反。
  • 补码:正数的补码就是其本身,负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1(即在反码的基础上+1)。

加载顺序

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
public class Test {
static {
System.out.println("static");
}

public static void main(String[] args) {
System.out.println("main");
System.out.println(Test1.a);
}
}

class Test1 {

static final int a = 4;

static {
System.out.println("Test1");
}

Test1() {
System.out.println("Test1 construct");
}
}

->output
static
main
4
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
public class Test {
static {
System.out.println("static");
}

public static void main(String[] args) {
System.out.println("main");
System.out.println(Test1.a);
}
}

class Test1 {

static final Integer a = 4;

static {
System.out.println("Test1");
}

Test1() {
System.out.println("Test1 construct");
}
}

->output
static
main
Test1
4

代码块执行顺序: 静态代码块->构造代码块->构造方法->局部(方法)代码块

构造代码块每次执行构造方法之前都会执行.

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
public class Test extends Test1{

public Test() {
System.out.println("Test construct");
}

static {
System.out.println("Test static");
}

{
System.out.println("Test");
}

public static void main(String[] args) {
new Test();
}

}

class Test1 {

{
System.out.println("Test1");
}

static {
System.out.println("Test1 static");
}

Test1() {
System.out.println("Test1 construct");
}
}

->output
Test1 static
Test static
Test1
Test1 construct
Test
Test construct

异常体系

异常体系

Java中异常的体系是树形结构,所有异常的超类是Throwale,它有俩个子类:Error和Exception,分别表示错误和异常,其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常,这两种异常有很大的区别,也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

  1. Error与Exception
    • Error是程序无法处理的错误,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
    • Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常,程序中应当尽可能去处理这些异常。
  2. 运行时异常和非运行时异常
    • 运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,程序中可以选择捕获处理,也可以不处理。
    • 非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过,如IOException、SQLException等以及用户自定义的Exception异常。

异常处理

  • 在try中return后,依旧会执行finally。
  • finally语句在return语句执行之后return返回之前执行的。
  • finally块中的return语句会覆盖try块中的return返回。
  • 如果finally语句中没有return语句覆盖返回值,那么原来的返回值可能因为finally里的修改而改变也可能不变(具体区分值和引用)。
  • 当发生异常后,catch中的return执行情况与未发生异常时try中return的执行情况完全一样。
  • 在Java中如果不发生异常的话,try/catch不会造成任何性能损失。在 Java 类编译后,正常流程与异常处理部分是分开的,类会跟随一张异常表,每一个try-catch都会在这个表里添加行记录。当执行抛出了异常时,首先去异常表中查找是否可以被catch,如果可以则跳到异常处理的起始位置开始处理,如果没有则原地return,并且copy异常的引用给父调用方,接着看父调用的异常表,以此类推。
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
class Solution {
private static int v;
private static Map<String, Integer> map = new HashMap<>();

public static void main(String[] args) {
System.out.println("a " + test1());
System.out.println("b " + v);

System.out.println("e " + test2().get("test"));
System.out.println("f " + map.get("test"));
}

private static int test1() {
try {
v = 1;
return v;
} finally {
v = 2;
System.out.println("c " + v);
}
}

private static Map<String, Integer> test2() {
try {
map.put("test", 2);
return map;
} finally {
map.put("test", 3);
System.out.println("d " + map.get("test"));
}
}
}

->output:
c 2
a 1
b 2
d 3
e 3
f 3

装箱和拆箱

Java为每种基本数据类型都提供了对应的包装器类型.

1
2
Integer i = 10;  //装箱
int n = i; //拆箱

Java Integer源码:

1
2
3
4
5
6
7
8
9
public Integer(int value) {
this.value = value;
}

public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
  • Java中int和Integer使用==比较将Integer拆箱成int后比较大小(jdk版本不小于1.5),Integer和Integer之间==比较,是对象之间的比较,看两个引用是否指向同一个内存地址.但是一个字节的整数-128到127之间的整数将被缓存至IntegerCache,所有一个字节大小的Integer都存储于IntegerCache中,new创建的除外.
  • 直接定义 Integer a = 1 通过 Integer.valueOf设置。
  • field.set(obj, int)通过反射设置value时也走了 Integer.valueOf 方法。
  • field.set(obj, new Integer(int)) 设置的是Integer类型,就不会再拆箱后再装箱。

实例一

1
2
3
4
5
6
7
8
Integer i01=59;
int i02=59;
Integer i03=Integer.valueOf(59);
Integer i04=new Integer(59);
System.out.println(i01==i02); // true
System.out.println(i01==i03); // true
System.out.println(i01==i04); // false
System.out.println(i02==i02); // true

实例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
Integer a = 1;
Integer b = 1;
Integer c = 2;
System.out.println(a==b); // true
System.out.println(a==c); // false
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(a, 2);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(a==b); // true
System.out.println(a==c); // false
System.out.println(a.intValue()==c.intValue()); // true
}

枚举

基本用法:

1
2
3
public enum Type{
A,B,C,D;
}

创建enum时,编译器会自动为我们生成一个继承自java.lang.Enum<E>的类.

1
2
3
4
5
final class Type extends Enum {
public static final Type A;
public static final Type B;
...
}

对于上面的例子,我们可以把Type看作一个类,而把A,B,C,D看作类的Type的实例。当然,这个构建实例的过程不是我们做的,一个enum的构造方法限制是private的,也就是不允许我们调用。

可以在创建枚举的时候指定值:

1
2
3
public enum Type{
A("test1"), B("test2"), C("test3"), D("test4");
}

上面说到,我们可以把Type看作一个类,而把A,B。。。看作Type的一个实例。同样,在enum中,我们可以定义类和实例的变量以及方法。看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Type{
A,B,C,D;

static int value;
public static int getValue() {
return value;
}

String type;
public String getType() {
return type;
}
}

在原有的基础上,添加了类方法和实例方法。我们把Type看做一个类,那么enum中静态的域和方法,都可以视作类方法。和我们调用普通的静态方法一样,这里调用类方法也是通过 Type.getValue()即可调用,访问类属性也是通过Type.value即可访问。

下面的是实例方法,也就是每个实例才能调用的方法。那么实例是什么呢?没错,就是A,B,C,D。所以我们调用实例方法,也就通过 Type.A.getType()来调用就可以了。
最后,对于某个实例而言,还可以实现自己的实例方法。再看下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Type{
A{
public String getType() {
return "I will not tell you";
}
},B,C,D;
static int value;

public static int getValue() {
return value;
}

String type;
public String getType() {
return type;
}
}

除此之外,我们还可以添加抽象方法在enum中,强制ABCD都实现各自的处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum Type{
A{
public String getType() {
return "A";
}
},B {
@Override
public String getType() {
return "B";
}
},C {
@Override
public String getType() {
return "C";
}
},D {
@Override
public String getType() {
return "D";
}
};

public abstract String getType();
}

lambda

lambda表达式也称为闭包。语法如下:

1
2
3
(parameters) -> expression
// 或
(parameters) ->{ statements; }

lambda表达式有如下特点:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

实例:

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
public class Java8Tester {
public static void main(String args[]){
Java8Tester tester = new Java8Tester();

// 类型声明
MathOperation addition = (int a, int b) -> a + b;

// 不用类型声明
MathOperation subtraction = (a, b) -> a - b;

// 大括号中的返回语句
MathOperation multiplication = (int a, int b) -> { return a * b; };

// 没有大括号及返回语句
MathOperation division = (int a, int b) -> a / b;

System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
System.out.println("10 / 5 = " + tester.operate(10, 5, division));

// 不用括号
GreetingService greetService1 = message ->
System.out.println("Hello " + message);

// 用括号
GreetingService greetService2 = (message) ->
System.out.println("Hello " + message);

greetService1.sayMessage("Runoob");
greetService2.sayMessage("Google");
}

interface MathOperation {
int operation(int a, int b);
}

interface GreetingService {
void sayMessage(String message);
}

private int operate(int a, int b, MathOperation mathOperation){
return mathOperation.operation(a, b);
}
}

泛型

  • 泛型,即参数化类型。顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数。

  • 泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率。

  • 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。

  • 泛型只在编译阶段有效

    1
    2
    3
    4
    5
    6
    7
    8
    9
    List<String> stringArrayList = new ArrayList<String>();
    List<Integer> integerArrayList = new ArrayList<Integer>();

    Class classStringArrayList = stringArrayList.getClass();
    Class classIntegerArrayList = integerArrayList.getClass();

    if(classStringArrayList.equals(classIntegerArrayList)){
    Log.d(TAG, "类型相同");
    }

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
public class Generic<T>{ 
// key这个成员变量的类型为T,T的类型由外部指定
private T key;

public Generic(T key) { // 泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}

public T getKey(){ // 泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}

传入的实参类型需与泛型的类型参数类型相同,即Integer,String等。

泛型接口

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
// 定义一个泛型接口
public interface Generator<T> {
public T next();
}

/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}

/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {

private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}

泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 泛型类
public class Generic<T> {
private T key;

public Generic(T key) {
this.key = key;
}

// 虽然在方法中使用了泛型,但是这并不是一个泛型方法
// 这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型
// 所以在这个方法中才可以继续使用T这个泛型。
public T getKey() {
return key;
}

/**
* 这才是一个真正的泛型方法。
*/
public <T> T showKeyName(Generic<T> container) {
// ...
}
}

泛型方法与可变参数

1
2
3
4
5
6
7
public <T> void printMsg( T... args) {
for(T t : args) {
Log.d(TAG, "t is " + t);
}
}

printMsg("111", 222, "aaaa", "2323.4", 55.55);

静态方法与泛型

1
2
3
4
5
6
7
8
9
public class StaticGenerator<T> {
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以
*/
public static <T> void show(T t){
// ...
}
}

泛型擦除

泛型是在 JDK 1.5 里引入的,如果不做泛型擦除,那么 JVM 需要对应使得能正确的的读取和校验泛型信息;另外为了兼容老程序,需为原本不支持泛型的 API 平行添加一套泛型 API。

逆变与协变(泛型通配符)

定义:如果A、B表示类型,f()表示一个类型的构造函数,Type1≤Type2表示Type1是Type2的子类型,Type1≥Type2表示Type1是Type2的超类型;

  • f()是逆变(contravariant)的:当A≤B时有f(B)≤f(A)成立;
  • f()是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
  • f()是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)f(B)相互之间没有继承关系。

对于任意两个不同的类型 Type1 和 Type2,不论它们之间具有什么关系,给定泛型类 G<T>G<Type1>G<Type2> 都是没有关系的,即 Java 中泛型是不变的,而数组是协变的:

1
2
3
4
5
6
7
public static void main(String[] args) {
// 编译报错
ArrayList<Number> list = new ArrayList<Integer>();;

// 可以正常通过编译,正常使用
Number[] arr = new Integer[]{1, 2};
}

在java泛型中引入了?符号来支持协变和逆变:

  • ? extends T(上边界通配符)实现协变关系,表示?是继承自T的任意子类型。也表示一种约束关系,只能提供数据,不能接收数据。
  • ? super T(下边界通配符)实现逆变关系,表示?T的任意父类型。也表示一种约束关系,只能接收数据,不能提供数据。
  • ?的默认实现是? extends Object,表示?是继承自Object的任意类型。
  • PECS 法则:「Producer-Extends, Consumer-Super」。

如下所示:

1
2
3
4
5
List<? extends Number> list1 = new ArrayList<Integer>();
List<? super Integer> list2 = new ArrayList<Number>();

// Collections类中的copy方法
public static <T> void copy(List<? super T> dest, List<? extends T> src) {}

泛型数组

Java不能直接创建泛型数组,一般都使用List替代。比如说HashMap<Integer> map = new HashMap<Integer>[]会编译失败,因为Java在编译器的类型擦除,上面的元素会变成Object类型,编译器在编译时会尽可能的发现可能出错的地方,因此会编译失败。

可以通过这种方式创建泛型数组:

1
List<Integer>[] genericArray = (List<Integer>[]) new ArrayList[10];

Type

概述

Type是Java语言中所有类型的公共父接口,Type的直接子类只有一个,也就是Class,代表着类型中的原始类型以及基本类型。接下来会解析Type的四个子接口。

在jdk1.5之前Java中只有原始类型而没有泛型类型,而在JDK 1.5之后引入泛型,但是这种泛型仅仅存在于编译阶段,当在JVM运行的过程中,与泛型相关的信息将会被擦除,如List<Person>List<String>都将会在运行时被擦除成为List这个类型。而类型擦除机制存在的原因正是因为如果在运行时存在泛型,那么将要修改JVM指令集,这是非常致命的。

此外,原始类型会生成字节码文件对象,而泛型类型相关的类型并不会生成与其相对应的字节码文件(因为泛型类型将会被擦除),因此,无法将泛型相关的新类型与Class相统一。因此,为了程序的扩展性以及为了开发需要去反射操作这些类型,就引入了Type这个类型,并且新增了ParameterizedType, TypeVariable, GenericArrayType, WildcardType四个表示泛型相关的类型,再加上Class,这样就可以用Type类型的参数来接受以上五种类型的实参或者返回值类型就是Type类型的参数。统一了与泛型有关的类型和原始类型Class。而且这样一来,我们也可以通过反射获取泛型类型参数。

ParameterizedType

参数化类型即我们通常所说的泛型类型,它的三个重要方法:

  • Type getRawType():该方法的作用是返回当前的ParameterizedType的类型,如List返回的是List的Type,即返回当前参数化类型本身的Type。
  • Type getOwnerType():返回ParameterizedType类型所在的类的Type,如Map.Entry<String, Object>这个参数化类型返回的是Map的类型。
  • Type[] getActualTypeArguments():该方法返回参数化类型<>中的实际参数类型,如Map<String, Person> map这个ParameterizedType返回的是String类,Person类的全限定类名的Type Array。该方法只返回最外层的<>中的类型,无论该<>内有多少个<>

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
private HashMap<String, Object> mMap;
private List<String> mList;
private Class<?> mCls;

public static void main(String[] args) {
Field[] fields = Main.class.getDeclaredFields();
for (Field field : fields) {
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
System.out.println(field.getName() + ": " + ((ParameterizedType) type).getActualTypeArguments()[0].getTypeName() + ", " + ((ParameterizedType) type).getRawType());
}
}
}
}

->output
mMap: java.lang.String, class java.util.HashMap
mList: java.lang.String, interface java.util.List
mCls: ?, class java.lang.Class

TypeVariable

类型变量,范型信息在编译时会被转换为一个特定的类型, 而TypeVariable就是用来反映在JVM编译该泛型前的信息(通俗的来说,TypeVariable就是我们常用的T,K这种泛型变量)。

  • Type[] getBounds():返回当前类型的上边界,如果没有指定上边界,则默认为Object。
  • String getName():返回当前类型的类名。
  • D getGenericDeclaration():返回当前类型所在的类的Type。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main<K, T> {
private K mK;
private T mT;

public static void main(String[] args) {
TypeVariable[] variables = Main.class.getTypeParameters();
for (TypeVariable variable : variables) {
int index = variable.getBounds().length - 1;
System.out.println(variable.getName() + ": " + variable.getBounds()[index] + ", " + variable.getGenericDeclaration());
}
}
}

->output
K: class java.lang.Object, class Main
T: class java.lang.Object, class Main

GenericArrayType

泛型数组类型,组成数组的元素中有泛型则实现了该接口,它的组成元素是ParameterizedType或TypeVariable类型。(通俗来说,就是由参数类型组成的数组。如果仅仅是参数化类型,则不能称为泛型数组,而是参数化类型)。**注意:无论从左向右有几个[]并列,这个方法仅仅脱去最右边的[]之后剩下的内容就作为这个方法的返回值。**

  • Type getGenericComponentType():返回组成泛型数组的实际参数化类型,如List[]则返回List。

实例:

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
public class Main<T> {
// 泛型数组类型
private T[] mTs;
private List<String>[] mLists;

// 不是泛型数组类型
private List<String> mList;
private T mT;

public static void main(String[] args) {
Field[] fields = Main.class.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
// 输出当前变量是否为GenericArrayType类型
System.out.println("Field " + field.getName() + " is " + (field.getGenericType() instanceof GenericArrayType));
if (field.getGenericType() instanceof GenericArrayType) {
// 如果是GenericArrayType,则输出当前泛型类型
System.out.println("Field " + field.getName() + " is " + (((GenericArrayType) field.getGenericType()).getGenericComponentType()));
}
}
}
}

->output
Field mTs is true
Field mTs is T
Field mLists is true
Field mLists is java.util.List<java.lang.String>
Field mList is false
Field mT is false

WildcardType

通配符表达式,或泛型表达式,它虽然是Type的一个子接口,但并不是Java类型中的一种,表示的仅仅是类似 <?>, <? Extends Number> 这样的表达式。

  • Type[] getLowerBounds():得到下边界的Type数组。
  • Type[] getUpperBounds():得到上边界的Type数组。

注:如果没有指定上边界,则默认为Object,如果没有指定下边界,则默认为String。

实例:

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
public class Main {
private List<String> mList0;
private List<?> mList1;
private List<? extends List> mList2;
private List<? super List> mList3;

public static void main(String[] args) {
Field[] fields = Main.class.getDeclaredFields();
for (Field field : fields) {
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
Type[] arguments = ((ParameterizedType) type).getActualTypeArguments();
for (Type argument : arguments) {
if (argument instanceof WildcardType) {
System.out.println("Field: " + field.getName() + ", "
+ ((WildcardType) argument).getUpperBounds() + ", " + ((WildcardType) argument).getLowerBounds());
}
}
}
}
}
}

->output
Field: mList1, [Ljava.lang.reflect.Type;@15db9742, [Ljava.lang.reflect.Type;@6d06d69c
Field: mList2, [Ljava.lang.reflect.Type;@7852e922, [Ljava.lang.reflect.Type;@4e25154f
Field: mList3, [Ljava.lang.reflect.Type;@70dea4e, [Ljava.lang.reflect.Type;@5c647e05

Socket

基础

  • port范围从0到65535,其中0到1023被系统保留。
  • Java提供的网络功能有四大类:
    • InetAddress:标识网络上的硬件资源
    • URL:统一资源定位符
    • Sockets:使用TCP协议
    • Datagram:使用UDP协议

InetAddress

无构造方法。

InetAddress address = InetAddress.getLocalHost();
System.out.println(address.getHostAddress());//222.20.25.251
System.out.println(address.getHostName());//admin-PC
InetAddress address1 = InetAddress.getByName("222.20.25.251");
System.out.println(address1.getHostName());//admin-PC

URL

通过URL的openStream()方法打开到此 URL 的连接并返回一个用于从该连接读入的 InputStream(字节输入流)。

try {
    URL url = new URL("http://www.baidu.com");
    InputStream inputStream = url.openStream();
    //将字节型输入流转换为字符型输入流
    InputStreamReader reader = new InputStreamReader(inputStream, "utf-8");
    //将字符输入流添入缓冲
    BufferedReader bufferedReader = new BufferedReader(reader);
    String data = bufferedReader.readLine();
    while (data != null){
        System.out.println(data);
        data = bufferedReader.readLine();
    }
    inputStream.close();
    reader.close();
    bufferedReader.close();
} catch (MalformedURLException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

Socket实现TCP编程

服务端:

//1、创建一个服务端Socket
ServerSocket serverSocket = new ServerSocket(8888);
//2、调用accept方法等待连接
System.out.println("服务器等待客户端连接。。。");
Socket socket = serverSocket.accept();
//3、获取输入流
InputStream is = socket.getInputStream();
InputStreamReader reader = new InputStreamReader(is, "utf-8");
BufferedReader bufferedReader = new BufferedReader(reader);
String info = null;
while ((info = bufferedReader.readLine()) != null){
    System.out.println("服务器接收到来自客户端的:" + info);
}
//4、关闭输入流
socket.shutdownInput();

OutputStream os = socket.getOutputStream();

PrintWriter writer = new PrintWriter(os);
writer.write("欢迎");
writer.flush();

//5、关闭资源
writer.close();
os.close();
bufferedReader.close();
reader.close();
is.close();
socket.close();
serverSocket.close();

客户端:

//1、创建客户端Socket,指定服务器IP和端口
Socket socket = new Socket("localhost", 8888);
//2、获取输出流
OutputStream os = socket.getOutputStream();
PrintWriter writer = new PrintWriter(os);//打印流
writer.write("用户名:刘家东----密码:123456");
writer.flush();
socket.shutdownOutput();
//3、获得输入流
InputStream is = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
String info = null;
while ((info = bufferedReader.readLine()) != null){
    System.out.println("响应信息:" + info);
}
//4、关闭资源
bufferedReader.close();
is.close();
writer.close();
os.close();
socket.close();

Socket实现UDP编程

DatagramPacket:表示数据报包

DatagramSocket:进行端到端通信的类

服务端:

  1. 创建DatagramSocket,指定端口号。
  2. 创建DatagramPacket,用于接收信息。
  3. 接收客户端发送的数据信息
  4. 读取数据

客户端:

  1. 定义发送信息
  2. 创建DatagramPacket,包含要发送的信息
  3. 创建DatagramSocket
  4. 发送数据

服务端:

public static void main(String[] args) throws IOException {
    //1、创建DatagramSocket,指定端口号。
    DatagramSocket socket = new DatagramSocket(8800);
    //2、创建DatagramPacket,用于接收信息
    byte[] data = new byte[1024];
    DatagramPacket packet = new DatagramPacket(data, data.length);
    //3、接收客户端发送的数据信息,此方法在接收到数据前会一直堵塞
    System.out.println("服务器已经启动。。。");
    socket.receive(packet);
    //4、读取数据
    String info = new String(data, 0, packet.getLength());
    System.out.println("服务端接收到客户端的信息:" + info);

    //响应
    //1、定义发送信息,服务器的地址、端口号、数据
    InetAddress address = packet.getAddress();
    int port = packet.getPort();
    byte[] response = "欢迎你!".getBytes();
    //2、创建DatagramPacket,包含要发送的信息
    DatagramPacket packet1 = new DatagramPacket(response, response.length, address, port);
    //3、创建DatagramSocket
    socket.send(packet1);
    //4、关闭资源
    socket.close();
}

客户端:

public static void main(String[] args) throws IOException {
    //1、定义发送信息,服务器的地址、端口号、数据
    InetAddress address = InetAddress.getByName("localhost");
    int port = 8800;
    byte[] data = "用户名:admin;密码:123456".getBytes();
    //2、创建DatagramPacket,包含要发送的信息
    DatagramPacket packet = new DatagramPacket(data, data.length, address, port);
    //3、创建DatagramSocket
    DatagramSocket socket = new DatagramSocket();
    socket.send(packet);

    //接收响应
    //1、创建DatagramPacket,用于接收信息
    byte[] resp = new byte[1024];
    DatagramPacket packet1 = new DatagramPacket(resp, resp.length);
    //2、接收响应
    socket.receive(packet1);
    //3、读取数据
    String reply = new String(resp, 0, packet1.getLength());
    System.out.println("响应:" + reply);
    //4、关闭资源
    socket.close();
}

总结

对于同一个socket,如果关闭了输出流,则与该输出流关联的socket也会被关闭,所以一般不用关闭流,直接关闭socket即可。

函数式接口

有且只有一个抽象方法的接口被称为函数式接口,函数式接口适用于函数式编程的场景,Lambda就是Java中函数式编程的体现,可以使用Lambda表达式创建一个函数式接口的对象,一定要确保接口中有且只有一个抽象方法,这样Lambda才能顺利的进行推导。

Java 8中专门为函数式接口引入了一个新的注解 – @FunctionalInterface,该注解可用于一个接口的定义上,不是必须的,其作用只是让编译器检查该接口是否满足函数式接口规范。

接口增强

默认方法

Java 8引入了新的语言特性——默认方法(Default Methods),默认方法是在接口中的方法签名前加上了default关键字的实现方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface InterfaceA {
default void foo() {
System.out.println("InterfaceA foo");
}
}

class ClassA implements InterfaceA {
}

public class Test {
public static void main(String[] args) {
new ClassA().foo(); // 打印:“InterfaceA foo”
}
}

静态方法

在Java8中接口里可以声明静态方法,并且可以实现。

1
2
3
4
5
public interface Person {
public static void doSomething(String something) {
System.out.println("I am Do :" + something);
}
}

Stream API

1
Stream.of(10, 3, 3, 15, 9, 23).map(n -> n * 2).filter(n->n>10).toArray();

Optional API

在 Java 8 引入Optional特性的基础上,Java 9 又为 Optional 类增加了三种方法:or()、ifPresentOrElse() 和 stream()。该类可用于解决空指针问题,从本质上来说,该类属于包含可选值的封装类(wrapper class),因此它既可以包含对象也可以仅仅为空。

1
2
3
4
// user1不能传空
User result = Optional.of(user1).get();
// user2可以为空
result = Optional.ofNullable(user2).orElse(user1);

指针和引用

指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递的特点是被调函数对形参的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值(这里是在说实参指针本身的地址值不会变)。

而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址(int &a的形式)。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

阅读全文 »