概述

java性能优化的步骤通常是先进行操作系统性能监控、JVM性能监控、性能分析后找到影响性能的原因,然后可能对诸如:HotSpot VM、JVM垃圾收集器、操作系统或应用程序的调优。本文只讨论常见的JVM内存分配方式,其余的以后有机会再总结。

术语

  • 堆(Heap)和非堆(Non-heap)内存

Java虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在Java虚拟机启动时创建的。在JVM中堆之外的内存称为非堆内存(Non-heapmemory)。

可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。

  • JVM内存限制(最大值)

首先JVM内存首先受限于实际的最大物理内存,假设物理内存无限 大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是 2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了

内存调配方式

出现调整JVM内存的场景最常见的莫过于OutOfMemoryError,出现这个错误通常的解决方法就是加大分配JVM堆内存

1.对于本地的程序

可以使用命令对运行时的jvm内存进行设置

java -Xms64m -Xmx256m Test

-Xms是设置内存初始化的大小

-Xmx是设置最大能够使用内存的大小(最好不要超过物理内存大小)

2.对于使用Eclipse运行的程序

可以在Eclipse安装目录下的eclipse.ini分配内存,eclipse.ini文件的内容要按照格式编写。

-vmargs
-Xms128M
-Xmx512M
-XX:PermSize=64M
-XX:MaxPermSize=128M

1.参数中-vmargs的意思是设置JVM参数,所以后面的其实都是JVM的参数

2.JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小

3.-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。

4.-XX:+UseParallelGC 在双核的CPU下让GC可以更快的执行

3.在应用服务器下的应用

根据不同的服务器在不同的配置文件下配置内存,以下主要讲Tomcat和Weblogic中的配置

3.1 Tomcat

在TOMCAT的目录下,也就是在TOMCAT安装目录/bin/catalina.bat文件最前面加入:

set JAVA_OPTS=-Xms800m -Xmx800m

根据以上配置,当你重新启动TOMCAT时,系统内存会增加近800M使用

3.2 Weblogic

可以在startweblogic.cmd中或者在/bin/setDomainEnv.cmd文件对每个domain虚拟内存的大小进行设置,而所有domain的默认的内存大小是在commom\bin\commEnv.cmd里面配置的,以下是commEnv.cmd里主要的JVM内存配置的代码片段:

:bea
if "%PRODUCTION_MODE%" == "true" goto bea_prod_mode

set JAVA_VM=-jrockit //使用的是Weblogic提供的jrockit作为JDK

set MEM_ARGS=-Xms768m -Xmx1024m -XX:MaxPermSize=256m

set JAVA_OPTIONS=%JAVA_OPTIONS% -Xverify:none

goto continue

:bea_prod_mode

set JAVA_VM=-jrockit

set MEM_ARGS=-Xms768m -Xmx1024m -XX:MaxPermSize=256m//调整后和把上面的MEM_ARGS也配置成一样

goto continue

以下是startweblogic.cmd文件的配置:

set DOMAIN_HOME=E:\Weblogic\Middleware\user_projects\domains\base_domain
set USER_MEM_ARGS=-XX:PermSize=256m  -XX:MaxPermSize=256m -Xms800m -Xmx800m
call "%DOMAIN_HOME%\bin\startweblogic.cmd" %*

以下是setDomainEnv.cmd文件的配置: 主要就是修改-Xms、-Xmx、-XX:PermSize、-XX:MaxPermSize的参数(视具体硬件、JVM负载情况进行修改)。如果需要设置-Xss等其它相关参数,也可添加到最后的MEM_ARGS中。

这里需要注意的是有个32位、64位的区别。实际上具体采用哪种配置,可查看文件, 如:

Weblogic11\wlserver_10.3\common\bin\commEnv.cmd 找到set JAVA_USE_64BIT=false配置,即表示非64位环境。

setDomainEnv.cmd文件的配置

实践

1.选择高版本的jdk,因为高版本的自动调优方式可以极大改善性能。

2.Weblogic有两种JDK供选择,一种是Sun的JDK,另外一种是Bea的jrockit。作为Weblogic集群推荐采用jrockit作为JDK环境,来达到更高的性能。

