使用 spring AOP 监控代码的执行耗时

前言

使用spring提供的aop功能,我们可以很方便的实现动态代理的功能。在使用上,spring提供了两种不同的实现,分别是Spring AOP 和 AspectJ。

提供方式以及对比

spring AOP

概述

  1. 纯java实现,不需要额外的编译流程,不需要引用其他三方包。
  2. 适合集成到Servlet容器或者应用服务中
  3. 仅支持方法级别的代理,不支持成员变量
  4. 设计宗旨是集成IoC,并且有效的解决大部分企业级应用中的需求,不同于AspectJ的细粒度。
  5. 与AspectJ进行互补

非侵略性是spring设计的一个中心原则,一般情况下,不会有spring的代码存在与业务代码中。但是某些情况中,不是这样,比如注解。

实现

默认使用标准JDK中的 动态代理是实现,针对任意接口,都可以实现。

可以配置使用CGLIB进行代理,可以针对类进行代理。CGLIB的使用,对开发者是透明得到,在针对没有实现接口的类进行代理的时候,spring会自动使用CGLIB进行实现。

一般情况下,建议业务类都实现一个接口,是比较好的编程实现

这个另说把,不必要的接口实现了以后,除了繁琐没有其他作用

在一个类实现了多个接口的时候,可以强制使用CGLIB。

很多考虑的地方 spring文档
xml
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>

案例

项目需要统计一下一个service类的核心方法执行时间

但是通常,一个核心方法内部有很多的子方法,如何在做代理的时候,将所所有方法进行代理

本次的实现,使用了AspectJ。

代码如下:

代理配置

package com.xxx.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * Created by xxxx on 17/3/22.
 */
@Aspect
@Component
public class CostTimeAspect {

    @Around("execution(* com.xxx.service.AService.*(..))")
    public Object printTimeMethod(ProceedingJoinPoint pjp) throws Throwable {

        long time = System.currentTimeMillis();
        Object obj = pjp.proceed();
        long cost = System.currentTimeMillis() - time;
        if (cost > 0) {
            System.out.println(pjp.getSignature().toShortString() + "costs mills:" + cost);
        }
        return obj;
    }

}

spring.xml

<aop:aspectj-autoproxy />

pom.xml

需要在 build 节点中增加,并且要适配指定的jdk版本

<plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>aspectj-maven-plugin</artifactId>
            <version>1.4</version>
            <dependencies>
                <dependency>
                    <groupId>org.aspectj</groupId>
                    <artifactId>aspectjrt</artifactId>
                    <version>1.7.3</version>
                </dependency>
                <dependency>
                    <groupId>org.aspectj</groupId>
                    <artifactId>aspectjtools</artifactId>
                    <version>1.7.3</version>
                </dependency>
            </dependencies>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>test-compile</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <outxml>true</outxml>
                <verbose>true</verbose>
                <showWeaveInfo>true</showWeaveInfo>
                <aspectLibraries>
                    <aspectLibrary>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring-aspects</artifactId>
                    </aspectLibrary>
                </aspectLibraries>
                <source>1.6</source>
                <target>1.6</target>
            </configuration>
        </plugin>

Java线程安全-《深入了解Java虚拟机》读书笔记

Java线程安全

《深入了解Java虚拟机》读书笔记

阿姆达尔定律(Amdahl law)

Amdahl加速定律定义了一个系统进行并行化改造时候可以提升的性能占比

公式大约如下:
$$
S’=\cfrac{1}{1-f+\cfrac{f}{S}}=\cfrac{1}{1-(1-\cfrac{1}{S})f}
$$

其中,

S’=是整体加速比

f=加速的部分占到整体系统的比重

S=加速了的部分的加速比重

举例:假设某个程序中,你优化了80%的代码,对这80%的代码你获得了加速比10,那么对整个程序而言,你的优化获得的加速比为:1/(1–0.8+0.8/10)=3.57,这远小于10。

S无限增大时候,S’逼近
$$
\cfrac{1}{1-f}
$$
也就是说,优化程序80%的代码,最大获得的加速比为5。

线程安全的定义

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的

——Brian Goetz《Java Concurrency In Practice》

Java中各种操作共享数据分类

不可变

Immutable,JDK1.5内存模型被修正以后的Java语言

Java语言中,如果共享数据是一个基本数据类型,只要在定义时使用final关键字修饰,可以保证是不可变的。

如果是一个对象,那就需要保证对象的行为不会对状态产生任何影响,比如java.lang.String中,substring()、replace()、concat()等方法,都不会影响它原来的值

保证对象行为不受影响的途径,最简单的就是把对象中带有状态的变量都声明为final,比如Integer类

Java API中,不可变的类型包括

  • String
  • 枚举类
  • java.lang.Number的部分子类
  • 不含AtomicInteger,AtomicLong,这两个类使用unsafe 的CAS操作 进行实现

绝对线程安全

完全满足Brian Goetz 提出的要求,调用者也不需要额外的同步措施,Java中基本没有类似的实现。

即使是在全部方法中使用了 synchronized 关键字修饰的 Vector类,在多线程的场景中调用方法,也还是会出现线程问题,虽然每个操作都是原子的,都会线程安全,但是多个原子操作的顺序却可能导致线程问题

相对线程安全

通常意义上的线程安全,保证一个对象单独的操作是线程安全的即可。Java中,大部分线程安全类都是属于这种类型,比如Vector、HashTable、Collections的synchronizedCollection()等

线程兼容

