什么是序列化

java 序列化是将对象转化为二进制流。不同的序列化框架会将对象转成不同的二进制流。通过 透过byte数组简单分析Java序列化、Kryo、ProtoBuf序列化 这篇文章里可以看到,不同的序列化框架最终转成的二进制流是不一样的。

Java 默认序列化

默认序列化机制

如果仅仅只是让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制。使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。

整个过程都是Java虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。

serialVersionUID

serialVersionUID的作用
不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L

Java 序列化实现

ObjectInputStream && ObjectOutputStream

类ObjectInputStream 和ObjectOutputStream是高层次的数据流,它们包含序列化和反序列化对象的方法。
ObjectOutputStream 类包含很多写方法来写各种数据类型,但是一个特别的方法例外:

public final void writeObject(Object x) throws IOException

上面的方法序列化一个对象,并将它发送到输出流。相似的ObjectInputStream 类包含如下反序列化一个对象的方法:

public final Object readObject() throws IOException, ClassNotFoundException

该方法从流中取出下一个对象,并将对象反序列化。它的返回值为Object,因此,你需要将它转换成合适的数据类型。

Serializable 接口

情境:一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable 接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。
解决:要想将父类对象也序列化,就需要让父类也实现Serializable 接口。如果父类不实现的话的,就 需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。

Externalizable 接口

无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化。JDK中提供了另一个序列化接口—Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效。
writeExternal:把一个Java对象写入到流中
readExternal:从流中读取一个Java对象

java序列化一览

Java 序列化框架比较

性能比较

测试方法

jvm-serializers 提供了一个很好的比较各种Java序列化的的测试套件。 它罗列了各种序列化框架, 可以自动生成测试报告。

适用性比较

  • json
    json的序列化框架有fastjson,jackson,gson等。
    适用于数据量小,实时性较低(例如秒级别)的服务。JSON格式具有非常强的前后兼容性,并且调式方便,所以对客户端与服务端的通讯尤其适用。
  • xml
    xml的序列化框架有XStream。XML的序列化和反序列化的空间和时间开销都比较大,对于对性能要求在ms级别的服务,不推荐使用。
  • hessian
    hessian主要用于java序列化。它的实现机制是着重于数据,附带简单的类型信息的方法:
  • 对于简单的数据类型。就像Integer a = 1,hessian会序列化成I 1这样的流,I表示int or Integer,1就是数据内容。
    • 对于复杂对象,通过Java的反射机制,hessian把对象所有的属性当成一个Map来序列化,产生类似M className propertyName1 I 1 propertyName S stringValue
    • 对于引用对象,在序列化过程中,如果一个对象之前出现过,hessian会直接插入一个R index这样的块来表示一个引用位置,从而省去再次序列化和反序列化的时间。
  • thift
    Thrift是Facebook开源提供的一个高性能,轻量级RPC服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。 但是,Thrift并不仅仅是序列化协议,而是一个RPC框架。相对于JSON和XML而言,Thrift在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的RPC解决方案;但是由于Thrift的序列化被嵌入到Thrift框架里面,Thrift框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如HTTP)。
  • protobuf
    序列化数据非常简洁,紧凑,析速度非常快,提供了非常友好的动态库。使用简介,反序列化只需要一行代码。但是在JavaBean和proto之间的转换较麻烦。
  • avro
    Avro的产生解决了JSON的冗长和没有IDL的问题。 Avro提供两种序列化格式:JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美,JSON格式方便测试阶段的调试。
  • 动态类型:Avro并不需要生成代码,模式和数据存放在一起,而模式使得整个数据的处理过程并不生成代码、静态数据类型等等。这方便了数据处理系统和语言的构造。
    • 未标记的数据:由于读取数据的时候模式是已知的,那么需要和数据一起编码的类型信息就很少了,这样序列化的规模也就小了。
    • 不需要用户指定字段号:即使模式改变,处理数据时新旧模式都是已知的,所以通过使用字段名称可以解决差异问题。

Reference

https://www.ibm.com/developerworks/cn/java/j-lo-serial/
http://www.infoq.com/cn/articles/serialization-and-deserialization
http://sqtds.github.io/2015/05/13/2015/java-serizable/
http://www.solinx.co/archives/377

调试过程

本地运行代码,输出如下:

1
2
3
4
5
15/11/12 12:09:51 INFO SparkContext: Running Spark version 1.5.1
Exception in thread "main" java.lang.NoClassDefFoundError: scala/collection/GenTraversableOnce$class
at org.apache.spark.util.TimeStampedWeakValueHashMap.<init>(TimeStampedWeakValueHashMap.scala:42)
at org.apache.spark.SparkContext.<init>(SparkContext.scala:287)
Caused by: java.lang.ClassNotFoundException: scala.collection.GenTraversableOnce$class

查了半天没有任何结果,大家分析的原因各式各样。后来看到了一位仁兄总结的帖子:solve spark issue of all masters are unresponsive,跑去spark机器看了一下log,果然有收获。

spark日志:

1
ReliableDeliverySupervisor: Association with remote system [akka.tcp://sparkDriver@100.64.80.93:57108] has failed, address is now gated for [5000] ms. Reason is: [scala.Option; local class incompatible: stream classdesc serialVersionUID = -114498752079829388, local class serialVersionUID = -2062608324514658839].

根据 scala.Option; local class incompatible 可以发现是 scala 的版本不对,spark 默认的是 scala-2.10,需要改变依赖的scala版本。

改完以后又发现,还是连接不上。本地的输出:

1
2
3
4
5
6
15/11/12 21:46:22 ERROR SparkUncaughtExceptionHandler: Uncaught exception in thread Thread[appclient-registration-retry-thread,5,main]
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@5430d0ff rejected from java.util.concurrent.ThreadPoolExecutor@7819693b[Running, pool size = 1, active threads = 0, queued tasks = 0, completed tasks = 1]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)

spark的日志如下:

A

