2012年10月30日星期二

JVM原理简述-GC

GC

        GC-垃圾回收是一种动态存储管理技术,它可以自动释放那些不再被使用的对象,按照特定的算法来实现资源自动回收。

垃圾收集方式

引用计数

        堆中的每个对象对应一个引用计数器,每当创建或重新赋值的时候,引用计数器就+1,而当对象出了作用域之后,计数器-1,直到计数器为0,就达到了被回收的条件。
        引用计数方式速度较快,不会产生停顿。但是开销较大,因为每个对象都对应了一个计数器。

对象引用遍历

        从根集开始扫描,识别出哪些对象可达,哪些不可到达。并利用某种方式标记可达对象。

垃圾收集算法

复制

        找到活动对象并拷贝到新的空间。这种算法适合存活对象较少的情况,内存开销较大。

标记清除

        从根开始扫描,将活动对象标记。然后再扫描未标记的对象,一次清除。此方法的优点是不需要移动对象,仅对不存活对象操作,适合存活对象较多的情况,容易产生内存碎片。

标记压缩

        标记清除的基础上,移动活动对象。缺点是成本较高,优点是没有内存碎片。

分代回收

        GC是对JVM堆中的对象进行回收,在上一篇博客中,hotspot堆分为young,old和perm三部分。新生对象首先进入young区,随着young区不断的增加,一些老的对象将被移到old区。而perm区主要存储了java class的类型信息和静态变量等。

youngGC

即年轻代垃圾回收,当创建新对象,Eden空间不足时,触发youngGC。有三种选择
1. 串行GC
将Eden中存活的对象移到From->To->Old.适用于单线程,单cpu,新生代较小,要求不高的应用。在client模式或32位机器下默认选择。
启用:-XX:+UseSerialGC
2. 并行回收GC,Parallel Scavenge
多线程执行,适应多CPU,要求较高的应用。在server模式下是默认的。启用:-XX:+UseParallelGC, 在8核心以下的机器上默认为cpu核心数,大于8核,则默认为3+(核心数*5)/8.也可以通过-XX:ParallelGCThreads=8指定。
3. ParNew GC
需配合旧生代使用CMSGC。启用:-XX:+UseParNewGC.可以通过-XX:DisableExplicitGC禁用。

OldGC

1. 旧生代串行GC
        算法:标记清除和标记压缩。采用单线程执行,耗时较长,需要暂停应用。client模式或32位机默认采用这种模式。-XX:+PrintGCApplicationStoppedTime可以查看应用暂停时间。
2. 旧生代并行GC
        算法:标记压缩算法。采用多线程方式,暂停时间减少。server模式默认采用这种回收方式。
3. 旧生代并发GC
        采用标记清除算法。对GC进行并发执行,大大缩短应用暂停时间,但是整体GC时间会加长。启用方式:-XX:+UseConcMarkSweepGC。可以通过-XX:ParallelCMSThreads=10指定并发线程数。通过-XX:+CMSClassUnloadingEnabled来启用持久代CMS.
        垃圾整理:使用-XX:UseCMSCompactAtFullCollection每次fullGC后都会启动垃圾整理.-XX:CMSFullGCsBeforeCompaction=2表示2次fullGC后开始整理。

fullGC

旧生代和持久代GC时,即是对新生代,旧生代持久代都GC,FullGC。
执行时,先对新生代GC,可以通过-XX:ScanengeBeforeFullGC禁止fullGC时对新生代GC,然后对旧生代持久代进行GC。
触发时机:
1. system.gc,可以通过-XX:DisableExplicitGC禁用。
2. 旧生代空间不足
3. 持久代空间满
4. CMSGC时出现promotion failed
5. 统计youngGC后要移到旧生代的对象大于旧生代剩余空间

查看JVM使用的GC算法

找到java进程ID,使用jmap命令:
jmap -heap {pid}
会打印出JVM的基本信息,包括分区信息,回收算法等。
打印之前先看一下启动参数:
java -server -Xms512m -Xmx512m -XX:NewSize=100m -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=58 -XX:PermSize=64m -XX:MaxPermSize=64m -XX:ThreadStackSize=512