指对象本身不是线程安全的,需要调用者进行正确的同步手段来保证对象在并发环境中可以安全使用。与之前Vector和HashTable对应的ArrayList、HashMap等就是。

Java中绝大多数的API属于这类型

线程对立

指无论调用端是否采取了同步措施,都无法在多线程的环境中并发使用的代码。

Java中此类例子很少,常见的对立的例子如下:

  • Thread类的 suspend()方法以及resume()方法,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时候是否同步,目标线程都有死锁的风险。如果subpend()中断的线程就是即将要执行resume的那个线程,那就肯定要死锁了。由于以上原因,suspend()、resume()方法已经被JDK声明废弃了
  • System.sctIn()
  • System.setOut()
  • System.runFinalizersOnExit()

线程安全实现方式

互斥同步

悲观锁的思路体现、也称阻塞同步

常见的并发正确性方案,同步指的是多个线程并发访问共享数据的时候,保证共享数据在同一时刻只被一个或者一些(信号量时候)线程使用。

互斥是实现同步的一种手段,主要的实现方式如下,互斥是因,同步是果,互斥是方法、同步是目的

  • 临界区
  • 互斥量
  • 信号量

Java中,synchronize关键字是最基本的互斥同步手段。synchronized关键字经过编译以后,会在同步块的前后分别形成monitorenter和 monitorexit两个字节码指令。

字节码需要一个reference类型的参数来指明要锁定和解锁的对象

  • 如果java程序中的synchronized明确指定了对象参数,那就是这个对象的reference
  • 如果没有指定
  • synchronized修饰的是实例方法,取对应的对象实例
  • 修饰静态类方法,取Class对象来作为锁对象

monitorenter 进行锁对象计数器+1,monitorexit进行锁对象计数器-1。

synchronized同步块对于同一线程来说是可重入的,不会出现自己把自己锁死的问题。

阻塞或者唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转化为和心态,因此状态转换会耗费很多的处理器时间。对于简单的同步块,比如getter方法,状态转换可能比用户代码执行的时间还要长。

synchronized在java中是一个重量级操作,非必要情况下不要使用。当然JVM也会做一些优化

使用ReentryLock实现锁

reentryLock与synchronized很相似,具备一样的线程重入特性,不过增加了一些高级功能

  • 等待可中断
  • 当等待锁的线程等待时间太长,可以中断等待,改为处理其他事情
  • 公平锁
  • 多个线程等待同一个锁时候,必须按照申请的时间顺序来依次获得锁,synchronized是非公平的
  • reentryLock可以通过构造参数选择是否公平
  • 锁绑定多个条件
  • 一个ReentryLock可以同时绑定多个Condition对象,synchronized中,wait\notify\notifyAll方法可以实现一个隐含的条件
  • newCondition可以实现多个条件
  • JDK1.6以上,ReentryLock与synchronized的性能完全持平,没有上述场景的前提下,建议使用原生的synchronized实现功能

非阻塞同步

随着硬件指令集的发展,基于冲突检测的乐观并发策略称为 非阻塞同步,区别于互斥同步,是一种先进行操作,发生冲突以后补偿的乐观锁思路。

这种策略需要操作和冲突检测这两个步骤具有原子性(当然不是使用synchronized。。。),常用指令有:

  • 测试并设置(Test-and-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Compare-and-Swap,CAS)
  • 加载连接、条件存储(Load Linked/Store Conditional,LL/SC)

前3条是老的指令,后2条是现代处理器新增加的

CAS

三个操作数,分别是内存位置V、旧的预期值A、新值B

CAS操作的时候,当且仅当V符合旧的预期值A时候,处理器会用新值B更新V值,否则不执行

JDK1.5以后,可以使用sun.misc.Unsafe类中的对应方法实行,并且限制了只有启动类加载器加载的class才能访问它,因此,如果不采用反射手段,只能通过其他Java的API来间接使用

AtomicLong等就是这样实现的

无同步方案

没有或者不涉及共享数据的前提下,可以采用这种方案。简单介绍2类

  • 可重入代码(Reentrant Code)所有的的可重入代码都是线程安全的。可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源,用到的状态量都是由参数中传入、不调用非可重入的方法等。
  • 一个简单的原则可以判断:如果一个方法,它的返回结果可以预测,只要输入了相同的数据,都能返回相同的结果,那就满足可重入的要求
  • 满足条件:

    (1)可以在执行的过程中可以被打断;

    (2)被打断之后,在该函数一次调用执行完之前,可以再次被调用(或进入,reentered)。

    (3)再次调用执行完之后,被打断的上次调用可以继续恢复执行,并正确执行。

  • 可以结合JVM来分析Java中的可重入代码,如果一个函数的所有访问变量,都是以值传递的方式传入局部变量表,那么这个方法就是可重入的。
  • 线程本地存储(Thread Local Storage):在Java中,就是ThreadLocal这个类。共享数g据的可见范围设置在同一个线程以内
  • ThreadLoca类的实现
  • 如果一个变量要被多线程访问,可以使用 volatile关键字声明
  • 经典Web交互模型中,“一个请求对应一个服务器线程”的处理模型

锁优化

自旋锁与自适应自旋

指的是让等待锁的线程通过空循环(自旋),而不进行让出CPU的操作,避免操作系统进行状态切换带来的时间损失。

  • 物理机器需要有一个以上的处理器,能让两个或以上的线程同时并行执行
  • 自旋等待的时间必须有一定的限度
  • -XX:+UseSpinning 开启自旋,1.6以后默认开启
  • -XX:PreBlockSpin 可以修改自旋次数
  • 1.6以后引入的自适应的自旋锁,通过程序运行和性能监控的不断完善,虚拟机对锁进行状况预测