3.部署应用时将JVM换成Server的模式会提升很大性能,但在启动时会比Client模式慢。32位的虚拟机在目录JAVA_HOME/jre/lib/i386/jvm.cfg,64位的在JAVA_HOME/jre/lib/amd64/jvm.cfg,目前64位只支持server模式

-server KNOWN
-client KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR

一般只要变更 -server KNOWN 与 -client KNOWN 两个配置位置先后顺序即可,前提是JAVA_HOME/jre/bin 目录下同时存在 server 与client两个文件夹,分别对应着各自的jvm.如果缺少其中一个,切换模式时就会报错。

参考资料

1.Weblogic11g-常用运维操作

2.JVM内存原理,weblogic内存的调优

3.Eclipse中进行JVM内存设置

概述

线程无处不在,即使在java中没有显示创建线程,但在框架中仍可能会创建线程,因此在这些线程调用的代码同样也要保证线程安全。

术语

对象的状态:存储的状态变量(例如实例或静态域)中的数据。

共享:变量可以由多个线程同时访问

可变:变量的值在其声明周期内可以发生变化。

线程安全性:当多个线程访问某个类时,不管运行环境采用何种调度方法或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。所以为了线程的安全性,“先检查后执行”以及“读取-修改-写入”等操作都必须是原子的。可以使用jdk的Atomic相关类。

可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

volatile变量:当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值

java同步容器、并发类的使用场景

同步容器

使用synchronized同步代码块或同步方法,如:jdk的Collections.synchronizedList(List list)相关方法底层就是同步方法的方式,Vector、Hashtable类的方法基本上都采用同步方法的方式。如果需要使用比同步集合拥有更好的并发性请使用并发容器。

并发容器

  • 并发Collection

ConcurrentHashMap:使用分段锁机制,实现是把Map中正在被写入的部分进行锁定,不影响其他部分的读写,所有执行读取操作的线程和执行写入操作的线程可以并发访问Map。只有当应用程序需要加锁Map以进行独占访问或者需要依赖同步map带来的一些其它作用时,才应该放弃使用ConcurrentHashMap

CopyOnWriteArrayList:所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制的数组来实现的。仅当迭代操作远远多于修改操作时,才应该使用“写入时复制”容器CopyOnWriteArrayList。

ConcurrentNavigableMap:支持并发访问,它还能让该类的 headMap(),subMap(),tailMap() 之类的方法返回的 map具有并发访问能力.

  • 并发Queue

重点:主要用于生产者-消费者场景

LinkedBlockingQueue:阻塞的队列,入队和出队都用了加锁,当队空的时候线程会暂时阻塞。

ArrayBlockingQueue:ArrayBlockingQueue是初始容量固定的阻塞队列,我们可以用来作为数据库模块成功竞拍的队列,比如有10个商品,那么我们就设定一个10大小的数组队列。

PriorityBlockingQueue:按优先级排序队列,可以自定义排序规则

SynchronousQueue:是一个长度只有1的特殊的队列。通过线程阻塞防止第二个元素的插入或获取空队列的元素。当线程池是无界的或者可以拒绝任务时,才有实际的价值。

ConcurrentLinkedQueue:使用的是CAS原语无锁队列实现,是一个异步队列,入队的速度很快,出队进行了加锁,性能稍慢。

DelayQueue:对元素进行持有直到一个特定的延迟到期,放入队列中的元素必须实现java.util.concurrent.Delayed接口

  • 并发Deque

重点:线程既是一个队列的生产者又是这个队列的消费者的时候可以使用到

LinkedBlockingDeque:双端队列是一个你可以从任意一端插入或者抽取元素的队列。每个消费者都有各自的双端队列并且当一个消费者完成了自己双端队列中的全部工作,那么它可以从其它消费者双端队列的末尾秘密获取工作,极大减少了竞争。非同步的有ArrayDeque、LinkedList

同步器

CountDownLatch:在计数器为0之前,一个或多个线程一直等待,就像倒计时。最重要的方法是countDown()和await(),前者主要是倒数一次,后者是等待倒数到0,如果没有到达0,就只有阻塞等待了

FutureTask:只有当所有线程的run或者call方法都执行完成,则通过get()方法获取计算结果,否则默认阻塞该方法,但也提供超时取消任务的方法。

Semaphore:信号量为空即没有获取到许可的线程将阻塞,用于控制访问资源的线程个数。

