xiaobaoqiu Blog

Think More, Code Less

Memcached

最近项目中的图片上传那块带来很大麻烦,原因是因为业务逻辑的问题,会导致统一张图片上传很多次(平均5次上传有4次是重复的),导致图片服务器压力很大.

直观想法是使用KV缓存,第一个想到的是Memcached.

其实很长时间之前就学习过一段时间的Memcached,但是一致没有整理,趁这次机会,整理一下Memcached相关的知识.

1.Memcached是什么

Memcached官方:http://memcached.org/

Memcached wiki:http://code.google.com/p/memcached/wiki/NewStart

Memcached是一个免费的开源的,高性能的,分布式的内存KV(key-value)缓存系统,一般通过缓解数据库压力来达到加速动态Web应用程序的目的.可以存储任意类型的小数据块,比如数据库查询结果,接口调用结果等.

Memcached可以称之为简单高效.它简单的设计保证了快速发布,使用者开发简单,面临大量数据缓存的时候能够解决很多问题.同时提供了绝大多数流行语言的API接口.

许多Web应用都将数据保存到RDBMS中,应用服务器从中读取数据并在浏览器中显示。 但随着数据量的增大、访问的集中,就会出现RDBMS的负担加重、数据库响应恶化、 网站显示延迟等重大影响。

这时就该memcached大显身手了。一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。

2.Memcached特性

memcached作为高速运行的分布式缓存服务器,具有以下的特点: 1. 协议简单 2. 基于libevent的事件处理 3. 内置内存存储方式 4. memcached不互相通信的分布式 下面分别介绍.

2.1协议简单

Memcached使用简单的基于文本行的协议。因此,通过telnet 也能在memcached上保存数据、取得数据。如下:

1
2
3
4
5
6
7
8
9
10
11
xiaobaoqiu@xiaobaoqiu:~$ telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
add myid 1 0 4
1234
STORED
get myid
VALUE myid 1 4
1234
END

协议文档位于memcached的源代码内,可以参考: https://github.com/memcached/memcached/blob/master/doc/protocol.txt

2.2基于libevent的事件处理

libevent是个程序库,它将Linux的epoll、BSD类操作系统的kqueue等事件处理功能 封装成统一的接口。即使对服务器的连接数增加,也能发挥O(1)的性能。 memcached使用这个libevent库,因此能在Linux、BSD、Solaris等操作系统上发挥其高性能。

关于libevent参考: http://libevent.org/ http://www.kegel.com/c10k.html

2.3内置内存存储方式

为了提高性能,memcached中保存的数据都存储在memcached内置的内存存储空间中。由于数据仅存在于内存中,因此重启memcached、重启操作系统会导致全部数据消失。另外,内容容量达到指定值之后,就基于LRU(Least Recently Used)算法自动删除不使用的缓存。

memcached本身是为缓存而设计的服务器,因此并没有过多考虑数据的永久性问题。

2.4memcached不互相通信的分布式

memcached虽然号称“分布式”缓存服务器,但服务器端并没有分布式功能。 各个memcached不会互相通信以共享信息。那么,怎样进行分布式呢? 这完全取决于客户端的实现。

3.Memcached安装

所有可下载版本见: http://code.google.com/p/memcached/wiki/ReleaseNotes

直接按照官网安装就可以,下面直接安装最新的版本,需要已经安装了libevent:

1
2
3
4
wget http://memcached.org/latest
tar -zxvf memcached-1.x.x.tar.gz
cd memcached-1.x.x
./configure && make && make test && sudo make install

memcached的命令参数见-h,从中也可以看出memcached的版本:

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
xiaobaoqiu@xiaobaoqiu:~$ memcached -h
memcached 1.4.14
-p <num>      TCP监听端口TCP(默认: 11211)
-U <num>      UDP监听端口TCP(默认: 11211, 0表示关闭)
-s <file>     用于监听的UNIX套接字路径(禁用网络支持)
-a <mask>     UNIX套接字访问掩码,八进制数字(default: 0700)
-l <addr>     监听的IP地址(默认:INADDR_ANY,所有地址), <addr> 形如host:port.
-d            以守护进程(daemon)形式运行
-r            最大核心文件(core file)限制
-u <username> 设定进程所属用户(只有root用户可以使用这个参数)
-m <num>      最大使用内存,单位MB(默认: 64 MB)
-M            当内存耗尽时候返回错误 (而不是移除过期项)
-c <num>      并发连接的最大数目(默认: 1024)
-k            锁定所有内存页。注意你可以锁定的内存是有上限的.
              试图分配更多内存会失败的,所以留意启动守护进程时所用的用户可分配的内存上限(不是前面的 -u <username> 参数;在sh下,使用命令"ulimit -S -l NUM_KB"来设置).
-v            提示信息(在事件循环中打印错误/警告信息)
-vv           详细信息(还打印客户端命令/响应)
-vvv          超详细信息(还打印内部状态的变化)
-h            帮助信息
-i            打印memcached版本和libevent版本
-P <file>     将进程id保存在文件中,只在-d选项下有效
-f <factor>   块大小增长因子(默认: 1.25)
-n <bytes>    chunk(key+value+flags)的最小空间 (默认: 48)
              (chunk数据结构本身需要消耗48个字节,所以一个chunk实际消耗的内存是n+48)
-L            尝试使用大内存页(如果可以的话). 增加内存页大小能够减少页表缓冲(TLB)
              丢失次数,提高效率.为了从操作系统获取大内存页,memcached会把全部数据项分配到一个大区块
-D <char>     使用 <char> 作为前缀和ID的分隔符.
              这个用于按前缀获得状态报告。默认是":"(冒号).
              如果指定了这个参数,则状态收集会自动开启;如果没指定,则需要用命令"stats detail on"来开启。
-t <num>      使用的线程数目 (默认: 4)
-R            每个事件的最大请求数目,即每个连接处理的最大请求数目(默认: 20)
-C            禁用CAS
-b            设置积压队列(backlog queue)的最大限制(默认: 1024)
-B            绑定协议,可选值:ascii,binary,auto(默认)
-I            重写每个数据页尺寸。调整数据项最大尺寸(默认: 1mb, min: 1k, max: 128m)
-S            Turn on Sasl authentication
-o            Comma separated list of extended or experimental options
              - (EXPERIMENTAL) maxconns_fast: immediately close new
                connections if over maxconns limit
              - hashpower: An integer multiplier for how large the hash
                table should be. Can be grown at runtime if not big enough.
                Set this based on "STAT hash_power_level" before a 
                restart.

启动memcached时候可以带上这些选项,启动memcached:

1
xiaobaoqiu@xiaobaoqiu:~$ memcached -d

需要注意的是,还有一个memcached.conf文件存在(通过whereis memcached可以看得到),他是memcached默认的配置参数,其内容就是我们上面介绍的memcached启动参数,我的文件如下:

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
# memcached default config file
# 2003 - Jay Bonci <jaybonci@debian.org>
# This configuration file is read by the start-memcached script provided as
# part of the Debian GNU/Linux distribution. 

# Run memcached as a daemon. This command is implied, and is not needed for the
# daemon to run. See the README.Debian that comes with this package for more
# information.
# 以守护进程的形似运行
-d

# Log memcached's output to /var/log/memcached
# 日志文件存放位置
logfile /var/log/memcached.log

# Be verbose
# -v

# Be even more verbose (print client commands as well)
# -vv

# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default
# Note that the daemon will grow to this size, but does not start out holding this much
# memory
# 分配给mencached的内存数目,单位是MB
-m 256

# Default connection port is 11211
# Memcached的监听端口
-p 11211

# Run the daemon as root. The start-memcached will default to running as root if no
# -u command is present in this config file
# 允许memcached的用户
-u memcache

# Specify which IP address to listen on. The default is to listen on all IP addresses
# This parameter is one of the only security measures that memcached has, so make sure
# it's listening on a firewalled interface.
# 监听的服务器IP地址
-l 127.0.0.1

# Limit the number of simultaneous incoming connections. The daemon default is 1024
# -c 1024

# Lock down all paged memory. Consult with the README and homepage before you do this
# -k

# Return error when memory is exhausted (rather than removing items)
# -M

# Maximize core file limit
# -r

我们启动memecached默认会带上这个文件的参数,可以通过ps命令看我们memcached启动的参数:

1
2
xiaobaoqiu@xiaobaoqiu:~$ ps -ef | grep memcached
memcache  1241     1  0 10:38 ?        00:00:00 /usr/bin/memcached -m 256 -p 11211 -u memcache -l 127.0.0.1

可以使用telnet连接上memcached,之后就可以输入memcached的命令了:

1
2
3
4
xiaobaoqiu@xiaobaoqiu:~$ telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.

4.Memcached命令

介绍Memcached的常用命令,不断补充中,官方文档参考: https://code.google.com/p/memcached/wiki/NewCommands 按照命令左右,简单分为存储,删除,自增自减,读取和状态四类命令

4.1存储命令

命令:add set replace 格式:

1
2
3
<command name> <key> <flags> <exptime> <bytes>

<data block>

其中参数command name指操作命令,即set/add/replace;key指缓存的键值;flags指客户机使用它存储关于键值对的额外信息,exptime缓存过期时间,单位为秒,0表示永远存储;bytes缓存值的字节数;data block指数据块.

(1).set:无论如何都添加或更新的命令(key不存在则添加,存在则更新)

下面的示例包含了key存在和不存在两种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xiaobaoqiu@xiaobaoqiu:~$ telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
set name 0 0 11
baoqiu.xiao
STORED
get name
VALUE name 0 11
baoqiu.xiao
END
set name 0 0 9 
memcached
STORED
get name
VALUE name 0 9
memcached
END
(2).add:只有数据不存在时添加值的add命令