锁消除

如果一段代码中,堆上所有的数据都不会逃逸出去从而被其他线程访问,那就可以认为数据是线程私有的,相当于栈上数据

锁粗化

有一系列的代码需要加锁,那么虚拟机会把加锁同步的范围扩展,粗化到整个操作序列的外部

轻量级锁

JDK1.6引入,在对象头中指定了一个标记位,加锁时候,通过CAS操作将线程栈与对象联系起来

  • 使用CAS操作替代互斥量操作,如果一个对象,只有一个线程要获取锁,那么使用这种情况
  • “对于绝大多数的锁,在整个同步周期内都是不存在竞争的”
  • 如果存在锁竞争,那么除了互斥量的开销以外,还额外发生了CAS操作

偏向锁

JDK1.6引入,同轻量级锁比较,偏向所就是直接将CAS操作也一并省略掉的技术

  • 一个线程在获取对象的锁同时,对象会在标记位中记录下这个线程的id
  • 后续的执行过程中,如果锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步
  • -XX:+UseBiasedLocking JDK1.6默认开启
  • 有的时候禁止偏向锁可以提高性能

redis知识点

redis

概述、适合场景

summary

  • 支持发布订阅、主从复制、磁盘持久化
  • 多种数据结构
  • 短事务
  • 高吞吐量、低延迟
  • geo支持(地理位置支持,geo作为一个单独的数据结构,有自己的增上改查命令)
  • geoadd
Name Type Data Storage options Query types Additional features Ops Latency
Redis In- memory non- relational database Strings, lists, sets, hashes, sorted sets, geo Commands for each data type for common access patterns, and partial transaction support Publish/ Subscribe, master/ slave replication, disk persistence, scripting (stored procedures) 120000 <1ms
MongoDB On-disk non- relational document store Databases of tables of schema-less BSON documents Commands for create, read, update, delete, conditional queries, and more Supports map- reduce operations, master/ slave replication, sharding, spatial indexes 3500 <100ms
Memcache In-memory key-value cache Mapping of keys to values Commands for create, read, update, delete, and a few others Multithreaded server for additional performance 80000 <1ms
MySQL Relational database Databases of tables of rows, views over tables, spatial and third-party extensions Select, insert, update, delete, functions, stored procedures ACID compliant (with InnoDB), master/ slave and master/ master replication 900 >100ms

Redis介绍

存储实现

内存数据结构

  • redisServer
  • redisDb
    • dict
    • dictht
      • dictEntry(链表)
      • redisObject
        • type
        • encoding
        • ptr
    • expires(过期属性)

redisObject分类

每一种类型的object都有两种实现,分别是压缩和非压缩

  • String
  • int(压缩)
  • sds
  • List
  • linkedlist
  • ziplist(压缩)
  • Hash
  • ht
  • ziplist(压缩)
  • Set
  • ht
  • intset(压缩)
  • Zset
  • skiplist
  • ziplist(压缩)

数据存储类型

  • INT:压缩存储string

  • 常量数字对象共享(0~9999)

  • SDS:存储string

  • int len;

    > int free;
    
    > char buf[]
    
  • 变长字符数组

  • 常用字符串共享

  • 优化长度计算

  • LinkedList 双向列表,存储list

  • 优化长度计算

  • 支持双端遍历

  • HT(hashtable):存储set和hash

  • 扩展、收缩,根据填充绿 used/size

  • 渐进式hash:hash操作、事件触发

  • INTSET:压缩存储set(整数)

  • SKIPLIST:跳表,存储有序集

  • ZIPLIST:压缩存储hash、list、zset

内存管理、淘汰

内存管理
  • Zmalloc(匹配系统,根据系统使用不同的分配算法)
  • Tcmalloc、Jemalloc、Malloc(MAC)
    • 根据不同系统的内存分配长度不同,提前计算,分配指定长度
  • 内存统计
内存淘汰
  • 被动淘汰
  • 达到maxmemory,采用淘汰策略
    • lru
    • volatile-lru/allkeys-lru/
    • random
    • vloatile-random/allkeys-random/
    • ttl
    • vloatile-ttl/
    • Noeviction(simple)
    • 访问时淘汰
  • 主动淘汰(random)

事件模型

网络模型-libae
  • Epoll
  • Select
  • Kqueue
  • Evport(Solaris)
事件类型
  • TimeEvent
  • FileEvent

线程模型

  • MainThread
  • BioThread

重要feature

主从同步

  • 常规同步:异步复制
  • 修改不会直接到从库
  • 主库收到cmd以后,同时修改master_repl_off(repl状态表)
  • 使用real_buf存储最后修改的数据
  • 主库针对 每一个slave有一个output buf
  • 定时 分别将每一个output buf传输到指定的 slave
  • slave返回 real_off响应
  • 主库 根据返回值 修改 master_repl_off表
  • 增量同步
  • Redis状态机