CyclicBarrier:当指定数量的线程都调用了await()之后,就表示这些线程都可以继续执行,否则就会等待

Exchanger:用于两个线程交换数据,线程执行exchange(V x) 时需要等待另一个线程也执行exchange(V x) 才交换数据

并发原子类

重点:修改方法的底层实现是通过JNI调用操作系统的原生程序

AtomicBoolean:可以用原子方式更新的 boolean 值。

AtomicInteger:可以用原子方式更新的 int 值。

AtomicLong:可以用原子方式更新的 long 值。

AtomicIntegerArray:可以用原子方式更新其元素的 int 数组。

AtomicLongArray:可以用原子方式更新其元素的 long 数组。

AtomicReference:可以被原子性读和写的对象引用变量,如:String

Executor框架

重点:异步执行机制,替换传统的new Thread(runnable).start()方式,使我们能够在后台执行任务;实现了线程池

Executors:返回Executor、ExecutorService、ScheduledExecutorService类的实现类对象。

ExecutorCompletionService: 同时执行几组任务,通过take()获取存放完成任务的队列上的任务。

ThreadPoolExecutor:提供可调的、灵活的线程池,但需要由Executors类的方法来提供实例。

ScheduledThreadPoolExecutor:基本同ThreadPoolExecutor,它能够将任务延后执行,或者间隔固定时间多次执行

ForkJoinPool:jdk7提供,是一个特殊的线程池,它的设计是为了更好的配合 分叉-和-合并 任务分割的工作。

RecursiveTask:jdk7提供,是一种会返回结果的任务。它可以将自己的工作分割为若干更小任务,并将这些子任务的执行结果合并到一个集体结果。可以有几个水平的分割和合并。

同步锁

重点:提供比synchronized更灵活的锁方式,允许以三种形式(可中断、不可中断和定时)尝试获取锁,并允许以任何顺序获取和释放多个锁

ReentrantLock:标准的互斥锁,每次最多只有一个线程能持有ReentrantLock。

ReentrantReadWriteLock:允许多个线程在同一时间对某特定资源进行读取,但同一时间内只能有一个线程对其进行写入。

实践

1.在实际情况中,应该尽可能使用现有的线程安全对象,如jdk提供的原子类等来管理类的状态,这体现了重用,而重用是能降低开发工作量、开发风险以及维护成本的。

2.对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁。

3.如果使用的是同步代码块方式,应该将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而即维护线程安全性又提高代码的并发性

4.当执行时间较长的计算或者可能无法快速完成的操作时,如:网络IO等,一定不要持有锁

5.除非需要更高的可见性,否则应将所有的域声明为private;除非需要某个域是可变的,否则应将其声明为final

6.final修饰集合变量,保证集合安全,不能保证集合内的数据安全;final修饰基本类型变量,能保证数据安全;final修饰的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。

7.不建议过度依赖volatile变量,只有当访问该变量不需要加锁时采用。

8.同步容器类在迭代器修改的时候需要使用同步代码来封装

9.使用完 ExecutorService 之后你应该将其关闭,以使其中的线程不再运行。shutdown() 方法在终止前允许执行以前提交的任务,而 shutdownNow() 方法阻止等待任务启动并试图停止当前正在执行的任务。两个方法底层都是通过interrupte中断实现

10.在java5.0或更高的JDK中,将很少使用Timer,而使用ScheduledThreadPoolExecutor,构建调度服务可以使用DelayQueue,它实现了BlockingQueue

11.并发程序的性能更多是吞吐量和可伸缩性上,因为java程序中串行操作的主要来源是独占方式的资源锁,因此通常可以通过以下方式来提升可伸缩性:减少锁的持有时间。降低锁的粒度,以及采用非独占的锁或非阻塞锁来代替独占锁。

参考资料

1.Java里的CompareAndSet(CAS)

2.Java 并发工具包 java.util.concurrent 用户指南

3.好书一本:《JAVA并发编程实战》

来源:念碎碎计设互交的博客@Danis2010

因为在做细节交互设计的时候,有时候明明知道却老偏偏漏掉一些特殊状况的方案设计,其中又以边缘状况的设计遗漏为多数,所以干脆给自己总结了几个“凡是”,以提醒自己记得别忘记。

在这里一一列举,希望对大家也有些许帮助。