已经存在的key是不能再add

1
2
3
4
5
6
7
8
9
10
xiaobaoqiu@xiaobaoqiu:~$ telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
add sex 0 0 1
f
STORED
add sex 0 0 1
m
NOT_STORED
(3).replace:只有数据存在时替换的replace命令

只有存在才能replace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xiaobaoqiu@xiaobaoqiu:~$ telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
replace host 0 0 9
localhost
NOT_STORED
add host 0 0 9
127.0.0.1
STORED
get host
VALUE host 0 9
127.0.0.1
END
replace host 0 0 9
localhost
STORED
get host 
VALUE host 0 9
localhost
END

4.2删除命令

(1).delete:删除已经存在的数据
1
2
3
4
5
6
7
8
9
10
11
xiaobaoqiu@xiaobaoqiu:~$ telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
delete unknow
NOT_FOUND
add unknow 0 0 6
unknow
STORED
delete unknow
DELETED

4.3自增自减命令

incr和decr命令,注意只能在数字类型上进行自增自减操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xiaobaoqiu@xiaobaoqiu:~$ telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
set age 2 0 2
25
STORED
incr age 1
26
incr age 3
29
decr age 2
27
set name 1 0 11
baoqiu.xiao
STORED
incr name 1
CLIENT_ERROR cannot increment or decrement non-numeric value

4.4读取命令

(1).get:获取一条或者多条数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xiaobaoqiu@xiaobaoqiu:~$ telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
add name 1 0 11
baoqiu.xiao
STORED
add age 2 0 2
25
STORED
get name age
VALUE name 1 11
baoqiu.xiao
VALUE age 2 2
25
END
(2).gets:获取一条或者多条数据

gets命令比get返回的值多一个数字(类似于版本号)用来判断数据是否发生过改变.

1
2
3
4
5
6
gets name age
VALUE name 1 11 12
baoqiu.xiao
VALUE age 2 2 13
25
END
(3).cas:意思是check and set

只有当最后一个参数和gets获取的那个用来判断数据发生改变的那个值相同时才会存储成功,否则返回 exists.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xiaobaoqiu@xiaobaoqiu:~$ telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
gets name
VALUE name 1 11 19
baoqiu.xiao
END
cas name 1 0 9 20
memcached
EXISTS
cas name 1 0 9 19
memcached
STORED

gets返回的版本参数为19,cas的最后一个参数必须为19才能设置成功.

4.5状态命令

(1).stat:显示memcachd状态

包括使用的内存大小等关键信息,下面包含各个项的意义:

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
xiaobaoqiu@xiaobaoqiu:~$ telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
stats
STAT pid 1241                   //memcached服务进程的进程ID
STAT uptime 24026               //memcached服务从启动到当前所经过的时间,单位是秒
STAT time 1411579124            //memcached服务器所在主机当前系统的时间,单位是秒
STAT version 1.4.14             //memcached组件的版本
STAT libevent 2.0.21-stable     //libevent版本
STAT pointer_size 64            //服务器所在主机操作系统的指针大小,一般为32或64
STAT rusage_user 0.404722       //进程累计使用用户时间
STAT rusage_system 0.313086     //进程累计使用系统时间
STAT curr_connections 5         //当前系统打开的连接数
STAT total_connections 21       //从memcached服务启动开始,系统打开过的连接总数
STAT connection_structures 6    //从memcached服务启动到现在,被服务器分配的连接结构数量
STAT reserved_fds 20            //
STAT cmd_get 15                 //累积get命令次数
STAT cmd_set 22                 //累积set命令次数
STAT cmd_flush 0                //累积flush命令次数
STAT cmd_touch 0                //
STAT get_hits 9                 //命中次数,即get成功的次数
STAT get_misses 6               //miss次数,即get失败的次数
STAT delete_misses 2            //delete miss次数
STAT delete_hits 5              //delete 命中次数
STAT incr_misses 0              //incr miss次数
STAT incr_hits 2                //incr 命中次数
STAT decr_misses 0              //decr miss次数
STAT decr_hits 1                //decr命中次数
STAT cas_misses 0               //cas命令miss的次数
STAT cas_hits 1                 //cas命令命中的次数
STAT cas_badval 1               //使用cas擦拭次数
STAT touch_hits 0
STAT touch_misses 0
STAT auth_cmds 0
STAT auth_errors 0
STAT bytes_read 968             //memcached服务器从网络读取的总的字节数
STAT bytes_written 706          //memcached服务器发送到网络的总的字节数
STAT limit_maxbytes 268435456   //memcached允许使用的最大字节,256M,见memcached.conf
STAT accepting_conns 1          //目前使用的连接数
STAT listen_disabled_num 0
STAT threads 4                  //工作线程的总数量
STAT conn_yields 0
STAT hash_power_level 16
STAT hash_bytes 524288
STAT hash_is_expanding 0
STAT expired_unfetched 0
STAT evicted_unfetched 0
STAT bytes 373                  //系统存储缓存对象所使用的存储空间,单位为字节
STAT curr_items 5               //当前缓存中存放的所有缓存对象的数量,不包括已删除的
STAT total_items 22             //自启动起所有缓存对象的数量,包括已删除的
STAT evictions 0                //从缓存移除的缓存对象数目,包括过期和空间不足时LRU移除
STAT reclaimed 0
END

5.Memcached客户端

许多语言都实现了连接memcached的客户端,见官网: https://code.google.com/p/memcached/wiki/Clients

主流的语言基本都支持:

1
C/C++, PHP, Java, Python, Ruby, Perl, .Net, Erlang, Lua等

这里以Java为例,典型的三种Client,其中spymemcached和Xmemcached: (1).Memcached-Java-Client https://github.com/gwhalin/Memcached-Java-Client (2).spymemcached http://code.google.com/p/spymemcached/ (3).Xmemcached https://code.google.com/p/xmemcached/

5.1性能比较

java memcached client官方发布的性能对比:https://github.com/gwhalin/Memcached-Java-Client/wiki/PERFORMANCE

XMemcached官方发布的性能对比:http://xmemcached.googlecode.com/svn/trunk/benchmark/benchmark.html

根据网络资料,Xmemcached是一个不错的选择,无论在低并发还是高并发访问的情况下,都可以保持一个比较优秀的性能表现.

5.2Xmemcache

maven支持:http://mvnrepository.com/artifact/com.googlecode.xmemcached/xmemcached

1
2
3
4
5
<dependency>
  <groupId>com.googlecode.xmemcached</groupId>
  <artifactId>xmemcached</artifactId>
  <version>1.4.1</version>
</dependency>

简单使用:

1
2
3
4
5
6
7
8
//memcached服务的IP及端口号
String addr="192.168.100.112:11211 192.168.100.113:11211"
MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses(addr));
MemcachedClient client = builder.build();

String cacheKey = buildCacheKey();        //key
Object cacheObject = buildCacheObject();  //value
cacheClient.set(cacheKey, exp, cacheObject);  //memcached set

JVM Internals

写这篇文章的本意是想了解JVM的内存结构,这篇文章基本上是翻译的英文文章,原文在这里.因为涉及到很多专业术语,所有很多地方都贴出了原始的英文词。

1.JVM组成图

下图显示了JVM内部的关键组成元素: 下面从两个部分解释上门这个图,第一部分是每个线程内创建的元素,第二部分是独立于各个线程的公共元素。

2.线程内创建的元素

JVM允许应用程序同时运行多个线程;在Hotspot JVM里,维护了一个从java线程到系统线程的的直接映射表。

当java线程准备完所有的准备操作,比如线程内的存储(thread-local storage),申请缓冲区(allocation buffers),同步对象(synchronization objects),线程内栈(stacks)和程序计数器(program counter),则原生的操作系统级别的线程(native thread)就创建出来了。

当java线程终止(terminates)则对应的操作系统级别的线程也被回收了。

操作系统负责调度所有的线程,并将各个线程的工作分发到各个可用的CPU上面。一旦操作系统的线程初始化完成,它就会调用run()方法。

当run()返回,未捕获的异常被处理之后,对应的操作系统的线程会确认这个java线程终止之后是否需要终止JVM(比如这是JVM的最后一个java线程);当操作系统的线程终止,java线程和对应的操作系统线程占用的资源会全部释放。

2.1 JVM系统线程(JVM System Threads)

使用jconsole或者其他debugger工具,我们可以看到除了主线程之外,还有多个线程在后台运行;主线程是用来调用public static void main(String[])方法的,其他线程是主线程创建的,在后台运行的其他几个线程主要包括:

1.VM thread

这个线程等待这样的操作出现,这些操作需要JVM到达一个安全点(safe-point)。在一个单独的线程内这样的操作都会出现的原因,是因为这些操作都需要JVM到达一个safe point,在这个时间点上将不会修改堆。 这个线程执行的操作包括:"stop-the-world"的垃圾回收,线程转储堆栈(thread stack dumps),线程挂起(thread suspension)和偏向锁撤销(biased locking revocation)。

2.Periodic task thread

这个线程负责那些用于调度执行周期性的、定时的操作的事件(比如中断)。

3.GC threads

这些线程完成JVM中各种垃圾回收操作。

4.Compiler threads

这些线程在运行时将字节码(byte code)编译成本地代码(native code)。

关于native code的理解:http://www.searchsoa.com.cn/whatis/word_3074.htm

5.Signal dispatcher thread

这个线程接受发到JVM的信号(signals)并调用合适的JVM方法处理这些信号。

2.2 每个线程

每个线程执行包括以下元素:

2.2.1 程序计数器(Program Counter,PC)

所有CPU都有一个PC,PC会保存下一条将要执行指令的地址,因此随着指令执行一般而言PC的值是增长的。JVM使用PC来跟踪正在执行的指令,PC实际上是指向方法区的的一个内存地址。