持久化

  • RDB(snapshot)
  • Save(阻塞)/bgsave(非阻塞)
  • 文件格式

    • Redis
    • rdb-version
    • select-db
    • key-value-pairs
    • eof
    • check-sum
    • db-data
    • optional-expire-time
    • type-of-value
    • key
    • value
  • rdbload,全量数据load到内存

  • 每一条数据 checksum检查
  • 通信协议:RESP(redis serialization protocol)
  • 便于实现、解析、理解、二进制安全
  • AOF(oplog)
  • 三种配置, 进行fsync
    • AOF_FSYNC_NO
    • AOF_FSYNC_EVERYSEC(每一秒写入)
    • AOF_FSYNC_ALWAYS(每一条cmd写入磁盘)
  • AOF load(fake client)
  • AOF rewrite
    • merge 不同的cmd,聚合cmd,最后只有一个cmd

集群方案

Twemproxy

​ 高效的路由转发

  • 根据key哈希,数据分片
  • 实现
  • client_in
  • server_in
  • server_out

redis-cluster

​ P2P

  • Gossip协议
  • Auto failover
  • replicas migration
  • online reshard,多个实例会成为一个分片

Mysql&Redis增量同步方案

  • 通过proxy分发请求
  • 写请求发送mysql
  • 读请求发送redis
  • Mysql-binLog 获取数据库增量更新
  • Databus、Broker进行消息队列写入
  • Redis根据队列内容增量更新

大数据系统性能分析

大数据系统性能分析

单机瓶颈分析

增加并发和吞吐

增加并发的方式

  • 主要是增加对CPU的利用
  • 多机
  • 单机多CPU
  • 单CPU多core
  • 单core超线程

方法

  • IO密集型应用:降低线程切换对cpu的浪费
  • 多进程 – 多线程 – 事件驱动 – 协程
  • CPU 密集性应用:增加计算的并发
  • 多进程 – 多线程
  • 除非资源到了瓶颈,否则不排队
  • 合适的并发模型
    • 比如ependingpool、事件触发、异步队列等
  • 队伍要均衡
    • 保证能够打到充分的并发
  • 过长的队伍、及时柔性处理(可丢服务)
    • 大数据系统中经常遇到数据堵塞等场景,需要有良好的柔性处理机制,如优先级机制,清除过期数据的机制,部分服务可丢等机制来解决
  • 能并发的任务不串行
  • 并发情况下,影响等待时间的主要是最长的任务时间
  • 串行情况下,是所有任务时间之和

去除不必要的动作

  • 减少网络重连
  • 长连接
  • 降低连接数
  • 连接池
  • 减少线程切换
  • 线程池
  • 减少内存分配和释放
  • 内存池
  • 减少耗时的操作合运算
  • memset,浮点运算,除法,指数,对数运算,慎用 stl
  • 在线转离线
  • 离线提前进行耗时运算

避免冲突

  • 多线程无锁算法
  • 无锁共享数据、无锁数据结构、Copy on write,更新不影响读取
  • HASH冲突解决(经常遇到由于hash冲突或者锁冲突导致的性能下降)
  • 合理的使用锁
    • 锁的时间尽可能短
    • 降低冲突概率
    • 避免死锁
    • 锁的影响范围尽可能小:blockingQueue的分段锁机制

IO优化

关于磁盘I/O的性能,引用一组Kafka官方给出的测试数据(Raid-5,7200rpm)

  • SequenceI/O: 600MB/s
  • RandomI/O: 100KB/s

随机修改

  • WAL(write ahead
    logging):随机写入转化为顺序的写入,写入成功即可返回,在故障时候通过 log 恢复
  • LSM-Tree:适合的应用场景:insert数据量大,读、update数据量不高,并且一般针对最新数据
  • 方法:写入数据到内存即返回,缓冲到一定量再写入磁盘;读取时候需要merge磁盘读取合内存中的内容(bigtable,
    tera)
  • 减少IO次数,批量去重
  • 适用于请求中重复度较高的,如链接库的写入,后链的特点就是重复度很高,批量去重能够去除较大部分的重复数据,降低对后端服务的io压力

随机读取

  • 尽量减少IOPS,无cache的情况下做到每请求只消耗1IO
  • 通过优化cache淘汰算法提高cache命中率
  • 基于统计的淘汰策略
  • 多级LRU队列
  • 合理李勇Flash存储,通过压缩等手段降低Flash压力

其他

  • 预处理,预充Cache,预热再服务
  • 充分利用硬件
  • 存储速度: 内存 >> SSD >> sata
  • 示例:kafka (顺序读写 + page cache)
    • 顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证
    • 充分利用pagecache,直接内存读取直接发送

集群瓶颈分析

减少数据传输量

数据压缩-CPU与网络io的权衡

  1. 减少跨机房io
  2. 打包访问
  3. 减少交互次数
  4. 数据压缩:CPU与网络IO的权衡

压缩算法对比

数据来自于hbase
| 算法 | remaining(%) | Encoding | Decoding |
| ———— | ———— | ——– | ——– |
| GZIP | 13.4% | 21MB/s | 118MB/s |
| LZO | 20.5% | 135MB/s | 410MB/s |
| Zippy/Snappy | 22.2% | 172MB/s | 409MB/s |

Snappy 在 Google 内部被广泛的使用,从 BigTable 到 MapReduce 以及内部的 RPC
系统

均衡

  1. 负载均衡,常用,一个好的负载均衡方法是保证整个分布式系统性能的基础

  2. RR,Random,Locality-aware,hash

  3. 热点+打散

  4. 自动拆分和融合节点

  5. 自动伸缩容量,弹性

  6. 消除长尾(比如分布式系统中有一个实例老是响应时间长,此时可以屏蔽这个节点)

  7. 拆分、并发

  8. 消峰、限流、缓冲+延迟处理(优先级机制)