1.凡是输入,必有限制

当我们在设计中使用到输入控件特别是文本输入框的时候,这个问题就来了。

第一个需要面对的是字符数的限制

字符数的限制有的来自于产品本身的业务限制,比如微博的140个字,注册时账号和密码的长度等。

有些产品看上去并不限制字符数,或者说并不给用户交代“限制”的概念。但其实也需要限制,这种限制来自于技术方面的考虑。比如写邮件的时候填入的收件人个数,搜索框接受的关键字字数,留言板的内容等。

虽然业务不要求,但录入的内容,可能会造成提交的困难,数据库的臃肿,被攻击的漏洞等,所以我们也不得不考虑“隐形的限制”,虽然边界看不到,一旦用户越界,还是需要有相应的解决方案。

输入并不全是文本,所以我们还需要面对其他格式的输入限制问题。

比如上传文件的个数,比如上传文件的大小,比如上传文件的格式,上传照片的的时候如果用户选了非图片格式的文件怎么办?一开始就让ta只能看图片文件还是选中后提醒?

这些限制也都从业务和技术两方面考虑,比如相册一次只能传20张照片,升级为会员就能传50张,这是业务限制。用浏览器相册一次只能传20张照片,安装插件或客户端,能传50张,这是技术限制,考虑浏览器负荷。

面对以上输入限制问题的时候,具体的解决方式当然是多种多样的,这里列举一些规则:

a.如果是字符输入,中英文字符数的计算规则。规则不一样,设计也不一样。如:新浪微博是限制140个中文280个英文(当然还有各种其他字符空格的情况

新浪微博分享文本框输入字符数量的限制

b.如何暗示限制?

暗示限制

c.违反规则的时候作何反馈?这个问题文章下面会继续提及。

d.输入控件根据容纳内容的变化?是直接用滚动条解决呢?还是伸长输入框呢?还是两者结合呢?等等。

e.别忘了以上内容。画一个输入框时想想这是不是一个无底洞?

2.凡是输入,必考虑输出

界面上的动态内容,可能来自于后台编辑的录入,也可能来自于前台用户的发表,总之,界面上的动态内容,都是先有人输入的。输入的人(特别是用户)可能不会去考虑以后展现的效果,但作为设计师的我们,就要为用户考虑了。

要考虑超量内容的显示。

这涉及到两个方面的考虑,先考虑输入时候的限制,我们要知道究竟某个显示空间究竟最多可能出现多少内容(当然,有时候是输入输出一起考虑,最终双方平衡)。再考虑这些内容在显示空间里如果显示不完,那怎么办?解决方案可以是索引页显示局部,到详情页再显示完全(总得要有个地方显示完全的信息)

索引页显示局部

也可以是显示部分内容,留待更多展开显示。或者滚动条来拯救等等等等。

既然有超量内容,也可能有空白内容。

当搜索结果为0的时候,怎么显示?当用户还没收到任何消息的时候,怎么呈现?当某个分类下的商品为空的时候,怎么办?这些问题也不要忘记考虑进我们的设计方案里面。

另外,还有分辨率的事。

无论是移动终端(特别是android)还是电脑,我们设计的界面会出现在各种尺寸的显示器上。这就决定了我们不得不考虑屏幕的适配问题。

比如一个pc上的弹出层,确定按钮的位置要保证在第一屏的话,那得考虑主流(里最小)屏幕高度。

比如一个默认的全屏应用壁纸,多大合适?用平铺合适还是居中合适?还是自适应?

比如480×800的屏幕能显示6行列表项,在480×854的屏幕就能多显示一行。

3.凡是成功,必考虑失败

别想着用户永远都能正确操作,人非圣贤。这是个大话题,涉及到怎么防止错误,错误后怎么提醒,怎么给予帮助,也许以后再专门做文章吧。

在这里想说的是,有时候做设计做昏头了,再加上进度紧张,往往只是把正常的流程做出来了,却忘记了,既然用户操作了,就可能失败。比如注册时表单项某些没填,登录时密码账号不符,拖动文件失败,粘贴图片失败,等等等等。

加载失败的时候

边缘状况并不是我们设计的主要内容,但却又是不得不设计,它体现着产品的贴心,考验着我们的细心,魔鬼都在细节里,别忘了它们 。