Posts tagged Java

内存屏障

2

前几天一直在看Java内存模型(JMM)这个东西,在了解JMM的过程中也不断地遇到了内存屏障这个名词,遂花了一点时间去了解了一下。

什么是内存屏障

内存屏障,说白了就是一个指令,其作用正如其名字里面所描述的,起到的是屏障的作用,具体地来说,内存屏障这条指令的作用是让屏障指令前面的指令不会被重排序到屏障指令之后,屏障指令之后的指令也不会被重排序到屏障指令之前,相当于一道屏障挡在了指令之间,前面和后面的指令都不能跨过屏障(关于指令重排序,这里不再解释,有兴趣了解的可以去Google一下)。

内存屏障的种类

鉴于CPU和编译器都可能对指令进行重排序,所以我将内存屏障分为两种:

  • 编译器级别的:防止编译器对指令进行重排序,例如GCC的asm volatile("" ::: "memory")等等。
  • 处理器级别的:防止处理器对指令进行重排序,例如X86的locklfence()sfenec()等等(关于处理器级别的内存屏障如何实现,可以参考这篇文章)。

Java的volatile如何通过内存屏障保证其语义的:

首先,需要知道Java中的volatile有两个语义:

  • 线程对volatile变量的读写都会马上反映到主存上,对其他线程马上可见。
  • 对volatile变量的读写操作不会和其他对共享内存进行操作的指令重排序。

跟踪HotSpot JVM中对volatile的变量的写操作可以发现,其在写操作之后插入了一条指令

asm volatile ("lock; addl $0,0(%%esp)" ::: "cc", "memory");

我们可以把这个指令分成两部分来看:

  • asm volatile ("" ::: "memory"):这个是GCC中常用的编译器级别的内存屏障,首先这是一条汇编指令,加入了volatile表示指令不会被重排,后面的clobber list里面加入了”memory”表示告诉编译器,指令对主存做了修改,所有CPU中的缓存都失效了,后面的指令需要重新从主存中重新Load数据。
  • lock; addl $0,0(%%esp):对于这一条指令,首先看addl $0,0(%%esp)这个部分,这个部分实际上是个无用操作,如果esp栈顶是0的话,就给栈顶加上一个0,由于这个操作会影响到程序状态字寄存器,所以在后面的clobber list中加入了cc(Condition Code Registry,也称为Flag Registry或Programm Status Word Registry);再看这个汇编指令前面加入了一个lock前缀,这个lock前缀的意思是执行指令的CPU会发出LOCK#信号,独占系统主线直到前面的读写操作全部完成并且执行完加了lock前缀的这条指令,在这个过程中其他CPU的读写操作全部被Hold住(参考Intel的开发手册)。所以lock前缀实际上可以看作是一个多处理器的内存屏障。

这两个部分合起来就起到了一个完整的内存屏障,即对编译器有效,也对处理器有效,另外也保证了volatile的可见性问题。

JVM规范之内存结构

0

原文地址:http://www.goldendoc.org/2011/11/jvm_memory_management/

JVM规范中一块很重要的地方就是内存的管理,JVM规范里面在功能上对JVM的内存进行了一定的划分,理解这一块内容对理解JVM的规范有一定的帮助。当然,就像许多规范一样,JVM规范虽然对内存进行了划分,但是各个JVM的实现却不一定会做这样的划分,说地确切一点是负责对应功能的内存肯定在每一个JVM的实现里面都是有的(如果没有提供参数给程序员调整的话,有些对你来说就是不可见的),但是其实现方式可以是五花八门的。另外,JVM对内存的分配和管理事实上是和Java Class文件的格式,JVM的运行机制都有密切相关,如果对这些东西有所了解,那么对内存的管理理解也会有所帮助。

先来看一张图来直观地感受下JVM规范里面是怎样对JVM的内存进行功能上的区分的:

JVM内存管理

在这张图里面,首先根据内存是线程私有的还是线程共享的,把内存区域分成了两块:

线程私有