关于opcode : http://www.luocong.com/learningopcode/doc/1._%E4%BB%80%E4%B9%88%E6%98%AFOpCode%EF%BC%9F.htm

2.2.2 栈(Stack)

每个线程都有一个自己的栈,这个栈用来保存在这个线程执行的每个方法的帧(frame),栈是后入先出的数据结构,因此当前执行的方法在栈顶。

每次方法的调用都会在创建一个新的帧并将这个帧增加到栈顶。栈不允许直接操作,而是只能push或者pop一个frame对象,frame对象会在堆上创建,因此不需要在内存上连续。

2.2.3 本地栈(Native Stack)

Java虚拟机实现可能会使用到传统的栈(通常称为C stack)来支持native方法(指使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈(native method stack)。

当Java虚拟机使用其他语言(例如C语言)来实现指令集解释器时,也会使用到本地方法栈。

如果Java虚拟机不支持native方法,并且自己也不依赖传统栈,可以无需支持本地方法栈,如果支持本地方法栈,那这个栈一般会在线程创建的时候按线程分配。

Java虚拟机规范允许本地方法栈实现成固定大小或者根据计算动态扩展和收缩。

任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入java栈。然而当他调用的是本地方法时,虚拟机会保持Java栈不变 ,不再在线程的java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那个他的本地方法栈就是C栈。我们知道,当C程序调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,他的返回值也以确定的方式传回调用者。

很可能本地方法接口需要回调Java虚拟机中的Java方法(这也是由设计者决定的),在这种情形下,该线程会保存本地方法栈的状态并进入到另一个Java栈。 上图所示,该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。图中的本地方法栈显示为 一个连续的内存空间。假设这是一个C语言栈,期间有两个C函数,他们都以包围在虚线中的灰色块表示。第一个C函数被第二个Java方法当做本地方法调用, 而这个C函数又调用了第二个C函数。之后第二个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过 本地方法接口回调了一个Java方法(第三个Java方法)。最终这个Java方法又调用了一个Java方法(他成为图中的当前方法)。

2.2.4 栈限制(Stack Restrictions)

栈的大小可以固定,也可以动态伸缩。如果线程需要的栈大小超出了允许值则会抛出StackOverflowError。如果一个线程需要一个新帧然而没有内存空间去申请这个帧,则会抛出OutOfMemoryError。

2.2.5 帧(Frame)

每次方法调用都会创建一个新帧并将其增加到栈顶,当方法调用正常返回或者跑出来一个未捕获的异常,则栈顶帧弹出。

每帧的内容包括: (1). 本地局部变量数组(Local variable array) (2). 返回值 (3). 操作栈(Operand stack) (4). 当前方法所属类的运行时常量池的引用(Reference to runtime constant pool for class of the current method)

2.2.6 本地变量数组(Local Variables Array)

本地变量数组包含了方法执行器件使用到的所有变量,包括this引用,所有方法参数和其他局部变量。对于静态方法,参数下标从0开始,对应instance方法(非static方法),0下标指this。

局部变量可以是以下类型:

1
2
3
4
5
6
7
8
9
10
boolean
byte
char
long
short
int
float
double
引用(reference)
返回地址(returnAddress)

所有类型的局部变量占有一个槽位(slot),long和double例外,他们占用了两个连续的槽位,因为这两个类型是双字节宽度(64位);

2.2.7 操作数栈(Operand Stack)

操作数栈在执行字节码指令(byte code instructions)时候使用,和原生的CPU中通用寄存器(general-purpose registers)的使用类似。

大部分JVM字节码操作Operand Stack的时间花费在进栈(push),出栈(pop),复制(duplicating),交换(swapping)或者执行操作来产生或消费values;因此,在本地变量数组和操作数栈之间移动值的字节码指令会频繁执行。

例如一个简单变量初始化会产生两条影响操作数栈的字节码:

1
int i;

会编译成下面两个字节码:

1
2
0:iconst_0 // Push 0 to top of the operand stack
1:istore_1 // Pop value from top of operand stack and store as local variable 1

关于更多本地变量数组,操作数栈和运行时常量池的交互细节参考下面的章节:Class File Structure.

2.2.8 动态链接(Dynamic Linking)

每个帧包含一个运行时常量池的引用,这个引用指向执行当前方法的类使用的常量池,这个引用用来支持动态连接。

C/C++代码通常是编译成目标文件(object file),然后将目标文件链接一起成为一个可以使用的东西(usable artifact),比如可执行文件或者dll文件。在链接阶段,每个目标文件内的符号引用(symbolic references)会被实际的可执行文件内的内存地址替换。

在java内链接阶段类似,当编译一个java类,所有变量的引用和方法的引用作为符号引用(symbolic reference)存储在类的常量池,符号引用是一个逻辑引用而不是指向真实物理内存的真实引用。

JVM不同实现可以选择什么时候解析符号引用,可以在类加载完成之后的核实阶段(when the class file is verified),这些称之为eager资源或者静态资源(static resolution);也可以在符号引用第一次使用的时候解析,称之为lazy资源或者late资源;

但是JVM必须表现得好像引用第一次用到的时候才出现,并在这一点上可能抛出任何解析错误。绑定(Binding)就是用真实地址引用替换域,方法或者类的符号引用,这个过程只会发生一次,因为符号引用完全被替换。

3.独立于线程的元素

3.1 堆

堆是用来在运行时申请类对象和数组.数组和对象不能存在栈上面,因为在设计上,栈上的帧在创建之后大小是不可变化的(而数组或者容器通常是可变的).帧只保存指向堆上对象和数组的引用.不像局部变量数组(每个帧内)中的私有变量和引用,对象是存在堆上,因此,当方法结束,对象是不会立刻被移除,这些堆对象只有在垃圾回收的时候才会被移除.

为了支持垃圾回收,堆通常划分为3部分: (1).新生代(Young Generation).通常又划分为Eden区和Survivor区. (2).老年代(Old Generation或者Tenured Generation) (3).永久代(Permanent Generation)

3.2 内存管理

除了垃圾回收器回收,对象和数组不会显示的释放其占有的内存.通常工作如下: (1).新的对象和数组在新生代上创建; (2).Minor垃圾回收(Minor garbage collection,即young gc)会在新生代上执行. 新生代中的存活对象会从eden取移到survivor区域. (3).Major垃圾回收(Major garbage collection,即full gc),会将新生代中的存活对象从新生代移到老年代.Major垃圾回收会导致所有应用线程暂停(就是所谓的"Stop the world"). (4).当老年代回收的时候会将永久代回收, 这两者任何一者满了都会导致二者同时被回收.

3.3 非堆内存(Non-Heap Memory)

被认为是JVM结构的逻辑组成部分的对象不是在堆上创建.

非堆内存包括:

(1).永久代(Permanent Generation)

包含:方法区(the method area)和驻留字符串(interned strings)

(2)代码缓存(Code Cache)

用于编译和保存那些被JIT编译成本地代码(native code)的方法.

3.4 即时编译(Just In Time (JIT) Compilation)

JVM主机的CPU会解析java字节码,但是速度没有直接执行本地代码快.为了提升性能,Oracle Hotspot虚拟机会寻找字节码中的经常被执行的热点代码,并将这些代码编译成本地代码.这些本地代码存在代码缓存(Code Cache)中,而代码缓存是在非堆的内存上创建.Hotspot虚拟机通过比较将字节码编译成本地代码花费的额外时间和直接解释字节码的时间,来选择一种最合适的方式.

3.5 方法区(Method Area)

方法区会存储每个类的如下信息: 1. 类加载器引用(Classloader Reference) 2. 运行时常量池(Run Time Constant Pool) 2.1 数字常量(Numeric constants) 2.2 域引用(Field references) 2.3 方法引用(Method References) 2.4 属性(Attributes) 3. 域数据(Field data) 每个域的数据包含:域名(Name),类型(Type),修改器(Modifiers),属性(Attributes) 4. 方法数据(Method data) 每个方法的数据包含:方法名(name),返回值类型(Return type),参数类型(有序)(Parameter Types (in order)),修改器(Modifiers),属性(Attributes). 5. 方法代码(Method code). 每个方法包含:字节码(Bytecodes),操作数栈大小(Operand stack size),本地变量数目(Local variable size),本地变量表(Local variable table)和异常表(Exception table).

其中异常表中包含一些列的异常处理器(exception handler),每个异常处理器包含:起点(start point),终点(end point),处理程序代码的偏移(PC offset for handler code),被捕获异常类的常量池序号(Constant pool index for exception class being caught).

所有的方法共享同一个方法区,因此访问方法区数据和处理动态链接的代码需要是线程安全.如果两个线程同时访问一个类的的未加载的方法或者域,必须保证方法或者域只被加载一次,并且需要保证在加载成功之前两个线程不能出现异常.

3.6 class文件结构

一个编译完成的class文件包含以下结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
    u4          magic;
    u2          minor_version;
    u2          major_version;
    u2          constant_pool_count;
    cp_info     contant_pool[constant_pool_count-1];
    u2          access_flags;
    u2          this_class;
    u2          super_class;
    u2          interfaces_count;
    u2          interfaces[interfaces_count];
    u2          fields_count;
    field_info      fields[fields_count];
    u2          methods_count;
    method_info     methods[methods_count];
    u2          attributes_count;
    attribute_info  attributes[attributes_count];
}

3.6.1 (1).magic

magic指文件的Magic Number(参考前面的文章:http://xiaobaoqiu.github.io/blog/2014/08/26/file-magic-number/),作用就是标记这是一个class文件而不是其他类型的文件;class文件的Magic number非常有趣,叫:ca fe ba be

1
2
xiaobaoqiu@xiaobaoqiu:~$ hexdump -C Conf.class -n20
00000000  ca fe ba be 00 00 00 32  00 40 0a 00 0e 00 29 07  |.......2.@....).|