1.
消息队列中使用优先级方式,可以一方面保证高优请求很快得到处理,另一方面达到全局缓冲

  1. 丢弃、降级服务

  2. 丢弃是说检测到服务扛不住的时候,自动丢弃一些请求
    2.
    降级这里说的是人工方式处理,比如搜索服务中,在高峰期可以关闭广告处理、甚至关闭另一个引擎

案例:mr任务老是跑很长时间,个别子分片总是运行不完

均匀分片

擅用cache

cache种类
  1. 内存 cache、分布式cache
  2. 有结果 cache/ 无结果cache/ 超时cache
  3. 只读cache/ 读写 cache
提升命中率
  1. 合理的cache key 设计
  2. 需要全局考虑一下,兼容各种访问

  3. 有效的淘汰算法

  4. 保存命中率高的item

案例:上游hash寻址下游(us寻址gss)

  1. 容易造成热点问题

Cache对延迟的优化效果

  1. 节省大量耗时操作时间:不必要的计算、网络、IO

  2. 案例7 均值200ms的服务,加了cache,命中70%

  3. 200×0.3+1×0.7=67ms

系统优化

容器+混步

  1. IAAS
  2. matrix
  3. PAAS
  4. Jpass
  5. Galaxy
  6. Beehive

全量模型->增量模型

  1. 适用于 全量数据量大,而增量更新比例小的情况
    1. 实例 linkbase3.0,将连接库从全量+patch
      改造为增量实时读写模型,节约8000槽位x36小时的计算资源
    2. spider实时统计策略,从1500太机器节约到500台以下

避免局部瓶颈

分布式环境下,每个子系统都非常重要

​ 木桶效应

案例9:一个分布式系统,消息队列发给模块a,模块a负责读取存储系统,merge数据,在写回存储系统(即
read-modify-write模型),性能非常低,加并发、加机倌增量更新比例小的情况
1. 实例 linkbas点影响了整体服务

分治的方法,让A模块只处理匹配分片的存储资源,不要全局访问节点

这样出现故障的话,不会影响全局的性能

不要牺牲可维护性

​ 尽量避免设计过度复杂的系统,人力成本也是成本,一定要可维护性高

tera + 每秒400W qps

类似 google 的 bigtable

【转载】PhantomReference and Finalization

本文翻译至:PhantomReference and Finalization

软引用,弱引用以及虚引用

软引用(SoftReferences),比较典型的应用是在内存缓存的场景中。JVM会尽可能地将对象保留在内存中,当JVM内存不足的时候,才会从最早的references开始清除。根据javadoc中的描述,整个清除过程是没有保障的。

弱引用(WeakReferences)是我最经常使用的类型。典型的用途是在创建一些弱引用的监听器(Listener),或者是想收集某个对象的额外信息(使用WeakHashMap)的场景中。非常有助于降低类耦合度

其实笔者读到这里的时候,是产生了一些疑问的。为何使用weakHashMap可以降低类耦合度?设想一下使用了WeakHashMap的场景,weakhashmap可以优雅的解决内存释放的问题,但是如果没有WeakHashMap的话,那么实现就会复杂许多。可以在对象不在使用的时候,将它从Map中移除。这就需要容易管理者构造一个清理的函数给对象调用者使用,或者使用一个监听器模式。比如在编写一些使用者非常广泛的api类型的代码时候(比如jdk的api),添加这样的函数可能会使使用者的api变得非常复杂。

虚引用(Phantom references)则适用于在垃圾回收之前进行的预处理,比如需要释放一些资源的场景。遗憾的是,很多开发者会使用finalize()方法去执行这些操作,这并不是一个好的方式。finalize方法如果没有小心的使用在恰当的线程,恰当的时机,那么很可能会对应用造成可怕的性能影响,甚至会影响应用的数据完整性。

在虚引用的构造方法中,开发者需要显式的指定一个ReferenceQueue去将已经标记为“phantom reachable”的对象加入ReferenceQueue队列中。“phantom reachable”代表连虚引用本身都引用不到的对象。最令人迷惑的是即使Phantom references继续保持着私有对象的引用(区别于软引用以及弱引用),get方法也会返回一个null。这样一来,一旦进入这个状态的对象就无法再一次获得强引用。

开发者可以一次一次的对ReferenceQueue调用poll()方法,检测是否有新的PhantomReferences进入“phantom reachable”状态。正常的写法中,可以使类继承于java.lang.ref.PhantomReference,以保证引用的对象只垃圾回收一次,然后无法继续被获取。

PhantomReference 以及 finalization的细节

对PhantomReference 来说,最常见的误解会认为它是被设计用来“修复”finalizers 带来的对象逃逸问题。举个例子来说,我们常常会这么说:

虚引用可以避免finalize()带来的基础问题:finalize()方法可以通过创建一个新的强引用,使自身免于垃圾回收而进行“逃逸”。所以,重写了finalize()方法的对象,需要至少在两条分别的垃圾回收链中,才会被正确的回收。

然而,使用了虚引用,也有可能使对象出现逃逸,请看以下的代码

由此可见,表面上看,引用类型非常有可能是通过成员变量 Reference#referent 指向那些已经失去引用的对象。但是实际上,垃圾回收器对对特定的对象产生了一个意外。这一现象也直接对上文中的结论产生了冲突:

虚引用只用对象在实际的内存空间中被移除时候,才会执行enqueued操作。

到底哪种说法是正确的, javadoc是这样说明的:

Phantom references are most often used for scheduling pre-mortem cleanup

所以,如果虚引用并不是设计用来修正finalize逃逸的问题(这个问题非常严肃,曾经被Lazarus、Jesus 以及许多其他学者指出),那么虚引用究竟有什么作用?

finalize()方法实际是通过垃圾回收线程去执行的,即使在简单的单线程应用中,考虑到潜在因素,也可能出现并发问题(比如错误的将共享状态放入同步方法中等)。但是使用了虚引用的话,你可以制定执行出队操作的线程(在单线程程序中,指定的线程会周期性的做这个任务)

使用 WeakReference 的话,会如何?

弱引用看起来也会满足垃圾回收之前的内存清理场景。区别在于合适进行引用的入队操作。PhantomReference会在执行finalization之后入队,而WeakReference会在之前。对于finalize()方法中没有关键实现的对象来说,不受影响。

但是对于那些需要在finalize()方法中执行一些清理的对象,就会有些许不同

(PhantomReference’s get() 方法总是返回null)。开发者需要存储尽可能多的状态信息,去进行清理操作。举个例子,清理array中的对象,设置为null以后,开发者需要记录下来array中对象的下标,方便后续跟踪查看。对于这类型操作,可以将类继承于PhantomReference,然后创建这个类的实例。

下面更进一步的说说。

想象一个场景:一名开发者准备在某个对象中编写一段清除钩子的代码(通过 finalize()或者是通过[Weak|Phantom]Reference),当这个对象仅仅有属于线程栈空间的强引用(比如局部变量)的时候,开发者调用了一个方法,那么这时,可能发生这样的事情:

出于性能的考虑,JVM会检测是否这个对象有失去引用的可能。所以,在执行方法的过程中,finalization 可能被并发的执行。这样可能导致一些不可预料的结果(finalization 可能修改了一些类内部的状态,比如其他方法也会使用这些状态)。这种情况非常罕见,可以采取以下的方式修复:

这种情况仅仅适用于那些仅仅在线程栈中持有的对象:
– 对象重写了finalize()方法。
– 有一个[Weak|Soft|PhantomReference]引用指向这个对象,同时已经进入了ReferenceQueue,有另外一个线程进行dequeue的操作

总结一下,最安全的清理机制,是通过PhantomReference以及ReferenceQueue,在同一个线程下进行清理。如果是启用了另一个线程,那么就需要使用同步方法快

记一次php+apache调优

近日手头拿到了一个调优问题,详细一看,发现是apache+php问题,问题的具体表现如下:

正常运行的服务常常停止响应, 一旦停止响应, 维护人员就不得己需要去重启apache服务

对php不熟,调查过程也顺便学习一下php的基本配置。

2015-3-11 调试

首先去服务器进程看了看,发现有一个 http.exe 在后台一直占用%50左右的 cpu,即使通过 Moniter 结束了服务,这个进程也依然坚挺,始终占用固定的CPU, 打开perfmon.exe,发现 内存换页频繁,且都是硬错误,估计就是那个进程导致的,直接结束之。 结果cpu一下子就下来了。

又研究了部分参数设置,尤其是 mpm_module 相关的设置
通过以下命令可以查看加载的模块

发现mpm_module的的加载情况和实际配置不符,windows系统中,其实只加载了后者,实际配置如下:

apache官方文档中有如下说明

AcceptEx()是一个微软的WinSock2 API ,通过使用BSD风格的accept() API提供了性能改善。一些流行的Windows产品,比如防病毒软件或虚拟专用网络软件,会干扰AcceptEx()的正确操作。如果遇到类似于如下的错误:

[error] (730038)An operation was attempted on something that is not a socket.: winnt_accept: AcceptEx failed. Attempting to recover.

就需要使用这个指令来禁止使用AcceptEx() 。

对于windows操作系统,可以使用专用的mpmwinntmodule,来提高性能,但是参数配置要注意。

2015-3-18 调试

观察了几天,发现还是有问题,找错误日志去看了看,发现了一个问题

每次运行到一个阶段,apache就会出现无法回收内存的严重问题,这样一来,用不了多久就得重启了

经过调查,从apache的 bug 一路找过去,最后发现这个是php的一个 bug,但是都没有解决。。。

最后又找了一阵子,发现一个 论坛 中有讨论,找到一句靠谱的话

Looks like php related.
You are using outdated/legacy PHP 4 and Apache 2.0, not anymore supported for quite some time.

It is a known issue with a PHP before 5.3 that it has memory management and stability issues.

#The most common solution for this kinfd of errors was using PHP with Fast CGI (mod_fcgid). I do not know there is still somewhere a version for Apache 2.0.

Also I see also PHP warnings: Undefined variable: idvue libellevue.

随后我找了一个php5.2.7稳定版进行升级,升级以后,做了简单的并发访问,没有打印异常。
继续观察观察

Java知识探究一:关于IO类库

经过组织考察,令我忽然发觉自己在最常用的Java中也有很多不明白的地方,实为平身一大憾事,今天特意抽时间将这些点滴记录下来,与大家一起分享

第一批想整理的知识点如下:

  1. Java的IO探究,IO的整个结构与发展,顺带附上公司某小工写的断点续传代码学习。
  2. Java的异常机制,关于编译时异常和运行时异常的探究。
  3. JavaCommon包的理解,尤其是collection包的一些小看法,其实容器嘛,什么样的Utils也逃不出一些基本的范畴,比如存、取、排序、安全性、校验等等等。