这一块区域主要由程序计数器(PC),虚拟机栈(Virtual Machine Stack)和本地方法栈(Native Method Stack)组成

  • 程序计数器:同计算机的程序计数器;如果正在执行Java方法,则指向虚拟机字节码指令的地址,如果是native的,这个计算器的值为空。
  • 虚拟机栈:虚拟机栈是和线程的生命周期相同的,每创建一个线程,就会创建一个虚拟机栈。而在一个线程中,每调用一个方法,就会在虚拟机栈中创建一个栈帧(Stack Frame),用于保存方法相关的信息,栈帧的具体结构看上图的箭头左边部分:
    • 本地变量表(Local Variable):用于保存本地变量,这里的本地变量是指方法的入参以及在方法中声明的变量,如果在编译Java文件的时候加上”-g”参数,然后用”javap -p -verbose”打印出class文件后可以看到一个方法的本地变量表的内容,下面是一个简单的“Hello, world!”程序的main方法的本地变量表:
      LocalVariableTable:
      Start  Length  Slot  Name   Signature
      0      9      0    args       [Ljava/lang/String;
      
    • 操作数栈(Operand Stack):首先要了解JVM的架构是基于栈的指令集,指令的操作都是对栈的操作,所以方法运行的时候的一些参数和中间变量都必须先放入栈中才能够被操作,这里面的栈指的就是操作数栈,注意不要和前面提到的虚拟机栈混为一谈。
    • 对运行时常量池的引用(Runtime Constant Pool Reference):这里面主要保存对在方法中调用的方法的符号引用,这些符号引用会在运行时被解析成对方法的真正引用。
  • 本地方法栈:本地方法栈的功能和虚拟机栈的作用相同,不同的是本地方法栈服务的对象是本地方法。

线程共享

线程共享这一块区域说起来就一块东西:堆;稍微细分一下可以分为方法区(Method Area)和”存放类实例的区域”

  • 方法区:主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译以后的代码,其中运行时常量池也在方法区中。
  • “存放类实例的区域”:这一部分内存是存放对象,所有实例化的对象都会放在这个地方。

直接内存

前面看到的都是JVM的运行时数据区的部分,JVM的内存中还有另外一部分叫直接内存(Direct Memory,对应上面的图中的最下方的地方),它不是JVM运行时数据区的一部分,不受JVM的GC管理。其中Java NIO中的Buffer直接在堆外分配的就属于这个部分。

OpenJDK编译手记

4

作为一位程序员,我坚信了解一件事物的最佳方式是深入到它的最底层去研究其运行的原理。既然我大部分时间都在写Java代码,那么就肯定需要了解JVM的运行原理。所以上周尝试了编译了一下JVM的开源实现:OpenJDK

准备工作

在开始编译之前,首先需要一些准备工作:

  • 操作系统:建议在2.x内核的Linux系统上编译,在Windows下编译的话还要装CYGWIN,比较麻烦,如果你在3.0以上内核版本的Linux下编译,首先你会遇到一个This OS is not supported的错误,然后即使你在编译的时候加上了DISABLE_HOTSPOT_OS_VERSION_CHECK=ok这个选项,你可能会遇到一些宏定义找不到之类的编译错误。
  • 源代码:编译OpenJDK当然需要OpenJDK的源代码,可以从这里下载
  • 依赖:编译OpenJDK所需的东西在下载过来的源代码包里面的README_builds.html中都有列出,这里列出一些大概的依赖,具体所需的版本可以在README_builds.html中找到:OpenJDK(是的,编译OpenJDK需要一个OpenJDK,有点鸡生蛋,蛋生鸡的感觉),make,g++,ant,axel(Linux声音相关),X11(图形相关,编译awt的时候需要),freetype2(字体相关)等
  • 环境变量:编译以前请先分别设置语言和编译所需的OpenJDK的路径:
    export LANG=C ALT_BOOTDIR=/usr/lib/jvm/java-openjdk
    

编译

安装好所需的依赖以后,可以用以下命令检查一下依赖是否都已经正确安装了:

make sanity

如果输出没有错误,则可以用一下命令编译:

make ALLOW_DOWNLOADS=true

加上ALLOW_DOWNLOADS=true的原因是编译过程中ant可能会需要下载一些东西,如果你要编译一个DEBUG版本,则可以用以下命令

make debug_build ALLOW_DOWNLOADS=true

遇到的问题

我在编译过程还是遇到了不少问题,首先在Ubuntu 11.10下编译,遇到了前面提到的Linux内核3.0的问题,后来改到在CentOS 6下编译。

在CentOS下编译首先遇到了一个ant的一个类找不到的问题(在一个叫optional的package下),后来看了一下我直接用yum install安装的ant默认并没有安装一些optional的包,后来又用yum install安装了那个类对应的包,算是解决了这个错误。如果读者是直接在ant的官方网站下载安装应该不会有这个问题。

再之后就是在编译过程中遇到了很多缺少X11的头文件的错误,幸好发现yum有yum provides这个命令可以找到头文件所在的包,然后用yum install安装。(:-),一直在Ubuntu下用apt-get的孩子伤不起啊~~)