1
15/11/12 21:46:03 ERROR ErrorMonitor: dropping message [class akka.actor.ActorSelectionMessage] for non-local recipient [Actor[akka.tcp://sparkMaster@10.19.27.215:4041/]] arriving at [akka.tcp://sparkMaster@10.19.27.215:4041] inbound addresses are [akka.tcp://sparkMaster@master1:4041]

B

1
15/11/12 22:00:41 WARN ReliableDeliverySupervisor: Association with remote system [akka.tcp://sparkDriver@100.64.80.93:61812] has failed, address is now gated for [5000] ms. Reason is: [Disassociated].

关于B这部分的log,怀疑是测试环境的spark的网络访问权限没有打开!最后打开网络访问权限后解决。spark master和worker之间的通信使用的是akka,tcp协议。

spark的A部分log和本地的log是一致的。

第二天接着查,查了很多地方。怀疑是Spark的配置不正确。
对于Spark的配置,官网说的是:

Options for the daemons used in the standalone deploy mode

SPARK_MASTER_IP, to bind the master to a different IP address or hostname

而我spark机器上的设置是:

  1. conf/spark-env.sh: export SPARK_MASTER_IP=master1
  2. /etc/hosts: 10.x.xxx.215 master1

一切配置正确但依然不行。Google上到处寻觅,找到 spark 的 group 里面的一个帖子,https://groups.google.com/forum/#!topic/spark-users/SKE4UOUQ_U8,提到

Yes, this message means that one of the workers tried to contact you using your IP address (10.129.7.154), but Akka is (somewhat stupidly) configured to rely on a DNS name (namely ip-10-129-7-154). If you’ve set up the Spark standalone mode, there was a bug in the scripts where they would use an IP address for the master instead of a hostname.

所以当我将 SMART_IP 改成 ip 而不是 hostname 后,本地终于能连上spark了,设置如下:

conf/spark-env.sh: export SPARK_MASTER_IP=10.x.xxx.215

几点备忘

  1. 通过 %% 方法获取正确的 Scala 版本
    如果你用是 groupID %% artifactID % revision 而不是 groupID % artifactID % revision(区别在于 groupID 后面是 %%),sbt 会在 工件名称中加上项目的 Scala 版本号。 这只是一种快捷方法。你可以这样写不用 %%:

  2. enable build.sbt auto import
    修改了 build.sbt,但是包没有引入生效

  3. ./spark-shell 加载配置文件
    在Spark 集群上运行一个应用,只需通过master的 spark://IP:PORT 链接传递到SparkContext构造器
    在集群上运行交互式的Spark 命令, 运行如下命令:
    MASTER=spark://IP:PORT ./spark-shell
    注意,如果你在一个 spark集群上运行了spark-shell脚本,spark-shell 将通过在conf/spark-env.sh下的SPARK_MASTER_IP和SPARK_MASTER_PORT自动设置MASTER

1. contact

MySQL CONCAT function is used to concatenate two strings to form a single string.

MySQL GROUP_CONCAT() function returns a string with concatenated non-NULL value from a group.

数据库Person表的内容如下:

id name source age
1 A GP 6
2 B GP 2
3 A FB 1
4 C FB 4
5 D FB 5
6 A FB 3
7 C TW 7


1.SQL:

1
2
3
4
select name, count(distinct source) as sourceCount,
group_concat(distinct source separator "/") as sources
from Person
group by name;

Query Result:

name sourceCount sources
A 2 GP/FB
B 1 GP
C 2 GP/TW
D 1 FB


2.SQL:

1
2
3
4
5
select name, count(distinct source) as sourceCount,
group_concat(distinct source separator "/") as sources
from Person
group by name
having sourceCount = 1 and sources = 'FB';

Query Result:

name sourceCount sources
D 1 FB


3.SQL:

1
2
3
select name, count(distinct age) as ageCount,
group_concat(age order by age separator "#") as ages
from Person;

Query Result:

name ageCount ages
A 3 1#3#6
B 1 2
C 2 4#7
D 1 5

2. mysql -N 不显示字段名

普通的查询语句,查询结果中带字段名

1
2
mysql -h xxxx -P 8000 -u'xxx' -p'xxx' -D xxdb
-e "select name from Person where name = 'A'";

+—————-+

| name |

+—————-+

| not found |

+—————-+

带-N的查询语句,查询结果中不带字段名

1
2
mysql -N -h xxxx -P 8000 -u'xxx' -p'xxx' -D xxdb
-e "select name from Person where name = 'A'";

+—————-+

| not found |

+—————-+

3. IFNULL

使用IFNULL能判断是否有查到结果。
在shell中跑mysql的指令容易出现空行,此时用IFNULL是最合适的了。

1
2
3
4
mysql -N -h xxxx -P 8000 -u'xxx' -p'xxx' -D xxdb
-e "select IFNULL(
(select name from Person where name = 'A' and age = 100),
'not found')"

+—————-+

| not found |

+—————-+

什么是内存泄露

内存泄露 memory leak,是指已申请的无用内存无法被回收。

内存泄漏有两种情况:

  • 一种情况如在C/C++语言中的,在堆中的分配的内存,在没有将其释放掉的时候,就将所有能访问这块内存的方式都删掉(如指针重新赋值)

  • 一种情况则是在内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用)

第一种情况,在Java中已经由于垃圾回收机制的引入,得到了很好的解决。所以,Java中的内存泄漏,主要指的是第二种情况。

内存泄露的一个例子:

1
2
3
4
5
for (int i = 0; i < 1000; i++) {
Object obj = new Object(
list.add(obj);
obj = null;
}

这段代码是:for循环中,new一个Object对象obj,然后将其添加到list中,最后将obj置空。

这个时候就发生了内存泄露,因为obj是可达的无用对象。发生GC时,尽管obj已经被置空成为了无用对象,但是obj能够从list可达,从而GC无法将其释放掉。次数obj占用的内存就是泄露了。

内存泄露与内存溢出

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。当发生内存溢出时,程序将无法进行,强制终止。在java中常见的java.lang.OutOfMemoryError就是内存溢出的log。

内存长期泄露终将导致内存溢出。

内存泄露的危害

一次内存泄露危害可以忽略,但内存长期泄露,可用内存会逐渐减少,导致降低性能,最终内存溢出。

在移动设备对于内存和CPU都有较严格的限制的情况下,Java的内存泄露还会导致程序性能降低甚至崩溃。

怎么产生内存泄露

容易引起内存泄漏的几大原因

  1. 静态集合类

    像HashMap、Vector 等静态集合类的使用最容易引起内存泄漏,因为这些静态变量的生命周期与应用程序一致,如示例1,如果该Vector 是静态的,那么它将一直存在,而其中所有的Object对象也不能被释放,因为它们也将一直被该Vector 引用着。

  2. 监听器

    在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

  3. 物理连接

    一些物理连接,比如数据库连接和网络连接,除非其显式的关闭了连接,否则是不会自动被GC 回收的。Java 数据库连接一般用DataSource.getConnection()来创建,当不再使用时必须用Close()方法来释放,因为这些连接是独立于JVM的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。

  4. 内部类和外部模块等的引用

    内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。

垃圾回收

可以手动执行垃圾回收吗?

只能建议jvm进行GC,但什么时候做GC由JVM决定

System.gc()

可以通过调用System.gc()建议JVM执行垃圾回收,但JVM不保证一定会执行GC操作。通常不推荐使用System.gc()。

finalize()

finalize()方法存在于java.lang.Object类中,可以被所有对象所使用。默认情况下其不执行任何动作。当垃圾回收器确定了一个对象没有任何引用时,其会调用finalize()方法。但是,finalize方法并不一定会被执行,因此也不建议覆写finalize()该方法。

内存泄露,会被垃圾回收吗

内存泄露 memory leak,是指已申请的无用内存无法被回收。GC只能回收第一种情况的内存泄露,见前面的释义。

设置null能防止内存泄露吗

最基本的建议就是尽早释放无用对象的引用,大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域后,自动设置为null。

不过这个真的有用吗?

查阅了网上的一些讨论以后有以下结论:

首先,jdk远比我们想象中的聪明,完全能判断出对象是否已经可以回收。但是在极少数情况下,这么做依然是有效的。

这些情况是:方法前面中有定义大的对象,然后又跟着非常耗时的操作,且没有触发JIT编译。

JVM即时编译器:即时编译器(Just In Time Compiler) 简称JIT
JAVA程序最初是通过解释器(Interpreter)进行解释执行的,当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。
为了提高热点代码的执行效率,就会将这些“热点代码”编译成与本地机器相关的机器码,进行各个层次的优化。 完成这个任务的编译器就是即时编译器(JIT)。

例如:

1
2
3
4
5
6
private void processObj() {
BigObject obj = … // 声明大对象obj
doSomethingWith(obj); // 使用obj
obj = null; // explicitly set to null
doSomethingElse(); //非常耗时的操作
}

此时显示的设置无用的对象obj为null才有效。

How to avoid Memory Leak in Java

贴出 How to avoid Memory leak issue in Java 一文中提到的防止java内存泄露的一些建议。

How to avoid Memory Leak in Java?

While coding if we take care of a few points we can avoid memory leak issue.

  1. Use time out time for the session as low as possible.
  2. Release the session when the same is no longer required. We can release the session by using HttpSession.invalidate().
  3. Try to store as less data as possible in the HttpSession.
  4. Avoid creating HttpSession in jsp page by default by using the page directive

    <%@page session=”false”%>

  5. Try to use StringBuffer’s append() method instead of string concatenation. String is an immutable object and if we use string concatenation, it will unnecessarily create many temporary objects on heap which results in poor performance.

    For ex. if we write String query = “SELECT id, name FROM t_customer whereMsoNormal” style=”margin-bottom: 0.0001pt;”> it will create 4 String Objects. But if we write the same query using StringBuffer’s append() it will create only one object as StringBuffer is mutable i.e. can be modified over and over again.

  6. In JDBC code, While writting the query try to avoid “*”. It is a good practice to use column name in select statement.
  7. Try to use PreparedStatement object instead of Statement object if the query need to be executed frequently as PreparedStatement is a precompiled SQL statement where as Statement is compiled each time the Sql statement is sent to the database.
  8. Try to close the ResultSet and Statement before reusing those.
  9. If we use stmt = con.prepareStatement(sql query) inside loop, we should close it in the loop.
  10. Try to close ResultSet, Statement, PreparedStatement and Connection in finally block.

在测试内存泄露时,对GC有一些收获

  1. cannot disable java gc
  2. 我们不能决定什么时候发生GC。
  3. System.gc() vs GC button in JVisualVM/JConsole
    As far as I know, Jconsole or any other tool, uses System.gc() only. There is no other option. As everyone know, java tells everyone not to rely on System.gc(), but that doesn’t mean it doesn’t work at all.

References

>

碰到的问题

今天做了一个feature的admin的接口api/xxv/abc/feature,便于ops在admin上直接管理配置信息。但是我们的域名是 ttt.company.com,所以admin应该访问
http://ttt.company.com/api/xxv/abc/feature
但是admin的域名是admin(-test).company.com,而且admin后台是需要登录的。这样就导致了前端跨域传cookie会有问题。

解决的方案

1. nginx 做请求转发

目前的做法是在服务端之前做一个反向代理,admin请求同域名的
http://admin-test.company.com/api/abc/feature
然后在nginx层将请求转发到
http://ttt.company.com/api/xxv/abc/feature

对于staging和online不同的环境,将请求转发到不同的server即可。

请求转发
http://admin-test.company.com/api/abc/feature
->
http://ttt.company.com/api/xxv/abc/feature

ngin配置

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name admin-test.company.com;
location /api/abc {
rewrite /api/abc/(.*) /api/xxv/$1 break;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://staging.server.hostname:8080;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name admin.company.com;
location /api/abc {
rewrite /api/abc/(.*) /api/xxv/$1 break;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://myserver-write-nodes;
}
}

2. 服务端处理跨域请求

在返回的response header中加入允许跨域访问的属性。例如:

Access-Control-Allow-Origin: {允许的域名}

更多信息参考:跨域 HTTP 请求(Cross-site HTTP request)

1. Btrace的简介

Btrace是由Kenai 开发的一个开源项目,是一种动态跟踪分析JAVA源代码的工具。它可以用来帮我们做运行时的JAVA程序分析,监控等等操作。

2. 官方参考手册

https://kenai.com/projects/btrace/pages/UserGuide

3. 实例

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
import com.sun.btrace.annotations.*;
import com.sun.btrace.AnyType;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class TestServiceImplTrace {
@TLS
private static long service_get_data_startTime = 0;
@OnMethod(
clazz = "com.xxx.mms.test.impl.TestServiceImpl",
method = "getTestData"
)
public static void startTestServiceImplExecute() {
section_facade_impl_startTime = timeMillis();
}
@OnMethod(
clazz = "com.xxx.mms.test.impl.TestServiceImpl",
method = "getTestData"
location = @Location(Kind.RETURN)
)
public static void endTestServiceImplExecute(AnyType[] args) { // 传入所有参数
long time = timeMillis() - section_facade_impl_startTime;
Object obj = args[4];
Integer end = (Integer)obj; // 将第5个参数转成Integer
printFields(args[0]); // 打印第1个参数的所有成员变量的值
if(end == 6){
print(strcat(“service getTestData execute time(millis): ", str(time)));
print(strcat(“\t string param: ", str(args[3]))); // 将第4个参数转成string并打印
println(strcat(“\t end: ", str(end)));
}
}
}

4. 心得

  1. btrace脚本的函数都没有走进去时,btrace pid tracing.java 是得不到结果的。
  2. Kind.LINE指向的行必须是代码能运行到的行。比如,以括号结束的行和空行都是无效的。
  3. 在刚启动btrace脚本监控时,会存在较大的耗时
  4. print有很多功能:
    printNumberMap
    printFields: print 每个域
    printArray:print 数组
  5. 如果服务的qps较低(0.2),直接去机器上app222通过ip请求,btrace的event不好用也达不到触发某个请求的目的,这个时候可以直接在本机请求此server的api,虽然与实际情况不符,但是能知道耗时的比例关系。

5. Btrace的原理

Btrace是由:Attach API + BTrace脚本解析引擎 + ASM + JDK6 Instumentation组成。简单来说就是:用Attach API附加*.jar然后使用BTrace脚本解析引擎 + ASM来实现对类的修改,在使用Instumentation实现类的内存替换。可详细的说明可以看refers的几篇文章。

6. 使用Btrace对java进程的影响

  • 装载时的影响:
    btrace每次使用,都会重新load所有的class。当然如果OnMethod不匹配,是不会被重新装载。所以跟你的OnMethod的匹配规则很有关系,如果使用+java.lang.Object。那就死定了。
  • 退出后的影响:
    btrace监控每次退出后,原先所有的class都不会被恢复,你的所有的监控代码依然一直在运行

抓取了下btrace改写过后的类:

1
2
3
4
5
6
public InstrumentServer(String ip, String port)
{
$btrace$com$agapple$btrace$Instrumentor$InstrumentTracer$bufferMonitor(this);
this.ip = ip;
this.port = port;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void $btrace$com$agapple$btrace$Instrumentor$InstrumentTracer$bufferMonitor(@Self Object arg0)
{
if (!BTraceRuntime.enter(InstrumentTracer.runtime)) return;
try {
Field ipField = BTraceUtils.field("com.agapple.btrace.Instrumentor.InstrumentServer", "ip");
Field portField = BTraceUtils.field("com.agapple.btrace.Instrumentor.InstrumentServer", "port");
String ip = (String)BTraceUtils.get(ipField, self);
String port = (String)BTraceUtils.get(portField, self);
BTraceUtils.println(BTraceUtils.strcat(BTraceUtils.strcat(BTraceUtils.strcat("ip : ", BTraceUtils.str(ip)), " port : "), BTraceUtils.str(port)));
BTraceRuntime.leave(); return; } catch (Throwable localThrowable) { BTraceRuntime.handleException(localThrowable);
}
}

注意其中的

1
if (!BTraceRuntime.enter(InstrumentTracer.runtime)) return;

再看一下BTraceRuntime中对应方法的实现:

1
2
3
4
5
private volatile boolean disabled;
public static boolean enter(BTraceRuntime current) {
if (current.disabled) return false;
return map.enter(current);
}

每次执行你的监控代码之前会先进行一个判断,判断当前是否处于监控中。你的客户端发起了exit指令后,该方法判断false,直接return。

所以btrace使用退出后会让你的代码多走了一个方法调用+一个对象属性判断,所以说影响还是非常少的。

7. 推荐阅读

Btrace系列之一:Btrace的基本原理 http://victorzhzh.iteye.com/blog/965789
btrace一些你不知道的事(源码入手) http://agapple.iteye.com/blog/1005918

refers

>
Java SE 6 新特性: Instrumentation 新功能 http://www.ibm.com/developerworks/cn/java/j-lo-jse61/
Btrace系列之一:Btrace的基本原理 http://victorzhzh.iteye.com/blog/965789
btrace一些你不知道的事(源码入手) http://agapple.iteye.com/blog/1005918

1. 设值注入(推荐)

1
2
3
4
<bean id="myService" class="com.zane.test.MyServiceImpl">
<property name="serializer" ref="Serializer"/>
<property name="httpService" ref="httpService"/>
</bean>

2. 构造器注入(死的应用)

1
2
3
4
<bean id="myModel" class="com.zane.test.MyModel">
<constructor-arg index="0" value="${name}"/>
<constructor-arg index="1" value=20"/>
</bean>

3. 注入List

1
2
3
4
5
6
7
8
9
<bean id="myTypes" class="java.util.ArrayList">
<constructor-arg>
<list>
<value type="com.zane.test.MyType">A</value>
<value type="com.zane.test.MyType">B</value>
<value type="com.zane.test.MyType">C</value>
</list>
</constructor-arg>
</bean>

4. 注入Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<bean id="myTypeValueMap" class="java.util.HashMap">
<constructor-arg>
<map>
<entry key="#{T(com.zane.test.MyType).A}">
<value type="java.lang.Integer">3</value>
</entry>
<entry key="#{T(com.zane.test.MyType).B}">
<value type="java.lang.Integer">4</value>
</entry>
<entry key="#{T(com.zane.test.MyType).C}">
<value type="java.lang.Integer">5</value>
</entry>
</map>
</constructor-arg>
</bean>

当注入的是第三方的jar包的key类型时,需要使用@Resource注入

1
2
3
@Resource
@Qualifier("myTypeValueMap")
private Map<MyType, String> myTypeValueMap;

否则使用Autowired即可

1
2
3
@Autowired
@Qualifier("myTypeValueMap")
private Map<MyType, String> myTypeValueMap;

1.java线程同步原理

java会为每个object对象分配一个monitor,当某个对象的同步方法(synchronized methods )或同步快被多个线程调用时,该对象的monitor将负责处理这些访问的并发独占要求。

当一个线程调用一个对象的同步方法时,JVM会检查该对象的monitor。如果monitor没有被占用,那么这个线程就得到了monitor的占有权,可以继续执行该对象的同步方法;如果monitor被其他线程所占用,那么该线程将被挂起,直到monitor被释放。

当线程退出同步方法调用时,该线程会释放monitor,这将允许其他等待的线程获得monitor以使对同步方法的调用执行下去。

注意:java对象的monitor机制和传统的临界检查代码区技术不一样。java的一个类一个同步方法并不意味着同时只有一个线程独占执行(不同对象的同步方法可以同时执行),但临界检查代码区技术确会保证同步方法在一个时刻只被一个线程独占执行。

java的monitor机制的准确含义是:任何时刻,对一个指定object对象的某同步方法只能由一个线程来调用。

java对象的monitor是跟随object实例来使用的,而不是跟随程序代码。两个线程可以同时执行相同的同步方法,比如:一个类的同步方法是xMethod(),有a,b两个对象实例,一个线程执行a.xMethod(),另一个线程执行b.xMethod(). 互不冲突。

2. wait(), notify(),notifyAll()

首先看一下Java中java.lang.Object类的实现:

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
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
protected void finalize() throws Throwable { }
}

wait()方法是object类的方法,解决的问题是线程间的同步,该过程包含了同步锁的获取和释放,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify()方法才会重新激活调用者。

注意:线程调用notify()之后,只有该线程完全从 synchronized代码里面执行完毕后,monitor才会被释放,被唤醒线程才可以真正得到执行权。

使用:

  • obj.wait()方法使本线程挂起,并释放obj对象的monitor,只有其他线程调用obj对象的notify()或notifyAll()时,才可以被唤醒。
  • obj.notifyAll()方法唤醒所有阻塞在obj对象上的沉睡线程,然后被唤醒的众多线程竞争obj对象的monitor占有权,最终得到的那个线程会继续执行下去,但其他线程继续等待。
  • obj.notify()方法是随机唤醒一个沉睡线程,过程更obj.notifyAll()方法类似。

wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,例如:

1
2
3
4
synchronized(x){
x.notify()
//或者wait()
}

以上内容说明了为什么调用wait(),notify(),notifyAll()的线程必须要拥有obj实例对象的monitor占有权。

每个对象实例都有一个等待线程队列。这些线程都是等待对该对象的同步方法的调用许可。对一个线程来说,有两种方法可以进入这个等待线程队列。一个是当其他线程执行同步方法时,自身同时也要执行该同步方法;另一个是调用obj.wait()方法。

当同步方法执行完毕或者执行wait()时,其他某个线程将获得对象的访问权。当一个线程被放入等待队列时,必须要确保可以通过notify()的调用来解冻该线程,以使其能够继续执行下去。

3. native

native is a java keyword. It marks a method, that it will be implemented in other languages, not in Java. The method is declared without a body and cannot be abstract. It works together with JNI (Java Native Interface).
Native methods were used in the past to write performance critical sections but with java getting faster this is now less common. Native methods are currently needed when

You need to call from java a library, written in another language.
You need to access system or hardware resources that are only reachable from the other language (typically C). Actually, many system functions that interact with real computer (disk and network IO, for instance) can only do this because they call native code.

synchronized关键字简洁、清晰、语义明确,因此即使有了Lock接口,使用的还是非常广泛。其应用层的语义是可以把任何一个非null对象作为”锁”。
synchronized在软件层面依赖JVM,Lock在硬件层面依赖特殊的CPU指令。

1. JVM如何实现synchronized

在java语言中存在两种内建的synchronized语法:synchronized语句和synchronized方法。
synchronized语句被javac编译成bytecode时,会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit字节码指令。
synchronized方法被javac编译成bytecode时,会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。

2. hotspot当前对synchronized的实现

当前的hotspot共有3种类型的锁,来实现synchronize的语义,之所以有3种,是因为这3种要解决的问题不同,所做的优化也不同。这3种锁分别为biased locking,stack lock,infalted(ObjectMonitor).简单除暴的来讲,从轻量级上来说,biased lock最优,inflated 最差。

3. synchronized锁住的是什么

synchronized锁定的是对象而非函数或代码。
当synchronized作用在方法上时,锁住的便是对象实例(this);当作用在静态方法时锁住的便是对象对应的Class实例,因为Class数据存在于永久带,因此静态方法锁相当于该类的一个全局锁;当synchronized作用于某一个对象实例时,锁住的便是对应的代码块。
每个对象只有一把锁(Lock)与之关联,当进行到synchronized语句或函数的时候,这把锁就会被当前的线程(thread)拿走,其他的(thread)再去访问的时候拿不到锁就被暂停了。
在HotSpot JVM实现中,锁有个专门的名字:对象监视器。

4. synchronized的使用场景

  1. public synchronized void method1
    锁住的是该对象,类的其中一个实例,当该对象(仅仅是这一个对象)在不同线程中执行这个同步方法时,线程之间会形成互斥。达到同步效果,但如果不同线程同时对该类的不同对象执行这个同步方法时,则线程之间不会形成互斥,因为他们拥有的是不同的锁。
  2. synchronized(this){ //TODO }
    同一
  3. public synchronized static void method3
    锁住的是该类,当所有该类的对象(多个对象)在不同线程中调用这个static同步方法时,线程之间会形成互斥,达到同步效果,但如果多个线程同时调用method1,method3,则不会引互斥,具体讲看最后讲解。
  4. synchronized(Test.class){ //TODO}
    同三
  5. synchronized(o) {}
    这里面的o可以是一个任何Object对象或数组,并不一定是它本身对象或者类,谁拥有o这个锁,谁就能够操作该块程序代码。

refers

周志明的《深入理解Java虚拟机》
https://blogs.oracle.com/dave/entry/biased_locking_in_hotspot
http://www.javaworld.com/article/2076971/java-concurrency/how-the-java-virtual-machine-performs-thread-synchronization.html
http://f.dataguru.cn/thread-472518-1-1.html

1. 同步的原理

JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处, JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个 monitor 与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。

2. Java对象头

锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。

长度 内容 说明
32/64bit Mark Word 存储对象的hashCode或锁信息等
32/64bit Class Metadata Address 存储到对象类型数据的指针
32/64bit Array length 数组的长度(如果当前对象是数组)

Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下:

25 bit 4bit 1bit
是否是偏向锁
2bit
锁标志位
无锁状态 对象的hashCode 对象分代年龄 0 01

在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:

3. 几种锁的类型

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。

Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。

锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

3.1 偏向锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。

偏向锁的进一步理解

偏向锁的释放不需要做任何事情,这也就意味着加过偏向锁的MarkValue会一直保留偏向锁的状态,因此即便同一个线程持续不断地加锁解锁,也是没有开销的。

另一方面,偏向锁比轻量锁更容易被终结,轻量锁是在有锁竞争出现时升级为重量锁,而一般偏向锁是在有不同线程申请锁时升级为轻量锁,这也就意味着假如一个对象先被线程1加锁解锁,再被线程2加锁解锁,这过程中没有锁冲突,也一样会发生偏向锁失效,不同的是这回要先退化为无锁的状态,再加轻量锁,如图:

另外,JVM对那种会有多线程加锁,但不存在锁竞争的情况也做了优化,听起来比较拗口,但在现实应用中确实是可能出现这种情况,因为线程之前除了互斥之外也可能发生同步关系,被同步的两个线程(一前一后)对共享对象锁的竞争很可能是没有冲突的。对这种情况,JVM用一个epoch表示一个偏向锁的时间戳(真实地生成一个时间戳代价还是蛮大的,因此这里应当理解为一种类似时间戳的identifier),对epoch,官方是这么解释的:

A similar mechanism, called bulk rebiasing, optimizes situations in which objects of a class are locked and unlocked by different threads but never concurrently. It invalidates the bias of all instances of a class without disabling biased locking. An epoch value in the class acts as a timestamp that indicates the validity of the bias. This value is copied into the header word upon object allocation. Bulk rebiasing can then efficiently be implemented as an increment of the epoch in the appropriate class. The next time an instance of this class is going to be locked, the code detects a different value in the header word and rebiases the object towards the current thread.

偏向锁的获取

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。下图中的线程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程。

偏向锁的设置

关闭偏向锁:偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay = 0。如果你确定自己应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁-XX:-UseBiasedLocking=false,那么默认会进入轻量级锁状态。

3.2 自旋锁

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。同时我们可以发现,很多对象锁的锁定状态只会持续很短的一段时间,例如整数的自加操作,在很短的时间内阻塞并唤醒线程显然不值得,为此引入了自旋锁。

所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会出让时间片,所以其他线程依旧有申请锁和释放锁的机会。

自旋锁省去了阻塞锁的时间空间(队列的维护等)开销,但是长时间自旋就变成了“忙式等待”,忙式等待显然还不如阻塞锁。所以自旋的次数一般控制在一个范围内,例如10,100等,在超出这个范围后,自旋锁会升级为阻塞锁。

对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。经过调查,目前只是通过汇编暂停了几个CPU周期,除了自旋周期选择,HotSpot还进行许多其他的自旋优化策略,具体如下:

  • 如果平均负载小于CPUs则一直自旋
  • 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
  • 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞 如果CPU处于节电模式则停止自旋
  • 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)

3.3 轻量级锁

轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。

轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程。下图是两个线程同时争夺锁,导致锁膨胀的流程图。


3.4 重量级锁

重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex互斥的功能,它还负责实现了Semaphore的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

4. 锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程使用自旋会消耗CPU 追求响应时间,锁占用时间很短
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,锁占用时间较长


refers:

周志明的《深入理解Java虚拟机》
https://blogs.oracle.com/dave/entry/biased_locking_in_hotspot
https://www.usenix.org/legacy/event/jvm01/full_papers/dice/dice.pdf
http://www.javaworld.com/article/2076971/java-concurrency/how-the-java-virtual-machine-performs-thread-synchronization.html
http://www.infoq.com/cn/articles/java-se-16-synchronized
http://www.majin163.com/2014/03/17/synchronized2/
http://www.cnblogs.com/javaminer/p/3889023.html
http://blog.csdn.net/coslay/article/details/41526635