3.6.2 (2).minor_version, major_version

指编译生成这个class文件的JDK版本号,包括主版本号(major_version)和次版本号(minor_version).JDK版本从十进制的45.0(其中主版本为45,次版本为0,16进制major_version=00 2D,minor_version=00 00)开始.JDK 1.6版本为50.0.

如上面的00 00 00 32表示编译生成Conf.class的major_version为50(00 32),minor_version为0(00 00);

3.6.3 (3).constant_pool

常量池,constant_pool_count表示常量池中常量的数目,contant_pool则表示常量数据,需要注意的是,contant_pool从下标1开始(而不是0),如上面的00 40表示常量池的大小为64,这表示常量池中有63个常量,下标范围是1~63.

3.6.4 (4).access_flags

访问标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public,是否定义为abstract类型,是否被声明为final等.具体标志及意义:

标志名称 标志值 意义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否为final类型,只有类能设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口和抽象类,这个值为真,其余为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生的
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM 0x4000 标志这是一个枚举

3.6.5 (5).this_class, super_class

指向常量池中类全名的索引(index),this_class指当前类的名称的索引,比如指向常量池中的org/jamesdbloom/foo/Bar,而super_class指父类的名称索引,比如指向java/lang/Object.

java中保证每个类都有父类(除了Object类),因此这两个索引都存在.

3.6.6 (6).interfaces_count, interfaces

当前类实现的接口,包括接口数量(interfaces_count)和具体的接口数组(interfaces),interfaces存的内容和this_class类似,也是存储索引,指向常量池中的接口的全称.

3.6.7 (7).fields_count, fields

索引数组,指向常量池中当前类每个字段(域)的完整描述.这里的字段包含类变量(static域)和实例变量.

fields中信息用于描述一个字段(域),包含的字段的作用域(public private protected修饰),是类级变量还是实例级变量(static),可变性(final),并发可见性(volatile),可否序列化(transient修饰符),字段数据类型(基本类型,对象,数组),字段名称.下面给出了字段表的格式

类型 | 名称 | 数量 —- | ——– | — u2 | access_flags | 1 u2 | name_index | 1 u2 | descriptor_index | 1 u2 | attributes_count | 1 attribute_info | attributes | attributes_count 其中access_flags和类的access_flags相似,可以设置的标志和含义如下表:

标志名称 | 标志值 | 意义 —- | —— | —————— ACC_PUBLIC | 0x0001 | 字段是否为public ACC_PRIVATE | 0x0002 | 字段是否为private ACC_PROTECTED | 0x0004 | 字段是否为proteted ACC_STATIC | 0x0008 | 字段是否为static ACC_FINAL | 0x0010 | 字段是否为final ACC_VOLATILE | 0x0040 | 字段是否为volatile ACC_TRANSIENT | 0x0080 | 字段是否为transient ACC_SYNTHETIC | 0x1000 | 字段是否为编译器自动生成 ACC_ENUM | 0x4000 | 字段是否为枚举 name_index和descriptor_index是两个索引值,对常量池的引用,分别代表字段的简单名称即字段的描述符.简单名称即没有类型和参数修饰的字段名称,如m字段的简单名称就是m.描述符则复杂一些,描述符左右是用来描述字段的数据类型,方法的参数列表(包括数量,类型和顺序)以及返回值,根据秒素符的规则,基本数据类型(int,boolean等8种)及void都用一个大写字符来表述,而对象类型则用字符L加上对象全名来表示,详见下表:

标志字符 | 含义 —- | ———– B | 基本类型byte C | 基本类型char D | 基本类型double F | 基本类型float I | 基本类型int J | 基本类型long S | 基本类型short Z | 基本类型bboolean V | void 对于数组,每一维度将使用一个前置的"[“字符描述,如

1
java.lang.String[][]

这样的二维数组,将被记录为

