目录
一 String、StringBuffer、StringBuilder
- String是一个不可变(Imutable)对象
- StringBuffer线程安全
- StringBuilder线程不安全(速度快)
1.1 StringBuffer选择合适的初始化大小
为了实现修改字符序列的目的, StringBuffer和StringBuilder底层都是利用可修改的( char ,JDK 9以后是byte )数组
,二者都继承了AbstractStringBuilder ,里面包含了基本操作,区别仅在于最终的方法是否加了synchronized.
这个内部数组应该创建成多大的呢?
如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加16
(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容
的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行arraycopy.
1.2 自动编译优化
你可以看到,在JDK 8中,字符串拼接操作
会自动被javac转换为StringBuilder操作
,而在JDK 9里面则是因为Java 9为了更加统一字符串操作优化 ,提供了StringConcatFactory ,作为一个统一的入口。javac 自动生成的代码,虽然未必是最优化的,但普通场景也足够了,你可以酌情选择。
二 动态代理原理
动态代理是一种方便运行时
动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装RPC调用、面向切面的编程( AOP )
。
实现动态代理的方式很多,比如JDK
自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制
,类似ASM、cglib (基于ASM )、Javassist 等。
2.1 spring中的两种代理方式
JDK Proxy的优势
- 最小化依赖关系,减少依赖意味着简化开发和维护, JDK本身的支持,可能比cglib更加可靠。
(不用外部依赖
) - 平滑进行JDK版本升级,而字节码类库通常需要进行更新以保证在新版Java.上能够使用。
- 代码实现简单。
基于类似cglib框架的优势
- 有的时候调用目标可能·不便实现额外接口·,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似cglib动态代理就没有这种限制。
- 只操作我们关心的类,而不必为其他相关类增加工作量。
- 高性能。
性能对比
在主流JDK版本中, JDKProxy在典型场景可以提供对等的性能水平,数量级的差距基本
上不是广泛存在的。而且,反射机制性能在现代JDK中,自身已经得到了极大的改进和优化,同时, JDK很多功能也不完全是反射,同样使用了ASM进行字节码(jvm中inflation
)操作。
我们在选型中性能未必是唯一考量,可靠性、可维护性、编程工作量等往往是更主要的考虑因素,毕竟标准类库和反射编程的槛要低得多,代码量也是更加可控的,如果我们比较下不同开源项目在动态代理开发上的投入,也能看到这一-点。
三 IO复制
3.1 普通本地IO操作
当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存
,再切换到用户态将数据从内核缓存读取到用户缓存
。写入操作也是类似,仅仅是步骤相反。
3.2 nio使用零拷贝
基于NIO transferTo的实现方式,在Linux和Unix(需要看底层操作系统是否支持)上,则会使用到零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。
注意, transferTo不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行Socket发送,同样可以享受这种机制带来的性能和扩展性提高。transferTo的传输过程是:
四 直接缓冲区
- Direct Buffer :如果我们看Buffer的方法定义,你会发现它定义了isDirect() 方法,返回当前Buffer是否是Direct类型。这是因为Java提供了堆内和堆外( Direct ) Buffer ,我们可以以它的allocate或allocateDirect 方法直接创建。
- MappedByteBuffer :它将文件按照指定大小
直接映射为内存区域
,当程序访问这个内存区域时将直接操作这块儿文件数据,省去了将数据从内核空间向用户空间传输的损耗。我们可以使FileChannel.map创建MappedByteBuffer ,它本质上也是种Direct Buffer(本质是一样的
).
4.1 优势与适用场景
- Direct Buffer生命周期内内存地址都不会再发生更改,进而内核可以安全地对其进行访问,很多I0操作会很高效。
- 减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高。
但是请注意, Direct Buffer创建和销毁过程中,都会比一般的堆内Buffer增加部分开销,所以通常都建议用于长期使用、数据较大的场景。
4.2 回收建议
- 在应用程序中,显式地调用
System.gc()
来强制触发。 - 另外一种思路是,在
大量使用
Direct Buffer的部分框架中,框架会自己在程序中调用释放方法
, Netty就是这么做的,有兴趣可以参考其实现( PlatformDependentO )。 - 重复使用
Direct Buffer.
五 线程状态
网上的资料对线程状态的分类有很多种,但在java api中明确只有5种
关于线程生命周期的不同状态,在Java 5以后,线程状态被明确定义在其公共内部枚举类型java.lang.Thread.State
中,分别是:
- 新建(NEW) ,表示线程被创建出来还没真正启动的状态,可以认为它是个Java内部状态
- 就绪( RUNNABLE) , 表示该线程已经在JVM中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它CPU片段,在就绪队列里面排队。在其他一些分析中,会
额外区分一种状态RUNNING
,但是从Java API的角度,并不能表示出来。 - 阻塞( BLOCKED ) , 这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待
Monitor lock。比如,线程试图通过synchronized去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。 - 等待( WAITING) , 表示正在
等待其他线程采取某些操作
。一个个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待( wait)
, 另外的生产者线程去准备任务数据,然后通过类似notify等动作,通知消费线程可以继续工作了。Thread.join()
也会令线程进入等待状态。- 计时等待( TIMED_ WAIT ) , 其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如wait或join等方法的指定超时版本
- 终止( TERMINATED) , 不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。
5.1 注意线程无故唤醒场景
六 守护线程
有的时候应用中需要一个长期驻留
的服务程序 ,但是不希望其影响应用退出,就可以将其设置为守护线程,如果JVM发现只有
守护线程存在时,将结束进程
,具体可以参考下面代码段。注意,必须在线程启动之前设置
。
t1.setDaemon(true);
七 慎用ThreadLocal
ThreadLocal使用完,需要调用remove
方法显示清除value。防止内存泄漏
特别是配合线程池一起使用时,因为线程池的线程会服用。如果没有remove,很可能一直回收不了value