Spring命名空间之AOP

Spring加载时,会使用ClassLoader去查找所有能找到的”META-INF/spring.handlers”文件,并存放在handlerMappings中(DefaultNamespaceHandlerResolver在干这事),遇到除beans外的Namespace,就会去这里查找对应的解析器,如果不存在就报错,存在就使用相应的解析器进行解析。

<aop>是由AopNamespaceHandler来进行解析的。

AspectJAutoProxyBeanDefinitionParser会注册3个类来处理解析任务,分别对应为:

“config”, new ConfigBeanDefinitionParser();

“aspectj-autoproxy”, new AspectJAutoProxyBeanDefinitionParser();

“scoped-proxy”, new ScopedProxyBeanDefinitionDecorator();

要使用aop命名空间,需要在spring的xml中加入:

xmlns:aop="http://www.springframework.org/schema/aop"  
  
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-xsd"

这样便可以在该xml中使用<aop …/>了。

<aop:aspectj-autoproxy/>

使用该配置后,AspectJAutoProxyBeanDefinitionParser会注册AnnotationAwareAspectJAutoProxyCreator到Spring管理的bean中。由于AnnotationAwareAspectJAutoProxyCreator是BeanPostProcessor,所以在所有bean初始化完成后,都会调用该类中的postProcessAfterInitialization来进行处理。

AnnotationAwareAspectJAutoProxyCreator使用findEligibleAdvisors来查找是否有bean对应的Advisors(注册在当前beanFactory中的实现了Advisor接口的类,例如BeanFactoryTransactionAttributeSourceAdvisor),如果有,创建一个该bean的代理(AbstractAutoProxyCreator)。

如果使用了<tx:annotation-driven/>,其中的mode属性默认为proxy,解析到这里的时候,AnnotationDrivenBeanDefinitionParser会自动调用AopNamespaceUtils->AopConfigUtils,而AopConfigUtils会把AnnotationAwareAspectJAutoProxyCreator注册到beanFactory里面去,也就是相当于写了个<aop:aspectj-autoproxy/>的标签。

如果不知道程序应该使用Spring-AOP还是AspectJ,这儿有较好的描述:

http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/aop.html#aop-choosing

Spring-AOP默认使用JDKProxy来代理实现了任何接口的类,如果没有实现任何接口,则使用Cglib2AopProxy来创建代理,对原类的方法调用时,会根据代理类设置的Callback[]进行相应处理。

Spring-AOP默认设置的Callback有

DynamicAdvisedInterceptor(拦截方法调用,检查需要使用的拦截器并执行,例如TransactionInterceptor)

DynamicUnadvisedInterceptor/StaticUnadvisedInterceptor

StaticDispatcher

AdvisedDispatcher(不知道拿来干嘛的)

EqualsInterceptor

HashCodeInterceptor

SerializableNoOp(貌似没啥用?)

解决手机调试Log不显示的问题

安卓可以通过Log.x输出日志,今天调试的时候,在模拟器上一切正常,但是手机上却啥东西都看不到,连GC信息都没有。以为是项目配置的问题,尝试了各种各样的方案,都无解,最后发现是手机ROM把全部日志关闭掉了。

adb shell  
echo 1 > /sys/kernel/logger/log_main/enable

将1写入日志开关文件,1为开,0为关

修改后一切正常,日志全都出来了。

不知道ROM关闭全部日志是不是会提高手机性能?

Java的垃圾回收器

原文在 http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html ,下面是个人的一些理解,可能有不对的地方。

  • 如果不指定-client或-server参数,JVM会根据系统状况自动判断使用client还是server模式,具体见这里。有趣的是,AMD64的CPU默认就是server模式(经测试,windows下仍然默认client模式),其他CPU时,除windows默认为client模式外,其他>=2C2G的电脑都会默认为server模式。
  • 如果不指定使用哪种垃圾回收器,JVM会根据应用程序运行的硬件情况(CPU/内存)来自动决定使用哪种GC、堆栈大小以及运行时编译器,也就是说,如果运行应用程序时不指定GC的参数,那么不同的硬件上使用的垃圾回收器可能是不同的。
  • 可以通过指定应用允许的最大暂停时间(full GC时程序会失去响应)和吞吐量来调节GC,而不是仅仅通过指定堆栈大小等参数来调节。
  • JVM中内存区域分为新生代、年老代和持久代,其中新生代又分为eden和两个 survivor 区,如下图(适用于 serial collector ):