1
[[java/lang/String;

一个整形数组

1
int[]

将被记录为

1
[I

方法的描述符,先参数列表后返回值描述,参数列表按照参数的严格顺序放在遇阻小括号内,方法

1
java.lang.String toString()

的描述符为

1
()Ljava/lang/String

方法

1
int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)

的描述符为

1
([CII[CIII)I

注意字段表集合中不会列出从超类中继承而来的字段,但是可能列出原本java代码中不存在的字段,例如在内部类中为例保持对外部类的访问性,会自动添加指向外部类实例的字段.另外,

3.6.8 (8).methods_count methods

类方法数及指向每个方法签名在常量池中完整描述的索引,如果方法不是abstract或者native,那么常量池中的描述会包含方法的字节码(bytecode).

方法的描述和字段描述类似,包含访问标志,名称索引,描述符索引,属性表集合.见下表:

类型 | 名称 | 数量 —- | ——– | — u2 | access_flags | 1 u2 | name_index | 1 u2 | descriptor_index | 1 u2 | attributes_count | 1 attribute_info | attributes | attributes_count 方法的修饰符没有volatile和transient关键字,增加了synchronized,native,strictfp和abstarct关键字,所有标志为及其取值如下:

标志名称 标志值 意义
ACC_PUBLIC 0x0001 方法是否为public
ACC_PRIVATE 0x0002 方法是否为private
ACC_PROTECTED 0x0004 | 方法是否为proteted
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
ACC_SYNCHRONIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是否为由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICT 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 字段是否为编译器自动生成

关于strictfp:修饰类和方法,意思是精确浮点(strict float point),符合IEEE-754规范的。当一个class用strictfp声明,内部所有的float和double表达式都会成为strictfp的。Interface method不能被声明为strictfp的,class的可以。在Java虚拟机进行浮点运算时,如果没有指定strictfp关键字时,Java的编译器以及运行环境在对浮点运算的表达式是采取一种近似于我行我素的行为来完成这些操作,以致于得到的结果往往无法令你满意。而一旦使用了strictfp来声明一个类、接口或者方法时,那么所声明的范围内Java的编译器以及运行环境会完全依照浮点规范IEEE-754来执行。因此如果你想让你的浮点运算更加精确,而且不会因为不同的硬件平台所执行的结果不一致的话,那就请用关键字strictfp。想了解IEEE-754的,这给个维基百科的链接

方法的代码经过编译器编译成字节码之后,存放在方法属性表集合中的一个名为Code的属性里面.

如果父类方法在在子类没有被重写(override),方法表中就不会出现类子父类的方法信息.但是会出现由编译器自动添加的方法,最典型的便是类构造器"“方法和实例构造器”“方法.

3.6.9 (9).attributes_count attribute_info

属性表(attribute_info),在Class文件,字段表和方法表中都可以携带自己的属性表集合.

Java虚拟机规范中预定义了9项虚拟机应当能识别的属性:

属性名称 使用位置 含义
Code 方法表 | Java代码编译成的字节码
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类,方法表,字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号和字节码的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 源文件名称
Synthetic 类,方法表,字段表 标志方法或者字段为编译器自动生成的

我们编译下面这个简单的类:

1
2
3
4
5
6
7
8
9
package org.jvminternals;

public class SimpleClass {

    public void sayHello() {
        System.out.println("Hello");
    }

}

使用下面这个命令我们就可以得到后续结果:

1
2
xiaobaoqiu@xiaobaoqiu:~/test/com/test$ javac SimpleClass.java
xiaobaoqiu@xiaobaoqiu:~/test/com/test$ javap -v -p -s SimpleClass > SimpleClass.bytecode

SimpleClass的编译结果(即字节码)如下:

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
public class org.jvminternals.SimpleClass
  SourceFile: "SimpleClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "<init>":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V
{
  public org.jvminternals.SimpleClass();
    Signature: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1    // Method java/lang/Object."<init>":()V
        4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      5      0    this   Lorg/jvminternals/SimpleClass;

  public void sayHello();
    Signature: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
        0: getstatic      #2    // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc            #3    // String "Hello"
        5: invokevirtual  #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      9      0    this   Lorg/jvminternals/SimpleClass;
}

我们可以看出,这个字节码主要由三部分组成:常量池,构造函数和sayHello函数.

运行时常量池后续会详细介绍.这里主要介绍方法的内容.每个方法包含四个区域: (1).签名和访问标志位; (2).方法字节码; (3).LineNumberTable 用于为调试器信息告知java代码的行数和字节码指令的对应关系.比如sayHello()中java代码的第6行对应其指令0,第7行对应指令8. (4).LocalVariableTable 列举取当前帧的局部变量,注意,每个非static方法都会带有一个this局部变量.

上面的字节码中涉及的几个指令: (1).aload_0 这个opcode是aload格式一组opcode的一员,他们用于将对象引用载入操作数栈(operand stack),其中代表正在被访问的本地局部变量数组中的位置,只能取0,1,2或者3.还有其他类似命令用于加载非对象类型的数据,如iload, lload, float and dload_,其中i代表int,l带表long,f代表float,d代表double.本地变量下标大于3的能够用iload,lload,float,dload和aload来加载.这些操作都需要一个操作数,代表被加载局部变量的下标. (2).getstatic 这个opcode用于将运行时常量池中的static字段的值压入操作数栈. (3).ldc 这个opcode用于将运行时常量池中的常量(constant)压入操作数栈. (4).invokespecial, invokevirtual 这是调用方法(invoke methods)的opcode组的一员,包括invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual.invokevirutal调用当前对象所属类(比如实际的子类)的方法.invokespecial用于调用当前类(可能为父类)实例的初始化方法和父类的方法.

运行时,invokespecial选择方法基于引用的类型,而不是对象所属的类(即静态绑定),但invokevirtual则选择当前引用的对象(动态绑定).
(5).return
这个opcode是return组的一员,包括ireturn,lreturn,freturn,dreturn,areturn和return.同样i代表int,l带表long,f代表float,d代表double,a代表对应引用,return表示返回void.

构造器包含两个指令,首先将this压入操作数栈,然后调用父类的构造器,父类的构造器会使用this值,染红将this从操作数栈中弹出.示意图如下:

sayHello()方法会复杂一些,因为它需要解决将符号引用(symbolic references)转换为运行时常量池中实际引用(actual references)的问题.第一个指令getstatic用于将非系统级别的静态字段的引用压入操作数栈.下一个指令ldc将字符串Hello压入操作数栈.最后一个指令invokevirtual调用System.out的println方法,它会弹出操作数栈中的Hello作为其参数,然后为当前线程创建一个新的帧.示意图如下:

3.7 类加载器(Classloader)

JVM启动首先使用bootstrap类加载器加载一个初始类(initial class),这个类会在执行static void main(String[])之前被链接(linked)和初始化.main方法的执行也会依次将额外需要的类或者接口加载,链接和初始化.

3.7.1 类加载(Loading)

类加载(Loading)就是通过特定名称找到对应类或者接口的class文件并将其读入一个byte数组,下一步会解析这个byte数组来确认其为一个类对象并且版本(包括majorversions和minor versions)正确.直属具名(named,相对于匿名类)父类或者直属具名接口也会被加载.做完这些之后,一个二进制形式(binary representation)对象就创建完毕.

3.7.2 链接(Linking)

链接(Linking)就是对类或接口进行校验,准备其类型和其直属父类或者父接口.

链接包含三个步骤:校验,准备和解析,其中解析是可选的.

校验就是确保类或者接口结构正确,并且遵守Java和JVM的语法要求,比如下面这些检查: 一致且格式正确的符号表(consistent and correctly formatted symbol table); final方法/类不被重载; 方法访问遵从其访问控制关键字; 方法的参数数目和类型正确; 字节码不会错误的操纵栈 变量在读之前被初始化; 变量的值和其类型相符且值有效; 在校验阶段做这些检查意味着这些检查不需要在运行时执行.在链接阶段做校验会减缓(slows down)类的加载,但是这样避免了在执行阶段执行多次这样的检查.

准备主要是申请静态存储(static storage)和JVM需要的数据结构(比如方法表)的内存.静态字段被创建并初始化为默认值,然而,这个阶段没有初始化器(initializers)或其他代码被执行.

解析是一个可选的阶段,会通过加载被引用的类或者接口来做符号引用的检查,如果这个阶段没有做符号引用的检查,则符号引用检查会被推迟到符号被字节码指令使用之前.

3.7.3 初始化

类或者接口的初始化包括执行类或者接口的初始化方法.

在JVM中有多种classloader,他们扮演不同的角色.除了bootstrap,每个classloader都委托给它的父类来加载它,因为bootstrap是根classloader.

Bootstrap Classloader通常用本地代码实现,因为它在JVM加载之后马上会被实例化.Bootstrap Classloader用来记载基本的Java APIs,比如rt.jar.它只加载根目录(boot classpath)下的类,这些类相比普通类有更高的可信度.因此在加载的过程中跳过了很多加载普通类需要的验证过程.

Extension Classloader用于从标准java扩展API(standard Java extension APIs)中加载类,比如security extension功能.

System Classloader是默认的应用程序classloader,它用于从classpath中加载应用程序的类文件.

用户定义的类加载器(User Defined Classloaders)是用于一些特殊的原因,比如需要在运行时重新加载类,或者需要区分不同组的类加载行为(通常在web服务器比如Tomcat中使用);

3.8 更快的类加载(Faster Class Loading)

从Java 5.0版本开始在HotSpot JMV中引入一个称之为Class Data Sharing(CDS)的特性.在JVM安装过程,安装器会加载一系列的JVM关键类到内存映射共享文档(memory-mapped shared archive),比如rt.jar.CDS会加少加载这些关键类的时间从而提升JVM启动速度,并且允许这些类在不同的JVM实例之间共享,从而降低内存占有.

参考:http://huxi.iteye.com/blog/1072696 http://www-01.ibm.com/support/knowledgecenter/SSYKE2_5.0.0/com.ibm.java.doc.user.zos.50/user/classdatasharing.html http://www-01.ibm.com/support/knowledgecenter/SSYKE2_5.0.0/com.ibm.java.doc.diagnostics.50/diag/understanding/shared_classes.html

3.9 方法区在哪(where Is The Method Area)

书The Java Virtual Machine Specification Java SE 7 Edition中明确陈述:“虽然方法区逻辑上是堆的一部分,简单的JVM实现可能选择不对其进行回收或压缩”.然而,Oracle JVM的jconsole工具显示方法区(包括code cache)不在堆上.OpenJDK的代码显示了CodeCache是VM的对象堆(ObjectHeap)一个单独的领域.

3.10 类加载器引用(Classloader Reference)

所有被加载的类包含一个执行加载它的类加载器的引用.另一方面,类加载器也包含了所有它加载的类的引用.

3.11 运行时常量池(Run Time Constant Pool)

JVM维护了一个各种类型常量的运行时常量池,虽然运行时数据包含了更多的数据,但是它的结构和符号表很相似.Java字节码运行时需要各种数据,通常这些数据会比较大,直接存在字节码中会使得字节码过于臃肿,因此将数据存在常量池,而自己码中保存一份指向常量池相应数据的引用.运行是常量池在动态链接(见上文)的时候使用.

存在常量池中的数据类型包括以下几类: 数字文本(numeric literals); 字符串文本(string literals); 类引用(class references); 字段引用(field references); 方法引用(method references);

比如下面这个代码:

1
Object foo = new Object();

翻译成字节码会是这样:

1
2
3
 0:   new #2        // Class java/lang/Object
 1:   dup
 2:   invokespecial #3    // Method java/lang/Object "<init>"( ) V

new操作之后紧接一个#2操作数,这个操作数是常量池中的一个下标,因此表示引用的是常量池中的第二个数据实体.第二个数据是实体是一个类引用,指向常量池中另外一个实体,这个实体即名称以// Class java/lang/Object开头的类名称.这个符号链接被用于查找类java.lang.Object.new操作用于创建一个类实例并初始化它的变量.然后这个类新实例被加到操作数栈.dup操作复制一份操作数栈的顶部数据项并将其重新加到操作数栈的顶部.最后第2行的字节码使用invokespecial调用实例初始化函数,其操作数也包含一个指向常量池的引用,初始化方法使用(弹出,consumes)操作数栈顶部的引用作为参数.最后,就产生了一个新的引用,指向一个创建且初始化完成的对象.

编译下面这个类:

1
2
3
4
5
6
7
8
9
package org.jvminternals;

public class SimpleClass {

    public void sayHello() {
        System.out.println("Hello");
    }

}

在生成的class文件中,常量池如下:

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
Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "<init>":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V

常量池包含以下类型数据:

Integer 一个4字节的整形常量;
Long    一个8字节的长整形常量;
Float   一个4字节的浮点数常量;
Double  一个8字节的双精度浮点数常量;
String  一个字符串常量,指向常量池中的另外一个utf8实体,实体包含实际的字节;
Utf8    一个UTF8编码的字符序列的字节流;
Class   一个Class类型常量,指向常量池中另外一个utf8实体,这个实体中包含完整的类名称(在动态连接过程中使用);
NameAndType  冒号分割的一对值,每个值都指向常量池中的另外一个实体.第一个值指向一个UTF8字符串实体,这个实体代表方法或者字段的名称.第二个值指向量外一个UTF8实体,这个实体表示一种类型,如果第一个值是字段,则这个类型指完整的类名称;如果如果第一个值是方法,则这个类型指方法参数的完整类名称列表.
Fieldref, Methodref, InterfaceMethodrefA  点号分割的一对值,每个值指向常量池中的另外一个实体;第一个值指向一个Class类型实体,第二个值指向一个NameAndType类型实体;

3.12 异常表(Exception Table)

异常表保存每个异常处理的信息,包括:

起点(Start point)
终点(End point)
异常处理器的偏移代码(PC offset for handler code)
被捕获的异常类在常量池中索引(Constant pool index for exception class being caught)

如果一个方法定义了try-catch或者try-finally这样的异常处理代码,那么就会生成异常表.异常表包含每个异常处理块信息,包括异常处理块应用的代码范围(即try-catch的范围),被处理的异常类型和异常处理代码的地址.

当一个异常被抛出,JVM在当前方法内寻找匹配的处理代码,如果到方法结束也没有发现处理代码,就会将当前栈帧弹出,然后在调用当前方法的方法(新的当前帧)内重新抛出这个异常.如果所有的帧都弹出了还是没有找到异常处理代码,则当前线程被终结(terminated).如果是最后一个非守护线程(non-daemon thread, 比如main线程)因为这个未处理的异常被终结,则JVM也会被终结.Finally处理块匹配所有类型的异常,因此无论什么类型的异常抛出,finally块总是被执行.当没有异常抛出,finally块也会在方法末尾被执行,实现方式是:在return语句被执行之前,跳转到finally代码块执行.

3.13 符号表(Symbol Table)

除了各种类型的运行时常量池,Hotspot 在永久代JVM还有一个符号表.符号表是一个从符号指针(symbol pointers)到符号(symbols)的Hash映射表(形如Hashtable<Symbol*, Symbol>),同时符号表还包含一个指向所有符号(包括在每个类运行时常量池中的符号).

引用计数用于控制什么时候将符号从符号表中移除.比如当一个类的引用被卸载(unloaded),这个类运行时常量池中的所有符号的引用计数减少(decremented).当符号的引用奇数递减到0,符号表知道这个符号不在被引用,因此符号表将其卸载.符号表以及下文介绍的字符串常量表中的所有实体都以一致规范的形式存在,从而提高效率并却保证每个实体只出现一次.

3.14 Interned Strings (String Table)

Java语言规范要求相同的字符串字面值(即包含相同的Unicode码点(Unicode code points)序列),必须指向同一个字符串实例.同时,如果字符串是一个字面值常量,在这个实例上调用String.intern()的返回值必须和字面值常量相同(相同的地址,即==而不仅仅是equals()).

因此下面这个表达式为真:

1
("j" + "v" + "m").intern() == "jvm"

在Hotspot JVM中,interned字符串保存在string table,这个表是一个从对象指针(object pointers)到符号映射(Hashtable<oop, Symbol>)的哈希表,string table也是保存在永久代. 符号表以及字符串常量表中的所有实体都以一致规范的形式存在,从而提高效率并却保证每个实体只出现一次.

在类被加在的时候,编译器自动将字符串字面值变为interned,然后被加到符号表中.另外,String实例能够通过调用String.intern()显示的成为interned.当String.intern()被调用的时候,如果符号表中已经存在则直接返回其引用,如果不存在,则将这个字符串加到String Table中并返回取引用.

4.参考

http://blog.jamesdbloom.com/JVMInternals.html

http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5.6

GraphicsMagick

1.介绍

GraphicsMagick 是个图片处理库,是从ImageMagick 5.5.2分支出来的,但是现在他变得更稳定和更轻、更快一些

GraphicsMagick号称图像处理领域的瑞士军刀。 短小精悍的代码却提供了一个鲁棒、高效的工具和库集合,来处理图像的读取、写入和操作,支持超过88种图像格式,包括重要的DPX、GIF、JPEG、JPEG-2000、PNG、PDF、PNM和TIFF。 通过使用OpenMP可是利用多线程进行图片处理,增强了通过扩展CPU提高处理能力。GraphicsMagick可以再绝大多数的平台上使用,Linux、Mac、Windows都没有问题。

GraphicsMagick支持大图片的处理,并且已经做过GB级别的图像处理实验。GraphicsMagick能够动态的生成图片,特别适用于互联网的应用。可以用来处理调整尺寸、旋转、加亮、颜色调整、增加特效等方面。GaphicsMagick不仅支持命令行的模式,同时也支持C、C++、Perl、PHP、Tcl、Ruby等的调用。

详细见GraphicsMagick官网: http://www.graphicsmagick.org/

2.Ubuntu安装

应该是所有的Unix-like的安装都差不多。

2.1 下载

官网找到最新的release版本的下载地址,这里使用的版本是1.3.20(Released August 16, 2014)。

2.2 安装

下载到本机之后,依次解压,配置,make

1
2
3
4
5
6
7
8
9
xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ sudo tar -xvzf GraphicsMagick-1.3.20.tar.gz

xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ cd GraphicsMagick-1.3.20/

xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ sudo ./configure

xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ sudo make

xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ sudo make install

2.3 验证

验证安装是否成功,gm version

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
xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ gm version
GraphicsMagick 1.3.20 2014-08-16 Q8 http://www.GraphicsMagick.org/
Copyright (C) 2002-2014 GraphicsMagick Group.
Additional copyrights and licenses apply to this software.
See http://www.GraphicsMagick.org/www/Copyright.html for details.

Feature Support:
  Native Thread Safe       yes
  Large Files (> 32 bit)   yes
  Large Memory (> 32 bit)  yes
  BZIP                     no
  DPS                      no
  FlashPix                 no
  FreeType                 yes
  Ghostscript (Library)    no
  JBIG                     no
  JPEG-2000                no
  JPEG                     no
  Little CMS               no
  Loadable Modules         no
  OpenMP                   yes (201107)
  PNG                      yes
  TIFF                     no
  TRIO                     no
  UMEM                     no
  WebP                     no
  WMF                      no
  X11                      yes
  XML                      no
  ZLIB                     yes

Host type: x86_64-unknown-linux-gnu

Configured using the command:
  ./configure 

Final Build Parameters:
  CC       = gcc -std=gnu99
  CFLAGS   = -fopenmp -g -O2 -Wall -pthread
  CPPFLAGS = -I/usr/include/freetype2
  CXX      = g++
  CXXFLAGS = -pthread
  LDFLAGS  = -L/usr/lib
  LIBS     = -lfreetype -lpng12 -lXext -lSM -lICE -lX11 -lz -lm -lgomp -lpthread

从上面也可以看到当前系统支持的图片格式。

2.4 安装常见delegates

GraphicsMagick支持很多格式,但是需要安装额外的delegates,常见的delegates见这里

这里以jpeg为例:

1
2
3
4
5
6
7
8
9
xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ sudo wget ftp://ftp.graphicsmagick.org/pub/GraphicsMagick/delegates/jpegsrc.v9.tar.gz

xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ sudo tar xvfz jpegsrc.v9.tar.gz

xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20/delegates/jpeg-9$ sudo ./configure --enable-shared

xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20/delegates/jpeg-9$ sudo make

xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20/delegates/jpeg-9$ sudo make install

再次重新安装GraphicsMagick,在configure的时候会发现支持jpeg:

1
2
3
4
5
6
7
8
xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ sudo ./configure
...
JPEG v1           --with-jpeg=yes           yes
...

xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ sudo make

xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ sudo make install

碰到的问题:

1
2
xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ gm version
gm: error while loading shared libraries: libjpeg.so.9: cannot open shared object file: No such file or directory

libjpeg.so.9在/usr/local/lib下面,首先尝试使用LDFALGS指定其路径,发现无效。

google到的解决办法:

1
2
xiaobaoqiu@xiaobaoqiu:/usr/local/lib$ sudo ranlib /usr/local/lib/libjpeg.a
xiaobaoqiu@xiaobaoqiu:/usr/local/lib$ sudo ldconfig /usr/local/lib

之后再重新安装GraphicsMagick就正常了。

关于ranlib的作用见:http://blog.csdn.net/yuntongsf/article/details/6284517

关于ldconfig的作用见:http://blog.csdn.net/wooin/article/details/580679

2.5 常用命令

gm常用的命令如下,详细使用(包括例子)见官网手册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xiaobaoqiu@xiaobaoqiu:/usr/local/GraphicsMagick/GraphicsMagick-1.3.20$ gm
GraphicsMagick 1.3.20 2014-08-16 Q8 http://www.GraphicsMagick.org/
Copyright (C) 2002-2014 GraphicsMagick Group.
Additional copyrights and licenses apply to this software.
See http://www.GraphicsMagick.org/www/Copyright.html for details.
Usage: gm command [options ...]

Where commands include: 
    animate - animate a sequence of images
      batch - issue multiple commands in interactive or batch mode
  benchmark - benchmark one of the other commands
    compare - compare two images
  composite - composite images together
    conjure - execute a Magick Scripting Language (MSL) XML script
    convert - convert an image or sequence of images
    display - display an image on a workstation running X
       help - obtain usage message for named command
   identify - describe an image or image sequence
     import - capture an application or X server screen
    mogrify - transform an image or sequence of images
    montage - create a composite image (in a grid) from separate images
       time - time one of the other commands
    version - obtain release version

几个常用的命令如下:

2.5.1 显示图片

1
xiaobaoqiu@xiaobaoqiu:~/Picture/testGraphicsMagick$ gm display house.jpg

原图如下:

2.5.2 原图信息

获取详细图片信息

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
xiaobaoqiu@xiaobaoqiu:~/Picture/testGraphicsMagick$ gm identify -verbose house.jpg 
Image: house.jpg
  Format: JPEG (Joint Photographic Experts Group JFIF format)
  Geometry: 480x300
  Class: DirectClass
  Type: true color
  Depth: 8 bits-per-pixel component
  Channel Depths:
    Red:      8 bits
    Green:    8 bits
    Blue:     8 bits
  Channel Statistics:
    Red:
      Minimum:                     0.00 (0.0000)
      Maximum:                   255.00 (1.0000)
      Mean:                      148.47 (0.5822)
      Standard Deviation:         70.25 (0.2755)
    Green:
      Minimum:                     0.00 (0.0000)
      Maximum:                   250.00 (0.9804)
      Mean:                      128.17 (0.5026)
      Standard Deviation:         58.91 (0.2310)
    Blue:
      Minimum:                     0.00 (0.0000)
      Maximum:                   235.00 (0.9216)
      Mean:                      100.93 (0.3958)
      Standard Deviation:         56.00 (0.2196)
  Filesize: 18.1K
  Interlace: No
  Orientation: Unknown
  Background Color: white
  Border Color: #DFDFDF
  Matte Color: #BDBDBD
  Page geometry: 480x300+0+0
  Compose: Over
  Dispose: Undefined
  Iterations: 0
  Compression: JPEG
  JPEG-Quality: 75
  JPEG-Colorspace: 2
  JPEG-Colorspace-Name: RGB
  JPEG-Sampling-factors: 2x2,1x1,1x1
  Signature: 2bbf2e59864556b88e49e6fcaa7c2a7373fc0b7b28cb7b07dd16904c13ccf328
  Tainted: False
  Elapsed Time: 0:01
  Pixels Per Second: 13.7M

2.5.3 格式转换

需要当前的GraphicsMagick支持的格式,另外支持转换成pdf格式:

1
2
3
xiaobaoqiu@xiaobaoqiu:~/Picture/testGraphicsMagick$ gm convert house.jpg house.png

xiaobaoqiu@xiaobaoqiu:~/Picture/testGraphicsMagick$ gm convert house.jpg house.pdf

png图片如下:

2.5.4 图片缩放

参数包含新的宽高

1
xiaobaoqiu@xiaobaoqiu:~/Picture/testGraphicsMagick$ gm convert -resize 160x100 house.jpg -quality 90 house_160_100.jpg

缩放效果如下:

2.5.5 图片裁剪

参数格式 : 宽x高+左上角x+左上角y

1
xiaobaoqiu@xiaobaoqiu:~/Picture/testGraphicsMagick$ gm convert -crop 200x200+50+50 house.jpg house_cut_200_200.jpg

裁剪效果如下:

2.5.6 图片旋转

参数为旋转角度,正数为顺时针旋转,负数为逆时针旋转

1
2
3
xiaobaoqiu@xiaobaoqiu:~/Picture/testGraphicsMagick$ gm convert -rotate 90 house.jpg house_rotate_90.jpg

xiaobaoqiu@xiaobaoqiu:~/Picture/testGraphicsMagick$ gm convert -rotate -90 house.jpg house_rotate_-90.jpg

旋转效果如下:

2.5.7 添加文字

参数包括字体的样式,大小,添加的位置等

1
xiaobaoqiu@xiaobaoqiu:~/Picture/testGraphicsMagick$ gm convert -font Arial -fill red -pointsize 18 -draw "text 50,50 'baoqiu.xiao'" house.jpg house_sign.jpg

效果如下:

2.5.8 添加水印

1
gm composite -geometry +50+50 -gravity southeast watermark.jpg house.jpg house_watermark.jpg

效果如下:

3. IM4JAVA

GraphicsMagicK提供了常见的几种编程API接口,见这里,但是没有java的接口。常见的两个java支持库是JMagick和IM4JAVA。

3.1 JMagick

JMagick下载地址:http://www.jmagick.org/

JMagick存在的问题是有crash的风险,不稳定(见这里),并且存在图片生成错误的问题(见这里).

3.2 IM4JAVA

官方网站 : http://im4java.sourceforge.net/index.html

下载地址 : http://sourceforge.net/projects/im4java/files/

Maven支持:http://mvnrepository.com/artifact/org.im4java/im4java 示例如下:

1
2
3
4
5
<dependency>
    <groupId>org.im4java</groupId>
    <artifactId>im4java</artifactId>
    <version>1.4.0</version>
</dependency>

简单java示例代码:

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
/**
 * 按照规定的尺寸对图片进行缩放
 *
 * @param srcPath         需要缩放的源文件路径
 * @param destinationPath 生成图片的目标路径
 * @param needWidth       需要缩放的宽度
 * @param needHeight      需要缩放的高度
 * @param quality         图片的质量,0~25:差,50~75:中等,75~100高
 * @return
 */
public static void zoom(String srcPath, String destinationPath, int needWidth, int needHeight, int quality) throws FsCommonException {
    // 创建图片转换命令
    ConvertCmd cmd = new ConvertCmd(true);
    IMOperation op = new IMOperation();
    op.addImage(srcPath);
    op.resize(needWidth, needHeight);
    //设置图片质量参数
    if (quality > 0) {
        op.addRawArgs("-quality", String.valueOf(quality));
    }
    op.addImage(destinationPath);
    try {
        cmd.run(op);
    } catch (Exception e) {
        LOGGER.error("缩放图片失败,输入文件:" + srcPath, e);
        ErrorCodeConstants.throwException(ErrorCodeConstants.CUT_FILE_FAIL);
    }
}

Linux Memory Usage

1.问题

这段时间最易个图片迁移的工作,涉及60W+图片的迁移,用java写的临时代码,部署到图片服务器上。因为服务器还有其他服务,看日志的同时,也再看迁移的应用对服务器CPU和内存使用的影响。使用top命令猛然发现Mem的used狂涨而free狂降,下面是两个时刻的top情况:

1
2
3
4
5
6
7
8
9
10
11
top - 15:41:56 up 176 days, 19:46,  2 users,  load average: 0.34, 0.32, 0.28
Tasks: 521 total,   1 running, 520 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.1%us,  0.1%sy,  0.0%ni, 98.6%id,  1.2%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:  66057208k total, 57548808k used,  8508400k free,  1154764k buffers
Swap: 50331640k total,        0k used, 50331640k free, 47054444k cached

top - 15:43:45 up 176 days, 19:48,  2 users,  load average: 0.25, 0.30, 0.27
Tasks: 521 total,   1 running, 520 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.2%us,  0.0%sy,  0.0%ni, 99.7%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:  66057208k total, 57941160k used,  8116048k free,  1154764k buffers
Swap: 50331640k total,        0k used, 50331640k free, 47448656k cached

2.疑惑

第一想法是内存泄漏了,之前写惯C++代码Windows下碰到过一次明显的内存泄漏,情况和这个完全类似。

但是这个是java代码,java想写出内存泄漏的代码不容易。

3.原因

问了个老鸟,原因是Linux的内存管理和Windows不一样。在Linux下,无论物理内存有多大都将其充份利用,会将应用程序调用过的硬盘数据保留在内存,利用内存读写的高速特性来提高Linux系统的数据访问性能。

3.1 linux中内存是如何使用

当有应用需要读写磁盘数据时,由系统把相关数据从磁盘读取到内存,如果物理内存不够,则把内存中的部分数据导入到磁盘,从而把磁盘的部分空间当作虚拟内存来使用,也称为Swap。如果给所有应用分配足够内存后,物理内存还有剩余,linux会尽量再利用这些空闲内存,以提高整体I/O效率,其方法是把这部分剩余内存再划分为cache及buffer两部分加以利用。

从磁盘读取到内存的数据在被相关应用程序读取后,如果有剩余内存,则这部分数据会存入cache,以备第2次读取时,避免重新读取磁盘。当一个应用程序在内存中修改过数据后,因为写入磁盘速度相对较低,在有空闲内存的情况下,这些数据先存入buffer,在以后某个时间再写入磁盘,从而应用程序可以继续后面的操作,而不必等待这些数据写入磁盘的操作完成。

如果在某个时刻,系统需要更多的内存,则会把cache部分擦除,并把buffer中的内容写入磁盘,从而把这两部分内存释放给系统使用,这样再次读取cache中的内容时,就需要重新从磁盘读取了。

通过以上分析可以得知,Mem中的空闲物理内存(free)不多,不一定表示系统运行状态很差,因为内存的cache及buffer部分可以随时被重用,在某种意义上,这两部分内存也可以看作是额外的空闲内存。

4.Linux和Windows内存管理的差异

无论物理内存有多大,Linux都将其充份利用,将一些程序调用过的硬盘数据读入内存,利用内存读写的高速特性来提高Linux系统的数据访问性能。

而Windows 是只在需要内存时,才为应用程序分配内存,并不能充分利用大容量的内存空间。

换句话说,每增加一些物理内存,Linux 都将能充分利用起来,发挥了硬件投资带来的好处,而Windows只将其做为摆设,即使增加8GB甚至更大。

5.什么是Linux的cache,buffers,Swap

Cached表示page cache大小,Buffers表示内存中块I/O缓冲(buffer cache)的大小。通常而言,Cached很关键,Buffers则影响不大。

简单来说,磁盘的操作有逻辑级(文件系统)和物理级(磁盘块),page cache对应的就是用来缓存逻辑级数据,buffer cache用来缓存物理级数据。

假设我们通过文件系统操作文件,那么文件将被缓存到Page Cache,如果需要刷新文件的时候,page cache将交给buffer cache去完成,因为buffer cache就是缓存磁盘块的。 也就是说,直接去操作文件,那就是page cache区缓存,用dd等命令直接操作磁盘块,就是buffer cache缓存的东西。

5.1 buffer

buffer是作为buffer cache的内存,是块设备的读写缓冲区。

buffer cache也叫块缓冲,由物理内存分配,是对物理磁盘上的一个磁盘块(磁盘的组织单位)进行的缓冲。

设立buffer cache的目的是为在程序多次访问同一磁盘块时,减少访问时间。系统将磁盘块首先读入buffer cache,如果cache空间不够时,会通过一定的策略将一些过时或多次未被访问的buffer cache清空。程序在下一次访问磁盘时首先查看是否在buffer cache找到所需块,命中可减少访问磁盘时间,不命中时需重新读入buffer cache。

对buffer cache 的写分为两种,一是直接写,这是程序在写buffer cache后也写磁盘,要读时从buffer cache 上读,二是后台写,程序在写完buffer cache 后并不立即写磁盘,因为有可能程序在很短时间内又需要写文件,如果直接写,就需多次写磁盘了,这样效率很低,而是过一段时间后由后台写(批量写),减少了多次访磁盘的时间。

buffer cache 是由物理内存分配,linux系统为提高内存使用率,会将空闲内存全分给buffer cache ,当其他程序需要更多内存时,系统会减少cahce大小。

5.2 cache

cache是page cache的内存, 文件系统的cache。

page cache也叫页缓冲或文件缓冲,文件读取是由外存上不连续的几个磁盘块,到buffer cache,然后组成page cache,然后供给应用程序。

page cache在linux读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问。具体说是加速对文件内容的访问,buffer cache缓存文件的具体内容——物理磁盘上的磁盘块,这是加速对磁盘的访问。

5.3 Swap

swap space交换空间,是虚拟内存的表现形式。系统为了应付一些需要大量内存的应用,而将磁盘上的空间做内存使用,当物理内存不够用时,将其中一些暂时不需的数据交换到交换空间。

做虚拟内存的好处是让进程以为好像可以访问整个系统物理内存。因为在一个进程访问数据时,其他进程的数据会被交换到交换空间中。

6.Linux内存常用命令

6.1 top

1
2
3
4
5
6
7
8
9
top - 16:13:52 up 1 day,  6:01, 16 users,  load average: 2.89, 2.27, 2.14
Tasks: 274 total,   7 running, 266 sleeping,   0 stopped,   1 zombie
%Cpu(s): 44.3 us, 16.6 sy,  0.0 ni, 38.4 id,  0.6 wa,  0.0 hi,  0.1 si,  0.0 st
KiB Mem:   8047584 total,  7905356 used,   142228 free,    73476 buffers
KiB Swap: 15879164 total,    26872 used, 15852292 free,  1962736 cached

PID USER      PR  NI  VIRT  RES  SHR S  %CPU %MEM    TIME+  COMMAND
29121 xiaobaoq  20   0 3938m 2.1g 2.1g S  34.2 27.7 119:28.33 VirtualBox
2548 xiaobaoq  20   0 1371m  79m  19m R  31.2  1.0 131:40.03 compiz

字段的意义如下:

1
2
3
4
5
6
7
8
9
10
11
12
PID:进程的ID
  USER:进程所有者
  PR:进程的优先级别,越小越优先被执行
  NInice:值
  VIRT:进程占用的虚拟内存
  RES:进程占用的物理内存
  SHR:进程使用的共享内存
  S:进程的状态。S表示休眠,R表示正在运行,Z表示僵死状态,N表示该进程优先值为负数
  %CPU:进程占用CPU的使用率
  %MEM:进程使用的物理内存和总内存的百分比
  TIME+:该进程启动后占用的总的CPU时间,即占用CPU使用时间的累加值。
  COMMAND:进程启动命令名称

top命令提供了额外的输入,常用的命令包括(注意是大写):

1
2
3
  P:按%CPU使用率排行
  T:按MITE+排行
  M:按%MEM排行

6.2 free

1
2
3
4
5
xiaobaoqiu@xiaobaoqiu:~$ free -m
             total       used       free     shared    buffers     cached
Mem:          7858       7687        171          0         71       1889
-/+ buffers/cache:       5726       2132
Swap:        15506         26      15480

-m表示单位为M,可以使用-g表示单位为G,默认为K。 -/+ buffers/cache表示物理内存的缓存统计,Used为4506,即实际使用的buffers与cache 总量,也是实际使用的内存总量;Free为3352, 即未被使用的buffers与cache和未被分配的内存之和,这就是系统当前实际可用内存。

Swap表示硬盘上交换分区的使用情况。只有mem被当前进程实际占用完,即没有了buffers和cache时,才会使用到swap。

注意一下几个关系: Free(-/+ buffers/cache行)= Free(Mem)+buffers(Mem)+Cached(Mem)

Used(Mem) = Used(-/+ buffers/cache)+ buffers(Mem) + Cached(Mem)

total(Mem) = used(-/+ buffers/cache) + free(-/+ buffers/cache)

从应用程序角度来看,对于应用程序来说,buffers/cached 是等于可用的,因为buffer/cached是为了提高文件读取的性能,当应用程序需在用到内存的时候,buffer/cached会很快地被回收。所以从应用程序的角度来说,可用内存=系统free memory+buffers+cached.

6.3 /proc/meminfo

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
xiaobaoqiu@xiaobaoqiu:~$ cat /proc/meminfo
MemTotal:        8047584 kB
MemFree:          162416 kB
Buffers:           73772 kB
Cached:          1943380 kB
SwapCached:         3860 kB
Active:          3636032 kB
Inactive:        1608472 kB
Active(anon):    3092468 kB
Inactive(anon):  1121848 kB
Active(file):     543564 kB
Inactive(file):   486624 kB
Unevictable:          48 kB
Mlocked:              48 kB
SwapTotal:      15879164 kB
SwapFree:       15852292 kB
Dirty:               664 kB
Writeback:             0 kB
AnonPages:       3223572 kB
Mapped:          2597276 kB
Shmem:            986964 kB
Slab:             311976 kB
SReclaimable:     247580 kB
SUnreclaim:        64396 kB
KernelStack:        6808 kB
PageTables:        61012 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    19902956 kB
Committed_AS:   11365592 kB
VmallocTotal:   34359738367 kB
VmallocUsed:      360776 kB
VmallocChunk:   34359367876 kB
HardwareCorrupted:     0 kB
AnonHugePages:         0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:       71156 kB
DirectMap2M:     8187904 kB

其中的MemTotal, MemFree, Buffers, Cached, SwapTotal, SwapFree和free命令中的值对应起来。

Cached is the size of the page cache。 Buffers is the size of in-memory block I/O buffer。

6.4 vmstat

vmstat -s显示内存使用的统计信息

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
xiaobaoqiu@xiaobaoqiu:~$ vmstat -s
      8047584 K total memory
      7841504 K used memory
      3624652 K active memory
      1584740 K inactive memory
       206080 K free memory
        69252 K buffer memory
      1890844 K swap cache
     15879164 K total swap
        39488 K used swap
     15839676 K free swap
      8744974 non-nice user cpu ticks
          345 nice user cpu ticks
      4309209 system cpu ticks
     29496503 idle cpu ticks
       240289 IO-wait cpu ticks
           19 IRQ cpu ticks
        14705 softirq cpu ticks
            0 stolen cpu ticks
      7878551 pages paged in
     18640168 pages paged out
          976 pages swapped in
         9899 pages swapped out
    664925325 interrupts
   1290574510 CPU context switches
   1409739162 boot time
        36758 forks

参考

  1. http://www.linuxintheshell.org/2012/06/05/episode-008-free-understanding-linux-memory-usage/

Form Data vs Request Payload

HTTP请求中的form data和request payload的区别

AJAX Post请求中常用的两种传参数的形式:form data 和 request payload

Form data

get请求的时候,我们的参数直接反映在url里面,形式为key1=value1&key2=value2形式,比如:

1
http://news.baidu.com/ns?word=NBA&tn=news&from=news&cl=2&rn=20&ct=1

如果是post请求,那么表单参数是在请求体中,也是以key1=value1&key2=value2的形式在请求体中。通过chrome的开发者工具可以看到如下:

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
RequestURL:http://127.0.0.1:8080/test/test.do
Request Method:POST
Status Code:200 OK
 
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip,deflate,sdch
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6
AlexaToolbar-ALX_NS_PH:AlexaToolbar/alxg-3.2
Cache-Control:max-age=0
Connection:keep-alive
Content-Length:25
Content-Type:application/x-www-form-urlencoded
Cookie:JSESSIONID=74AC93F9F572980B6FC10474CD8EDD8D
Host:127.0.0.1:8080
Origin:http://127.0.0.1:8080
Referer:http://127.0.0.1:8080/test/index.jsp
User-Agent:Mozilla/5.0 (Windows NT 6.1)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.149 Safari/537.36
 
Form Data
name:mikan
address:street
 
Response Headers
Content-Length:2
Date:Sun, 11 May 2014 11:05:33 GMT
Server:Apache-Coyote/1.1

这里要注意post请求的Content-Type为application/x-www-form-urlencoded(默认的),参数是在请求体中,即上面请求中的Form Data。

前端:

1
2
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xhr.send("name=foo&value=bar");

在servlet中,可以通过request.getParameter(name)的形式来获取表单参数。

1
2
3
4
5
6
7
8
9
10
/**
 * 获取httpRequest的参数
 * 
 * @param request
 * @param name
 * @return
 */
protected String getParameterValue(HttpServletRequest request, String name) {
    return StringUtils.trimToEmpty(request.getParameter(name));
}

Request payload

如果使用原生AJAX POST请求的话,那么请求在chrome的开发者工具的表现如下,主要是参数在

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
Remote Address:192.168.234.240:80
Request URL:http://tuanbeta3.XXX.com/qimage/upload.htm
Request Method:POST
Status Code:200 OK

Request Headers
Accept:application/json, text/javascript, */*; q=0.01
Accept-Encoding:gzip,deflate,sdch
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6
Connection:keep-alive
Content-Length:151
Content-Type:application/json;charset=UTF-8
Cookie:JSESSIONID=E08388788943A651924CA0A10C7ACAD0
Host:tuanbeta3.XXX.com
Origin:http://tuanbeta3.XXX.com
Referer:http://tuanbeta3.XXX.com/qimage/customerlist.htm?menu=19
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36
X-Requested-With:XMLHttpRequest

Request Payload
[{widthEncode:NNNcaXN, heightEncode:NNNN5NN, displayUrl:201409/03/66I5P266rtT86oKq6,…}]

Response Headers
Connection:keep-alive
Content-Encoding:gzip
Content-Type:application/json;charset=UTF-8
Date:Thu, 04 Sep 2014 06:49:44 GMT
Server:nginx/1.4.7
Transfer-Encoding:chunked
Vary:Accept-Encoding

注意请求的Content-Type为application/json;charset=UTF-8,而请求表单参数在Request Payload中。

后端获取(这里使用org.apache.commons.io.):

1
2
3
4
5
6
7
8
9
10
/**
 * 从 request 获取 payload 数据
 *
 * @param request
 * @return
 * @throws IOException
 */
private String getRequestPayload(HttpServletRequest request) throws IOException {
    return IOUtils.toString(request.getReader());
}

二者区别

参考:http://stackoverflow.com/questions/10494574/what-is-the-difference-between-form-data-and-request-payload

if a request (typically POST) has Content-type header set to application/x-www-form-urlencoded the body is expected to be in the form of a standard querystring with url-encoded key=value pairs joined by &. Form data section then shows the key-value parameters (when viewed parsed). This way was much more common in past because it is a default for HTML forms.

other cases are shown in Request payload section (and nowadays parsed for readability as well for common formats like JSON).

如果请求的Content-Type设置为application/x-www-form-urlencoded,那么这个Post请求被认为是HTTP POST表单请求,参数出现在

其他情况如使用原生AJAX的POST请求如果不指定请求头Request Header,默认使用的Content-Type是text/plain;charset=UTF-8,参数出现在Request payload块。