下面是jvm-heap信息:
Attaching to process ID 31410, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 20.8-b03

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 536870912 (512.0MB)
   NewSize          = 104857600 (100.0MB)
   MaxNewSize       = 283508736 (270.375MB)
   OldSize          = 314572800 (300.0MB)
   NewRatio         = 7
   SurvivorRatio    = 8
   PermSize         = 67108864 (64.0MB)
   MaxPermSize      = 67108864 (64.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 94371840 (90.0MB)
   used     = 7155464 (6.823982238769531MB)
   free     = 87216376 (83.17601776123047MB)
   7.582202487521702% used
Eden Space:
   capacity = 83886080 (80.0MB)
   used     = 6906936 (6.586967468261719MB)
   free     = 76979144 (73.41303253173828MB)
   8.233709335327148% used
From Space:
   capacity = 10485760 (10.0MB)
   used     = 248528 (0.2370147705078125MB)
   free     = 10237232 (9.762985229492188MB)
   2.370147705078125% used
To Space:
   capacity = 10485760 (10.0MB)
   used     = 0 (0.0MB)
   free     = 10485760 (10.0MB)
   0.0% used
concurrent mark-sweep generation:
   capacity = 432013312 (412.0MB)
   used     = 7523480 (7.174949645996094MB)
   free     = 424489832 (404.8250503540039MB)
   1.7414926325233238% used
Perm Generation:
   capacity = 67108864 (64.0MB)
   used     = 39960504 (38.10930633544922MB)
   free     = 27148360 (25.89069366455078MB)
   59.545791149139404% used

2012年10月24日星期三

Nginx做文件缓存原理

        关于反向代理不多赘述,Nginx是一个高性能的http服务器,可以通过配置Nginx缓存来实现静态文件访问加速。

http304原理

        在使用浏览器访问的时候,经常可以看到一个状态值304返回。304的英文表示是Not Modified,即文件未被修改,而这个值是通过http server返回给客户端的。以Nginx为例,当返回304时,并未产生任何的数据下载。
Request URL:http://s1.t.itc.cn/mblog/pic/20122_21_9/92542601162191005.jpg
Request Method:GET
Status Code:304 Not Modified

        从截图上可以看到只有228B字节的返回,这只相当于http header的大小,而图片则不需要被重新下载传输,也就是通过这个会加快网页的加载速度。

        实现304的原理关键在于两个http header。分别是Last-Modified和If-Modified-Since。
        http server在首次收到图片访问请求的时候,会向浏览器端返回Last-Modified这个header,标示当前文件的上次修改日期。
Cache-Control:max-age=86400
Connection:keep-alive
Date:Wed, 24 Oct 2012 08:56:16 GMT
Expires:Thu, 25 Oct 2012 08:56:16 GMT
Last-Modified:Wed, 24 Oct 2012 07:49:33 GMT
Server:nginx/0.8.54
        浏览器收到这个参数后,再次请求时,会带上If-Modified-Since参数传递给服务器。http server收到这个参数,会和本地缓存中的Last-Modified比较,如果一致则返回http304.
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:GBK,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:zh-CN,zh;q=0.8
Cache-Control:max-age=0
Connection:keep-alive
Host:t.itc.cn
If-Modified-Since:Wed, 24 Oct 2012 07:49:33 GMT
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1

Nginx判断是否返回304的原理

nginx的缓存目录会缓存访问过的文件,这些文件的格式如下:
 [1m~JP^@^@^@^@……………………[1m~Y~GP^@^@^@^@~_^F [1m^@^@[1m^@[1m^A-cache
KEY: t.itc.cn/mblog/pic/20122_21_9/92542601162191005.jpg
HTTP/1.0 200 OK^M
Server: Resin/3.1.9^M
Last-Modified: 23 Oct 2012 09:40:38 GMT^M
ETag: BExxWmSgiYb1234^M
Accept-Ranges: bytes^M
Expires: Fri, 26 Oct 2012 07:33:00 GMT^M
Content-Type: image/jpeg^M
Content-Length: 7810^M
Date: Wed, 24 Oct 2012 07:32:59 GMT^M
^M
…[1m^@^PJFIF^@^A^A^A^@H^@H^@^@…[1m^@C^@^H^F^F^G^F^E^H^G^G^G               ^H
缓存文件中存储了缓存的主键key,和header值,通过匹配缓存中的Last-Modified来达到是否返回http 304的判断。如果没有缓存,则获取文件在源站的修改时间作为Last-Modified值。如果是接口处理后返回的文件,需要接口自己设置Last-Modified值。

简单配置Nginx缓存

proxy_temp_path /var/tmp/nginx/proxy;
proxy_cache_path /var/tmp/nginx/cache levels=1:2 keys_zone=cache_one:20m inactive=1d max_size=5g;
 
server {
 
 listen  80;
 server_name localhost;
 
 root /var/www;
 
 location ~ \.(jpg|png|jpeg|gif|css|js)$ {
  proxy_cache cache_one;
  proxy_cache_valid 200 304 12h;
  proxy_cache_key $host$uri$is_args$args;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-For $remote_addr;
  proxy_pass http://127.0.0.1:8181;
  proxy_set_header X-Forwarded_For $proxy_add_x_forwarded_for;
  expires 1d;
 }
}

2012年10月18日星期四

Java对象序列化

什么是序列化

        序列化是指将一个对象转换为一个一连串的字节描述的过程。而反序列化是将这个一连串的字节数据转化为对象的过程。在一个分布式的环境中,经常需要将一个Object从一端传递到网络的另外一端,此时就需要在发送端序列化之后,经过网络将字节流传递到另一端,对端再将字节流转换为对象。

场景

持久化存储,将对象持久化为流存储到对象或缓存中;
远程过程调用,将序列化之后的流经过网络传输解析;而对开发人员来说,底层的控制协议都被屏蔽了,它们看到了一个在同一个虚拟机内调用的效果。

如何序列化对象

        只要实现java.io.Serializable接口的对象都可以被JVM序列化,java.io.Serializable是一个标记接口,没有定义任何方法,它只是为了声明一个对象是可以被序列化的。例如:
public class User implements Serializable{
  private long id;
  private String name;
}
Java中,ObjectOutputStream和ObjectInputStream负责序列化java对象,通过ObjectOutputStream的writeObject方法来将一个对象写入到流中,通过ObjectInputStream的readObject方法将一个对象从流中读出,例如:
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bo);
out.writeObject(test);
byte[] bytes = bo.toByteArray();
out.flush();
   
ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(bytes));
KryoTest s = (KryoTest)input.readObject();
AssertUtil.assertNotNull(s);
input.close();
默认对象所有的非静态和非瞬时的域都会被包含进来,而与域的声明没有关系。此时有两个办法来保证某些域不被序列化。第一种就是使用Transient关键字,将域声明为瞬时的,此时序列化将不会包含瞬时域。另外一种方式是通过serialPersistentFields来声明要被序列化的域,例如如果只序列化Person的name域:
private static final ObjectStreamField[] serialPersistentFields = { 
    new ObjectStreamField("name", String.class) 
};  

自定义序列化

自定义序列化是通过在对象中声明四个私有方法来完成的。这四个方法分别是:

private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private Object readResolve() throws ObjectStreamException;
private Object writeReplace() throws ObjectStreamException;
通过writeObject和readObject方法可以自定义序列化逻辑,例如额外序列化一个字段等。
而通过writeReplace和readResolve方法可以在写对象前和读取流之后对操作对象做自定义修改。这四个方法的执行顺序是writeReplace->writeObject->readObject->readResolve

其他序列化工具

java的序列化性能一直都不被恭维。而当前市面上也有很多开源的序列化工具,例如kyro,protostuff等,这些开源的序列化工具的性能大大超过了java本身的序列化性能。
以kyro举例,将同一个对象序列化反序列化50000次,得出的耗时时间比:

Output output = new Output(1, 4096);
kryo.writeObject(output, test);
byte[] bb = output.toBytes();
output.flush();
   
Input input = new Input(bb);
KryoTest s = (KryoTest) kryo.readObject(input, KryoTest.class);
AssertUtil.assertNotNull(s);
input.close();
耗时时间:654:2056(ms)

2012年10月9日星期二

JVM原理简述

什么是JVM?

         JVM是Java Virtual Machine(java虚拟机)的简称。它是一种规范。通过在实际计算机上仿真模拟各种计算功能来实现的。Java虚拟机有自己完善的硬件体系架构,例如处理器,堆栈,寄存器等,还有相应的指令系统。由于JAVA虚拟机屏蔽的具体操作系统平台的相关信息,使得java程序只需要生成在java虚拟机上运行的代码(字节码),就可以在多种不同平台上运行。而字节码又经过java虚拟机解释成具体平台的机器指令来执行。

什么是字节码?

          ByteCode指通过编译生成,但是与机器码无关,通常要经过解释器转义后才可以成为机器码的中间代码。编译器将特定的源码编译为字节码,而字节码又经过虚拟机转义成为可以直接运行的指令。

JVM的执行过程

编译

        由java编译器将源码编译为字节码(class文件)
        编译过程如下:
        1. 词法分析,扫描类名,变量和函数
        2. 语法分析
        3. 注解处理
        4. 语义分析,名字,表达式,变量,方法,类型,参数等一系列检查
        5. 生成字节码文件(class)

装载

        首先,由类装载器(classloader)装载class文件。
        1. BootStrapClassLoader装载jdk/lib下和Xbootclasspath指定的所有jar包;
        2. ExtClassLoader装载jdk/lib/ext下和-Djava.ext.dirs指定的所有jar包
        3. 由AppClassLoader装载-classpath和-Djava.class.path指定的目录和jar;
        4. 自定义类装载器,运行时动态加载class文件。
       注:装载时会进行验证,如果找不到class文件会抛出classNotFoundException
       装载完成后,会进行字节码校验:
       1. FileFormat-VerifyError
       2. 引用类:NoClassDefFoundError
       3. 变量:NoSuchFieldError
       4. 方法:NoSuchMethodError
       5. 权限
       检查完毕后,开始初始化class。
       jvm中执行代码有两种方式:1. 解释执行,即由jvm中的java解释器将字节码解释为不同操作系统平台的指令执行;2. 编译执行,将字节码编译为机器码执行。这个编译过程由JVM中的JIT完成。只有使用频率高的代码才会被JIT编译为机器码执行,以提高效率。

JVM内存区域

由图1中可以看出,JVM内存区分为1. 方法区,2. 堆,3. 栈,4. 程序计数器,5. 本地方法栈。

方法区(Perm区)

        由classloader加载的class的类型信息全部存储在方法区中。所有的静态变量也存在于方法区中。方法区是所有线程共享的,因此方法区是线程安全的。方法区是内存永久保存区域,当perm空间不足时,会引发full GC,GC之后还不足,会抛出permGen space不足的错误。-XX:PermSize, -XX:MaxPermSize来指定方法区的最小和最大值。
        主要存储内容:
        1. 此类型的全限定名
        2. 此类型的超类的全限定名
        3. 此类型是接口还是类
        4. 此类型的访问修饰符号(public,private等)
        5. 此类型的常量池,字段,方法信息
        6. 除常量意外的所有静态变量
        7. 一个到classloader的引用
        8. 一个到class的引用

        JVM的堆中存在了所有类的实例和所有数组。一个JVM只有一个堆空间,各个线程都共享这个空间。
        图中可以看出,堆分为young区和old区,一般young和old的配置比例为1:m。
        young区保存大部分新对象,便于高效回收。其中Eden区存储新创建的对象,Eden满的时候,会有一次小范围的youngGC,将Eden中依然存活的对象存储至from,当from满时,此区域的活动对象会移动到to,直到from/to区域都满的时候,依然存活的对象会移到old区。-Xmn设置新生代大小。-XX:NewSize和-XX:MaxNewSize设置young的最小值和最大值;-XX:NewRatio=m设置young:old=1:m。-XX:SurvivorRatio=8设置Eden和from/to的比例为8:1:1
        old区保存存活时间较长的对象,当整个新生代满的时候,会触发一次full GC,将新生代的存活对象移到old。通过-Xmx-Xmn设置旧生代大小。
-Xmx和-Xms来指定heap的最大和最小值,避免JVM动态调整则设置为同一个值即可。

程序计数器

每一个线程都有自己的程序计数器,里面存放了下一条被执行指令的地址。

        每一个线程被创建的时候,都会得到自己的程序计数器和java栈。java栈以帧为单位保存调用信息。当线程调用一个方法,JVM会向栈中压入一个新的栈帧,当方法调用结束时,栈帧会被弹出。java栈帧是线程私有的,不存在多线程的安全性问题。
        栈帧由3部分构成,局部变量区,操作数栈,帧数据区。
        1. 局部变量区
        编译器将调用方法的局部变量和参数按照声明顺序放入局部变量数组中,从0开始计数。如果是实例方法,this将被存储到数组的第一个位置。java中的对象传递都是按照引用传递的,因此java对象全部存储在堆中,而局部变量区和操作数栈中只存储了对象的引用。
        2. 操作数栈
        JVM把操作数栈作为它的工作区,大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈,然后等相关的指令将结果再次弹出。操作数栈扮演了暂存操作数的角色。
        3. 帧数据区
        栈帧还可以存储常量池的解析,正常方法返回和异常派发机制。这些都存在帧数据区。当方法要访问常量池时,会通过帧数据区的指向常量池的指针来访问。
        如果方法正常返回,jvm会将返回值压入调用方法的栈帧的操作数栈中。
        为了处理异常,帧数据区还保存了一个对异常表的引用。

本地方法栈

        主要是为JVM使用本地方法而服务的。线程调用本地方法时,JVM会保持java栈不变,不再压入新的栈帧。在sun的hotspotJVM中,本地方法栈和栈是合二为一的。

垃圾回收

放在下一篇文章中讨论。