闲话不多说,先开始今天的主题,研究一下IO的整个结构

从体系结构上划分,IO系统总共分为两大模块, IO和NIO(非阻塞),IO诞生于JDK1.4之前,JDK1.4时,产生了NIO,并且借用NIO重构了部分IO的代码,比如FileInputStream中增加了对NIO进行支持的getChannel()方法,再比如Reader和FileReader基本用nio全部重写了。

一、Think in IO

IO从实现上,大致分为字节流和字符流两种:

  1. 字节流。对文件的读写操纵以字节为单位,说的直白一点,就是操作byte,byte数组。对应无符号整数的话,就是read方法的正常返回值范围在[0,255]之间,范围有限的返回值有很多优点,比较有代表性的一个就是可以流来做一个简单的zip实现,算法的话,采用huffman树。当然,一个一个字节操作的话,效率不高,利用Buffer则效率提高不少。但是字节流有个问题,那就是在操作文本文件的时候,对于编码会有很多多余的代码,例子如下
    FileInputStream is = new FileInputStream("F:\\books\\base\\vim常用指令.txt");
            byte[] buff = new byte[BUFFER_SIZE];
            int readSize = 0;
            while ((readSize = is.read(buff)) != -1)
            {
                System.out.println(readSize);
                if(readSize<1024){
                    byte[] tmp = new byte[readSize];
                    System.arraycopy(buff, 0, tmp, 0, readSize);
                    System.out.print(new String(tmp, "GBK"));
                }else{
                    System.out.print(new String(buff, "GBK"));
                }
            }
    
  2. 字符流。以字符作为单元进行操作,Reader内部实现其实就是以char或者char数组作为缓存容器的。操作文本文件时候方便许多。编码采用系统默认的编码格式。找了好久才找到代码的说+_+,代码隐藏的很深,从Reader找到ImputStreamReader,再到StreamDecoder再到nio包中的Charset,最终是优先获取系统中的环境变量,System.getProperties()也可以获取,windows7中文版的话,获取到的是“ file.encoding=GB18030”
    /**
         * Returns the default charset of this Java virtual machine.
         *
         * <p> The default charset is determined during virtual-machine startup and
         * typically depends upon the locale and charset of the underlying
         * operating system.
         *
         * @return  A charset object for the default charset
         *
         * @since 1.5
         */
        public static Charset defaultCharset() {
            if (defaultCharset == null) {
            synchronized (Charset.class) {
            java.security.PrivilegedAction pa =
                new GetPropertyAction("file.encoding");
            String csn = (String)AccessController.doPrivileged(pa);
            Charset cs = lookup(csn);
            if (cs != null)
                defaultCharset = cs;
                    else 
                defaultCharset = forName("UTF-8");
                }
        }
        return defaultCharset;
        }
    

下面详细叙述一下字节流

一、InputStream 和 OutputStream 是两个 abstact 类,对于字节为导向的 stream 都扩展这两个鸡肋(基类 ^_^ ) ;