大多数对象创建时,先存放在eden区,当eden区满了的时候,执行一次部分GC,将存活的对象拷贝到survivor 区。
两个survivor 区中,其中有一个始终为空的状态,执行部分GC时,另一个survivor 区以及eden区中的live对象会被全部拷贝到这个区,这样来回移动移动数次,仍然存活的对象将拷贝到年老代。至于一个对象需要拷贝多少次才能移动到年老代,是在每次GC时由JVM决定的,JVM决定这个值的大小,以确保survivor区处于半满状态(其实根据实际监控来看,应该是保持在大半满靠全满状态),通过XX:+PrintTenuringDistribution可以查看该数值。在Jconsole中,查看survivor 区的时候,只能看到一个区的大小,这是因为永远只有一个survivor 区处于可用状态。
持久代中主要存放程序定义的类、方法等,不属于堆(也有人说是一个特殊的堆,不过从jconsole上看,应该是Non-heap memory),不参与GC。
在程序运行过程中,Eden和survivor区的capacity大小在每次执行GC后都可能发生变化。
上图中Virtual区是指,当-Xms的值比-Xmx的值小时,JVM预留的一部分尚未交付给自己使用的内存。
  • 在启动参数中加上 -verbose:gc 可以打印出每次GC释放的空间以及耗时。
  • JVM GC相关参数:
  • -XX:MinHeapFreeRatio=<minimum>  -XX:MaxHeapFreeRatio=<maximum>   -Xms<min>  -Xmx<max> 当某一代的内存超出了Min/MaxHeapFreeRatio的比例时(默认为40%-70%),JVM会自动伸缩该代内存大小以确保在指定比例范围内,但自动伸缩不会超出该代允许的内存大小范围。由于系统默认的-Xmx和-Xmx都比较小,对于服务器程序,强烈建议启动时指明-Xms和-Xmx的值,并且这两个值相等,这样可以避免程序自己决定内存大小带来的开销,当然,如果这个值设置得不合适,JVM就无能为力了。
  • – XX:NewRatio=<3> 指定新生代和年老代在堆内存中的比例(1:3),即新生代占总heap区的1/4。 NewSize 和 MaxNewSize 可用来指定新生代的实际内存大小。
  • -XX:SurvivorRatio=<6> 指定eden区和每个survivor区的比例(1:6),即每个survivor占新生代的1/8(这里感觉有点奇怪,感觉应该是survivor比eden=1:6,但是官方文档这么说: sets the ratio between eden and a survivor space to 1:6 )。一般来说,调节这个参数对性能调优不是太重要。如果survivor区太小,GC时不够用来装来自eden区的live对象,那么这些对象会直接溢出到年老代,如果这个区太大,那么将会浪费掉一部分内存。
垃圾回收器的类别:
  • serial collector 使用单线程来完成GC操作。由于不涉及到线程之间的通信,所以非常高效。如果只有一个CPU核心,或者程序使用的内存不会超过100M时,建议使用这种GC。这种GC不能有效利用多核,当系统只有单核CPU时,JVM会默认使用这类GC。估计现在已经很少见了。
  • parallel collector ( throughput collector ) 在小GC时(新生代GC),可以显著减少GC的开销。在多核或者多线程的硬件环境下,当应用使用的内存为中到大的规模时,应该使用这类GC。当系统有多核的时候,JVM会默认使用这种GC。默认情况下,这类GC仅在小GC时使用多线程,JDK6中,可以使用 -XX:+UseParallelOldGC 的参数来让FULL GC(Old区GC)也使用多线程。使用的线程数等于系统的核数,或者用- XX:ParallelGCThreads=<N> 来指定。由于GC时可能有部分对象从新生代移动到年老代,所以每个GC线程都会预留一定的年老代空间,这样可能会产生年老代碎片,减少GC线程数可以减少这种碎片化的影响,增大年老代空间。
  • concurrent collector 通过牺牲应用性能来缩短GC时的服务停顿。使用 -XX:+UseConcMarkSweepGC 参数打开。