整个编译过程大概需要1个多小时(AMD速龙3800,2G RAM,单Job),这个期间可以搞点下午茶吃吃,搞部小电影看看~~

最后感谢下方攀同学在过程中提供的帮助,^_^

HttpClient 4.0.1 请求特别慢的原因分析

9

问题描述

昨天晚上碰到一个比较奇怪的问题,用HttpClient 4.0.1向一个服务器发送请求,非常慢,请求的时间超过2秒,但是采用HttpClient 3.1或者HttpClient4.1.2速度都非常快,请求的时间在2毫秒左右,非常奇怪,我采用的代码如下:

    public static void main(String[] args) throws Exception {
        HttpClient client = new DefaultHttpClient();
        HttpPost httppost = new HttpPost("http://127.0.0.1:8080/qstore");
        Map<String, Object> requestMap = new HashMap<String, Object>();
        requestMap.put("method", "get");
        requestMap.put("chunkName", "bbt_chunk");
        requestMap.put("key", "liushunnian1");
        String data = JSONObject.fromObject(requestMap).toString();
        StringEntity reqEntity = new StringEntity(data);
        httppost.setEntity(reqEntity);

        while (true) {
            long start = System.currentTimeMillis();
            HttpResponse response = client.execute(httppost);
            long end = System.currentTimeMillis();
            System.out.println("Time:" + (end - start) + "ms");
            HttpEntity entity = response.getEntity();
            BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));
            System.out.println(reader.readLine());
        }
    }

原因分析

为了找出原因,我打开了HttpClient的Debug日志:

    System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog");
    System.setProperty("org.apache.commons.logging.simplelog.showdatetime", "true");
    System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.http", "debug");

打出的部分日志如下:

2011/10/20 14:01:12:329 CST [DEBUG] headers - >> POST /qstore HTTP/1.1
2011/10/20 14:01:12:329 CST [DEBUG] headers - >> Content-Length: 61
2011/10/20 14:01:12:329 CST [DEBUG] headers - >> Content-Type: text/plain; charset=ISO-8859-1
2011/10/20 14:01:12:329 CST [DEBUG] headers - >> Host: 10.20.150.29:8080
2011/10/20 14:01:12:329 CST [DEBUG] headers - >> Connection: Keep-Alive
2011/10/20 14:01:12:329 CST [DEBUG] headers - >> User-Agent: Apache-HttpClient/4.1.2 (java 1.5)
2011/10/20 14:01:12:329 CST [DEBUG] headers - >> Expect: 100-continue
2011/10/20 14:01:14:331 CST [DEBUG] wire - >> "{"chunkName":"bbt_chunk","method":"get","key":"liushunnian1"}"
2011/10/20 14:01:14:333 CST [DEBUG] wire - << "HTTP/1.1 200 OK[\r][\n]"

可以看到客户端与服务端数据交互的过程:

  1. HttpClient首先向服务器端发送了一个头信息,并且期望返回一个100-Continue
  2. 过了大约两秒钟后,HttpClient又向服务器发送了请求中请求体

从上面的这个过程可以看出,HttpClient向服务器端发送100-Continue以后一直在等待服务端的返回,在等待超时之后才将请求体发送给服务器端,那么什么是100-Continue呢?看看Http规范里面的说明:

The client SHOULD continue with its request. This interim response is used to inform the client that the initial part of the request has been received and has not yet been rejected by the server. The client SHOULD continue by sending the remainder of the request or, if the request has already been completed, ignore this response. The server MUST send a final response after the request has been completed. See section 8.2.3 for detailed discussion of the use and handling of this status code.

也就是说当客户端要向服务器端发送大数据的时候,客户端可以先向服务器端发送一个请求,如果服务器端返回的是状态码100,那么客户端就可以继续把请求体的数据发送给服务器端。这样在某些情况下可以减少网络开销。

再看看HttpClient里面对100-Continue的说明:

The purpose of the Expect: 100-Continue handshake is to allow the client that is sending a request message with a request body to determine if the origin server is willing to accept the request (based on the request headers) before the client sends the request body. The use of the Expect: 100-continue handshake can result in a noticeable performance improvement for entity enclosing requests (such as POST and PUT) that require the target server’s authentication. The Expect: 100-continue handshake should be used with caution, as it may cause problems with HTTP servers and proxies that do not support HTTP/1.1 protocol.