040832408755959

  1. FileInputStream,打开本地文件的流,常用,有3个构造方法
    public FileInputStream(File file)
    public FileInputStream(String name)
    public FileInputStream(FileDescriptor fdObj) 值得强调,这个构造是不能直接用的,FileDescriptor 相当于打开文件的句柄,可以用一个文件流创建另一个,这样创建的流相当于是一个。一个流关闭的话, 另一个也不能读取。
  2. PipedInputStream,必须与PipedOutputStream一起使用,必须是两个或者多个线程中使用,类似生产者消费者模型, PipedOutputStream将数据写到共享的buffer数组中,通知PipedInputStream读取。有两点注意事项:a) 使用PipedInputStream的read方法时候要注意,如果缓冲区没有数据的话,会阻塞当前线程,在主线程中运行的话,会卡住不动。b)PipedOutputStream所在的线程如果停止,那么PipedOutputStream所使用的资源也会回收,会造成pipe 的“broken”,PipedInputStream的read方法也会报错。“A pipe is said to be broken if a thread that was providing data bytes to the connected piped output stream is no longer alive. ”
  3. FilterInputStream,本身是不能被实例化的,是BufferedInputStream等的父类,其实不创建这个类也可以实现它的子类,这个类内部的方法几乎全部都是复用父类的方法。其实它存在的意义更多是代表一个抽象,意思是在InputStream的基础之上对返回数据进行了重新包装或者处理,处理原因可能各不相同,于是又了各不相同的子类。
  4. LineNumberInputStream,这个类是字节流和字符流转换中的失败产物,已经确定为被废弃,废弃的理由是在字节流中强制的判断读取换行,不考虑编码方面的问题。先不管功能能不能实现,首先从抽象层次上面就有欠缺。挪到字符流里面就皆大欢喜。对应的有LineNumberReader这个类可以使用。具体参见LineNumberReader详解。
  5. DataInputStream,直接读取目标文件的byte,拼接或转化byte为其他基本类型,比如下面方法
    public final int readInt() throws IOException {
            int ch1 = in.read();
            int ch2 = in.read();
            int ch3 = in.read();
            int ch4 = in.read();
            if ((ch1 | ch2 | ch3 | ch4) < 0)
                throw new EOFException();
            return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
        }
    

    对于基本类型可以这样转化,但是对于float和double,各自用了Float类和Double类中的native方法进行转化,想来与操作系统底层有关系。

    public final double readDouble() throws IOException {
        return Double.longBitsToDouble(readLong());
        }
    

    唯一实现的比较复杂的是readUTF方法,需要读取全部数据,必须是符合格式的,需要用DataOutputStream的writeUTF进行对应的写。DataInputStream在实际运用中,还是应该与DataOutputStream一起使用,不然的话,意义不是十分大。

  6. BufferedInputStream,初始化一个8192大小的缓存,提高效率用,调用API上面没有任何不同,只是减少了直接读取系统数据的次数。内部持有一个普通的inputStream,只有缓冲区空了以后,才真正调用inputStream的read去写满缓冲区,所以直接用BufferedInputStream的read方法可以提高效率。
    有点意思的是这个类里面用了一个AtomicReferenceFieldUpdater对象来进行对volatile类型缓冲byte数组的更新和替换,这个类的compareAndSet方法带有原子性质的比较和更新。

    /**
         * Atomic updater to provide compareAndSet for buf. This is
         * necessary because closes can be asynchronous. We use nullness
         * of buf[] as primary indicator that this stream is closed. (The
         * "in" field is also nulled out on close.)
         */
        private static final 
            AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater = 
            AtomicReferenceFieldUpdater.newUpdater
            (BufferedInputStream.class,  byte[].class, "buf");// 创建原子更新器
    ...
    /**
         * Fills the buffer with more data, taking into account
         * shuffling and other tricks for dealing with marks.
         * Assumes that it is being called by a synchronized method.
         * This method also assumes that all data has already been read in,
         * hence pos > count.
         */
        private void fill() throws IOException {
            byte[] buffer = getBufIfOpen();
        if (markpos < 0)
            pos = 0;        /* no mark: throw away the buffer */
        else if (pos >= buffer.length)    /* no room left in buffer */
            if (markpos > 0) {    /* can throw away early part of the buffer */
            int sz = pos - markpos;
            System.arraycopy(buffer, markpos, buffer, 0, sz);
            pos = sz;
            markpos = 0;
            } else if (buffer.length >= marklimit) {
            markpos = -1;    /* buffer got too big, invalidate mark */
            pos = 0;    /* drop buffer contents */
            } else {        /* grow buffer */
            int nsz = pos * 2;
            if (nsz > marklimit)
                nsz = marklimit;
            byte nbuf[] = new byte[nsz];
            System.arraycopy(buffer, 0, nbuf, 0, pos);
                    if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {//进行更新比较, 如果buf对象和buffer相同, 那么进行更新,不同的话,不更新
                        // Can't replace buf if there was an async close.
                        // Note: This would need to be changed if fill()
                        // is ever made accessible to multiple threads.
                        // But for now, the only way CAS can fail is via close.
                        // assert buf == null;
                        throw new IOException("Stream closed");
                    }
                    buffer = nbuf;
            }
            count = pos;
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
            if (n > 0)
                count = n + pos;
        }
    
  7. PushBackInputStream,特点是unread()方法,作用是在读取流的过程中自行添加入字节或者字节数组,进行重新读取,小说中随机插入的广告url倒是可以用这个实现,冷不丁的在读取过程中插入一个urlbyte数组,倒也方便。
  8. ByteArrayInputStream,特点是内存操作,读取的数据全部都在缓存数组中,构造方法如下
    public ByteArrayInputStream(byte buf[])
    public ByteArrayInputStream(byte buf[], int offset, int length)
    
  9. StringBufferInputStream,这个类已经被废弃,原因是错误的对字节流进行向字符流的转化,忽略了编码问题。值得一提的是, 这个类里基本所有部分方法都是线程安全的。swing的某个类中还引用了这个方法。
  10. ObjectInputStream,这个类可以说的比较多
    1. 实现了两个接口,ObjectInut:定义了可以read到的类型,ObjectStreamConstants:定义了读取文件类型的常量,使用readObject时候,区分读取到的对象是什么类型,从序列化的对象进行读取时候,需要通过标志位来判断读取到的是什么对象,这个常量里面定义了这些值, 都是short的。
    2. 拥有一个内部类BlockDataInputStream,这个类的作用是读取基本类型数据时候进行缓存,以提高效率,但是也产生了问题,http://www.tuicool.com/articles/v6RNNr 反序列化和序列化一定注意,建议使用read(byte[],start,end) 替代简单的read(byte[]),使用后者的话, 可能出现读取乱码,内容错误等问题,尤其是音视频, 可能出现杂音,因为ObjectInputStream是根据单个字节来判断数据类型的,所以一定要准确。
  1. OutputStream, 基本每个InputStream都有一个对应的OutputStream,来实现对应的功能,基本全都是抽象方法。
    1. FileOutputStream,FileDescriptor相当于句柄, 既然是句柄, 就会有多个流可能使用之, 所以FileDescriptor有incrementAndGetUseCount方法, 用来线程安全的进行引用计数器+1的操作。另外值得注意的是,FileOutputStream还有追加写入的构造方法
      public FileOutputStream(File file, boolean append)
              throws FileNotFoundException
          {
              String name = (file != null ? file.getPath() : null);
          SecurityManager security = System.getSecurityManager();
          if (security != null) {
              security.checkWrite(name);
          }
              if (name == null) {
                  throw new NullPointerException();
              }
          fd = new FileDescriptor();
              fd.incrementAndGetUseCount();
              this.append = append;
          if (append) {
              openAppend(name);
          } else {
              open(name);
          }
          }
      
    2. PipedOutputStream,需要与InputStream进行配合使用,不在赘述