垃圾回收器的选择:
  • 如果应用程序占用的内存不超过100M: -XX:+UseSerialGC
  • 如果应用程序运行在单核服务器上并且对应用暂停服务没啥要求:  -XX:+UseSerialGC
  • 如果应用程序的峰值处理能力最重要,并且对应用暂停服务一秒种或者更长时间可以接受:  -XX:+UseParallelGC  -XX:+UseParallelOldGC 此类情况多出现在后台跑的业务中,非用户交互的系统。
  • 如果希望尽量缩短应用程序的暂停响应时间: -XX:+UseConcMarkSweepGC 此类情况较为常见,主要为涉及到用户交互的系统。由于可以通过多台服务负载均衡来弥补性能损失,而且可以保证服务不会在那里傻呆着几秒钟甚至几十秒钟来GC而没反应。
当使用并行GC时,JVM中内存分区和串行不同,如下图:
JVM的下一代G1垃圾回收器会逐渐取代传统的CMS垃圾回收器,但是JDK1.7.2目前还没有使用G1作为默认的垃圾回收器。G1的内存分布和上面的两种分布有非常大的不同,有兴趣的可以去看看。
系统的剩余可用内存空间可能会对垃圾回收性能产生严重影响。我有两台硬件完全相同的服务器,部署完全相同的应用,负载基本相同,但是垃圾回收的性能有本质差别,1台135次FULL GC耗时1分钟,另外一台82次FULL GC耗时34分钟。现在能找到比较明显的不同就是,一台剩余物理内存900M左右,另外一台剩余物理内存40M左右,不知道是不是由于这个原因造成了GC性能如此显著的区别。

Java wait/notify中的坑

近日在基于Netty写异步HttpClient的时候,需要等http连接建立并通道打开后,才能使用该连接来发送数据,但是Netty中只能等待到连接建立就会返回一个用来收发数据的channel,如果channel并没有打开,用来发送数据时就会报错,因此需要在代码中等到channel打开后再返回,想到了使用简单的wait&notify来解决,先上一段代码:


public class HttpClient{
    private Boolean connected = false;

    private final class InnerHandler extends SimpleChannelUpstreamHandler {
        @Override
        public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
            synchronized(connected){
                connected = true;
                channel = e.getChannel();
                logger.debug("{}",this);
                connected.notifyAll();
            }
            logger.debug("channelConnected");
        }
    }

    public void connect() throws Exception {
        ......InnerHandler hander = new InnerHandler();......
        ChannelFuture future = bootstrap.connect();
        future.await();

        synchronized(connected){
          if (future.isSuccess() && !connected) {
            logger.debug("connection opened,waiting for channel connected!");
            logger.debug("{}", hander);
            connected.wait();
          }
          logger.debug("new http client channel established!");
        }
    }
}

结果程序运行到connected.wait()便死锁了,经分析发现,connected.wait()和connected.notifyAll()进行操作的不是同一个对象,因为connected = true这里将connected 重新指向了另一个对象,代码上看来connected.notifyAll()没啥问题,其实这个信号已经没人收得到了,开始的那个connected.wait()也再也得不到任何信号,会一直等下去了。
修改代码,使用一个专门的对象来做锁:
private Object lockObject = new Object();
将synchronized、wait和notifyAll使用的对象都换为lockObject,一切正常。可见,使用对象锁的时候,尽量使用一个不会被潜在改变引用地址的对象做锁,最好专门新建一个Object来做锁。
当然只要保证synchronized、wait和notifyAll使用的是同一个对象,不专门弄个Object来做锁也是可以的,为了多搞点问题出来加深印象,将代码修改了一下:


public class HttpClient{
    private boolean connected = false;

    private final class InnerHandler extends SimpleChannelUpstreamHandler {
        @Override
        public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
            connected = true;
            channel = e.getChannel();
            synchronized(this){                
                logger.debug("notify:{}",this);
                notifyAll();
            }
            logger.debug("channelConnected");
        }
    }

    public void connect() throws Exception {
        ......InnerHandler hander = new InnerHandler();......
        ChannelFuture future = bootstrap.connect();
        future.await();

        if (future.isSuccess() && !connected) {
          logger.debug("connection opened,waiting for channel connected!");
          synchronized(hander){            
            logger.debug("wait:{}", hander);
            hander.wait();
          }
          logger.debug("new http client channel established!");
        }
    }
}

使用hander对象来做锁,这儿的代码已经确保大家使用的都是同一个hander,运行代码,发现一切正常,再运行几次,发现又死锁了,分析debug信息:
connection opened,waiting for channel connected!
notify: com.skymobi.http.HttpClient$InnerHandler@cc52fc
channelConnected
wait: com.skymobi.http.HttpClient$InnerHandler@cc52fc
原来程序执行时先跑去notify,然后再wait了,开始时不是很清楚为啥一定要加个synchronized,只是不加的话就不能用来wait,于是囫囵吞枣的加上个synchronized。这儿看来,加上synchronized应该是获取到锁,然后修改某些状态值,供别的线程根据这些状态值去判断是否需要做某些事情,一般synchronized中如果要使用wait,都需要先判断是否满足需要wait的条件,否则就会导致死锁,而在notify的synchronized代码片段中,一般会对判断条件使用的值进行修改,然后再通知wait的线程。
最后修改过能运行的代码如下:


public class HttpClient{
    private boolean connected = false;

    private final class InnerHandler extends SimpleChannelUpstreamHandler {
        @Override
        public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {

            synchronized(this){          
                connected = true;
                channel = e.getChannel();      
                logger.debug("notify:{}",this);
                notifyAll();
            }
            logger.debug("channelConnected");
        }
    }

    public void connect() throws Exception {
        ......InnerHandler hander = new InnerHandler();......
        ChannelFuture future = bootstrap.connect();
        future.await();

        synchronized(hander){     
          if (future.isSuccess() && !connected) {
            logger.debug("connection opened,waiting for channel connected!");            
            logger.debug("wait:{}", hander);
            hander.wait();
          }
          logger.debug("new http client channel established!");
        }
    }
}

RHEL中yum环境设置

Ubuntu下的apt相当好用,可惜现在服务器的操作系统很少用到Ubuntu,大部分都是安装的Red Hat Enterprise Linux,RHEL的yum库要付费给Red Hat,所以需要把RHEL升级到CentOS,然后使用CentOS的免费yum库。

1.安装安全证书

rpm –import http://mirror.centos.org/centos-5/5/os/x86_64/RPM-GPG-KEY-CentOS-5

2.安装release package

rpm -Uvh http://mirror.centos.org/centos-5/5/os/x86_64/CentOS/centos-release-notes-5.8-0.x86_64.rpm

rpm -Uvh http://mirror.centos.org/centos-5/5/os/x86_64/CentOS/centos-release-5-8.el5.centos.x86_64.rpm

3.rpm -qa | grep yum

卸载 原有的yum

4. 装上新CentOS的yum

rpm -Uvh http://mirror.centos.org/centos-5/5/os/x86_64/CentOS/yum-metadata-parser-1.1.2-3.el5.centos.x86_64.rpm

rpm -Uvh http://mirror.centos.org/centos-5/5/os/x86_64/CentOS/yum-fastestmirror-1.1.16-14.el5.centos.1.noarch.rpm

强制安装

rpm -Uvh –nodeps http://mirror.centos.org/centos-5/5/os/x86_64/CentOS/yum-3.2.22-26.el5.centos.noarch.rpm

5.更新

yum update

至此便可以正常使用yum来管理软件包了