里面提到的一句话是100-Continue应该谨慎使用,因为有些服务器不支持,可能会造成一些问题。那么我这次遇到的原因就是因为服务器端不支持,导致客户端一直要等到100-Continue请求超时以后才把请求体发送给服务器端。

而至于版本的问题是因为HttpClient 3.1和HttpClient 4.1.2都已经把100-Continue默认关闭掉了,只有在HttpClient 4.0.1下才是默认打开的,我奇怪的是HttpClient的文档里面都说了这个应该谨慎使用,为什么还要默认打开?

解决方法

解决方法一:当然,最简单的解决方法当然是更换HttpClient的版本,根据HttpClient的ReleaseNotes,4.1以后的版本都已经将100-Continue默认关闭掉了。

解决方法二:另一个解决方法就是在4.0.1中将100-Continue给关闭掉:

httppost.getParams().setBooleanParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, false);

解决方法三:如果你一定想要让100-Continue开着,那么可以减短100-Continue的超时时间,默认的超时时间是最大3秒钟,可以通过以下方法设置:

httppost.getParams().setIntParameter(CoreProtocolPNames.WAIT_FOR_CONTINUE, 100);

Servlet 3.0 请求异步处理

0

一、什么是请求异步处理

请求异步处理是Servlet 3.0规范中引入的一个新的东西,因为有时候,我们请求的资源需要一段时间才能够返回(比如从网络或者数据库取大量数据),这个时候处理请求的线程就处于忙等待的状态,没有办法去处理其他的请求,造成了资源上的浪费,所以在Servlet 3.0规范中引入了请求的异步处理,让一些耗时比较长的事情交给异步线程处理,将容器的处理线程给释放出来,提高资源的利用率。

二、如何让一个Servlet或者Filter支持请求异步处理

在Servlet 3.0中,你可以在Servlet或者Filter中使用请求异步处理,这里面仅仅以Servlet为例,在Filter中使用请求异步处理和在Servlet中使用请求异步处理是相同的。

要让一个Servlet支持请求异步处理,你可以在web.xml中定义servlet的时候,再添加一个<async-support>标签,值设为true,样例如下:

<servlet>
    <servlet-name>Hello</servlet-name>
    <servlet-class>com.khotyn.test.servlet.HelloServlet</servlet-class>
    <async-supported>true</async-supported>
</servlet>

当然,由于Servlet 3.0中支持以注解的方式声明Servlet,你还可以在注解中将一个Servlet标明为支持请求异步处理:

@WebServlet(urlPatterns = "/hello", asyncSupported = true)
public class HelloServlet extends HttpServlet {
	….........
}

三、如何使用请求异步处理

在将一个Servlet表明为支持请求异步处理以后,你就可以在Servlet中使用AsyncContext了,我们看一下样例代码:

public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException,       ServletException {
    final AsyncContext context = request.startAsync(request, response);
    context.start(new Runnable() {

        @Override
        public void run() {
            System.out.println("Hello, world!");
            context.complete();
        }
});
}

这是一个简单的例子,里面有一个关键的类,AsyncContext,从这个例子中我们可以看到AsyncContext的基本使用流程:

首先,通过Request的startAsync方法获取AsyncContext,其中startAsync方法有两种形式:

public AsyncContext startAsync() throws IllegalStateException;
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException;

其中,第一种不带参数的直接采用Servlet容器传过来的Request和Response,而第二个可以是包装过的Request和Response,如果你的Servlet没有标明支持请求的异步处理,那么调用这个方法会抛出IllegalStateException。

然后,我们调用了AsyncContext的start方法来启动异步线程,而在异步线程的run方法里面,调用了context的complete()方法来完成异步线程的调用,在不支持请求异步处理的Servlet中,请求等到Servlet的service()方法退出后就被提交了,而在支持AsyncContext的Servlet中,需要等到异步线程调用了AsyncContext的complete()才会被提交。

另外有一点需要说明的是,你不一定要采用AsyncContext的start方法来启动异步线程,事实上,你可以采用任何一种方式启动异步线程,Thread.start(),executor.execute()方法等等都可以启动,Servlet容器关心的是AsyncContext的状态,至于你用什么方式启动线程,启动了多少的线程去做。

怎么样?AsyncContext使用起来非常简单吧!

Go to Top