Java应用:基础与实践——Basis and practice

javac编译源码为class文件和步骤
1、分析和输入到符号表(Parse and Enter)
Parse过程所做的为词法和语法分析(com.sun.tools/javac.parser.Scanner)要完成的是将代码字符串转变为token序列(例如Token.EQ(name:=));语法分析(com.sun.tools.javac.parser.Parser)要完成的是根据语法由token序列生成抽象语法树
Enter(com.sun.tools.javac.comp.Enter)过程为将符号输入到符号表,通常包括确定类的超类型和接口,根据需要添加默认构造器,将类中出现的符号输入类自身的符号表中等
2、注解处理(Annotation Processing)
该步骤主要用于处理用户自定义的annotation,可能带来的好处是基于annotation来生成附加的代码或进行一些特殊的检查,从而节省一些共用代码的编写,例如采用Lombok时,可编写如下代码
public class User{
private @Getter String username;
}
编译时引入Lombok对User.java进行编译后,并通过javap查看class文件可看到自动生成了public String getUsername(0方法
3、语义分析和生成class文件
语义分析步骤基于抽象语法树进行一系列语义分析,包括将语法树中的名字、表达式等元素与变量、方法、类型等联系到一起;检查变量使用前是否已经声明;推导泛型方法的类型参数;检查所有语句都可到达等等
在完成语义分析后开始生成class文件(com.sun.tools/javac.jvm.Gen)生成的步骤为:首先将实例成员初始化器收集到构造器中,将静态成员初始化器收集为<clinit>();接着将抽象语法树生成字节码,采用的方法为后序遍历语法树,并进行最后的少量代码转换(例如String相加转变为StringBuild操作)最后符号生成class文件
ClassLoader抽象类提供的几个关键方法:
loadclass
负责加载指定名字的类,ClassLoader的实现方法为先从已经加载的类中寻找,若没有,则继续从parent ClassLoader寻找;如果仍然没找到,则从System ClassLoader中寻找。最后再调用findClass方法寻找;如果要改变类的加载顺序,则可覆盖此方法;如果加载顺序相同,则可通过覆盖findclass来做特殊的处理,例如解密、固定路径寻找等。当通过整个寻找类的过程仍未获取class对象时,则抛出ClassNotFoundException
findClass
此方法直接抛出ClassNotFoundException,因此要通过覆盖loadClass或此方法来以自定义的方式加载相应的类
findSystemClass
负责从System ClassLoader中寻找类,如未找到,则继续从Bookstrap ClassLoader中寻找,若仍未找到,则返回null
defineClass
负责将二进制字节码转换为Class对象,如果二进制的字节码的格式不符合JVM Class格式,则抛出ClassFormatError;如果生成的类名和二进制字节码中的不同,则抛出NoClassDefFoundError;如果加载的class是受保护的、采用不同签名的,或者类名是以java.开头的,则抛出SecurityException;如果加载的class在此ClassLoader中已加载,则抛出LinkageError
resolveClass
此方法负责完成Class对象的链接,如果链接过,则直接返回
JVM类加载过程会抛出几种异常
ClassNotFoundException
最常见的异常,产生的原因是当前ClassLoader中加载类时未找到类文件,位于System ClassLoader的类容易判断,只要加载的类不在Classpath中,user-Defined ClassLoader的类则会麻烦些,要具体查看ClassLoader加载类的过程,才能判断ClassLoader要从什么位置加载到此类
例如直接在代码中执行Class.forName("com.bluedavy.A"),当前类的classloader下没有该类所在jar或class文件,抛出ClassNotFoundException
NotClassDefFoundError
较之上一异常更难处理,造成异常的主要原因是加载的类中引用到的另外的类不存在,例如要加载A,但A中调用了B,B不存在或当前ClassLoader无法加载B,抛出异常 例如
public class A {
private B b = new B();
}
当采用Class.forName加载A时,虽能找到A.class,但B.class不存在,则会抛出NoClassDefFoundError。因此须先查看是加载哪个类报出的,再确认该类中引用的类是否存在于当前ClassLoader能加载到的位置
LinkageError
在自定义ClassLoader的情况下更容易出现,原因是此类已经在ClassLoader加载过了,重复加载会造成该异常,注意避免在并发情况下出现此问题
由于JVM这个版本的保护机制,使得在JVM中没办法直接更新一个已经load的class,只得创建一个新的ClassLoader来加载更新的class然后将新的请求转入该ClassLoader中获取类
ClassCastException
该异常有多种原因,在JDK5支持泛型后,合理使用泛型可相对减少此异常的触发,原因中比较难查的是两个A对象由不同的ClassLoader加载的情况,此时如果将其中某个A对象造型成另一个A对象,会抛出ClassCastException
线程在创建后,都会产生程序计数器(pc)和栈;PC存放了吓一条要执行的指令在方法内的偏移量;栈中存放了栈帧,每个方法每次调用都会产生栈帧。栈帧主要分为局部变量区和操作数栈两个部分,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果如图
指令解释执行
执行方法为冯.诺依曼体系中的FDX循环方式,即获取下一条指令,解码并分派,然后执行。在实现FDX循环时有switch-threading  token-threading   direct-threading   inline-threading等多种方式
switch代码如图

 

 

以上方式简单实现了FDX的循环方式,问题是每次执行完都得重新回到循环开始点,然后重新获取下一条指令,并继续switch,导致大部分时间都花费在跳转和获取一条指令上,而真正业务逻辑代码非常短
token-threading代码如下

 

冗余了fetch和dispatch,消耗的内存量会大一些,但出去了switch,性能会变好,为了让解释执行能更加高效,Sun JDK做了其他优化:栈顶缓存、部分栈帧共享
栈顶缓存
将本来位于操作数栈顶的值直接缓存在寄存器上,这对于大部分只需要一个值的操作而言,无需将数据放入该操作数栈,可直接在寄存器计算,然后放回操作数栈
部分栈帧共享
当调用方法时,后一方法可将前一方法的操作数栈作为当前方法的局部变量,从而节省数据copy带来的消耗
编译执行
Sun JDK在执行过程中对执行效率高的代码进行编译,对执行不频繁的代码则继续采用解释的方式,在编译上Sun JDK提供了两种模式:
client compiler (-client)和server compiler (-server)
client compiler 又称为C1,较为轻量级,只做少量性能开销比高的优化,占用内存较少,适用桌面交互式应用。在寄存器分配策略上,采用线性扫描寄存器分配算法,其他方面的优化有:方法内联、去虚拟化、冗余削除等
1、方法内联
对于Java类面向对象的语言,通常要调用多个方法来完成功能。执行时要经历多次参数传递、返回值传递及跳转等,C1采取的方法内联的方式,即把调用到的方法的指令直接植入当前方法中
2、去虚拟化
在装载class文件后,进行类层次的分析,若发现类中的方法只提供一个实现类,对于调用了此方法的代码也可以进行方法内联,从而提升执行的性能
3、 冗余削除
指在编译时,根据运行时状况进行代码折叠或削除 如图
server compiler又称C2,较为重量级,采用大量的传统编译优化技巧来进行优化,占用内存相对C1较多,适用于服务端应用。和C1不同的是寄存器分配策略及优化的范围,寄存器分配策略上C2采用的是传统的图着色寄存器分配算法,因为C2会收集程序的运行信息,因此优化的范围更多在于全局的优化,不仅仅是一个方法块的优化。收集的信息主要有:分支的跳转/不跳转的频率、某条指令上出现过的类型、是否出现过空值、是否出现过异常
逃逸分析是C2进行很多优化的基础,逃逸分析是指根据运行状况来判断方法中的变量是否会被外部读取,若不会,则认为此变量是逃逸的,基于逃逸分析C2会在编译时做标量替换、栈上分配、同步削除等优化
1、标量替换
用标量替换聚合量,好处是如果创建的对象并未用到其中的全部变量,则可以省略一定的内存
代码:Point point = new Point(1,2);
         System.out.println ("Point.x="+Point.x+";point.y="+Point.y);
当point对象在后面的执行过程中未使用时,经过编译后,代码变成类似下面的结构
int x = 1;
int y = 2;
System.out.println( "Point.x="+x+";point.y="+y );
2、栈上分配
上面的例子中,如果P没有逃逸,那C2会选择在栈上直接创建Point对象实例,而不是在JVM堆上。在栈上分配的好处是更加快速,并且回收时随着方法的结束,对象也被回收
3、同步削除
值如果发现同步的对象未逃逸,也没有必要同步了,在C2编译时会直接去掉同步
默认情况下,Sun JDK根据机器配置选择client或server模式,当机器配置CPU 超过2核且内存超过2GB即默认为server模式,在32位Window机器上始终是client模式,也可以在启动时通过增加-client或-server强制指定。在JDK7中可能会引入多层编译的支持,多层编译是指在-server模式下采用如下方式
1、解释器不再收集运行状况信息,只用于启动并触发C1编译
2、C1编译后生成带收集运行信息的代码
3、C2编译,基于C1编译后代码收集的运行信息来进行激进优化,当激进优化的假设不成立时,在退回使用C1编译的代码
Sun JDK未选择在启动时即编译称机器码,有以下原因
1、C2是根据运行状况来进行动态编译,如分支判断、逃逸分析等,这些措施会对提升程序执行的性能起到很大的帮助,静态编译的情况下无法实现。C2收集运行数据越长时间,编译出的代码越优
2、解释执行比编译执行更节省内存
3、启动时解释执行的启动速度比编译再启动更快
程序在未编译期间解释执行方式较慢,需要取一个权衡值,在Sun JDK中主要依据方法上的两个计数器是否超越阈值,其中一个是调用计数器,即方法被调用次数;另一个是回边计数器,即方法中循环执行部分代码的执行次数
CompileThreshold
指当方法被调用多少次后,就编译位机器码,在client模式下默认为1500次,在server模式下默认为1000次,可通过在启动时添加
-XX:CompileThreshold = 10000来设置
OnStackReplacePercentage
用于计算是否触发OSR编译的阈值,默认情况下client模式为933,server模式下为140,该值可通过在启动时添加-XX:OnStackReplacePercentage = 140设置,在client模式时,计算规则为Compile Threshold *(OnStackReplace/100),在Server模式时,计算规则为(CompileThreshold *(OnStackReplacePercentage-InterpreterProfilePercentage))/100
反射执行
基于反射可动态调用某对象实例中对应的方法,访问查看对象的属性等,无须在编写代码时就确定要创建的对象,使Java可以灵活实现对象的调用。如MVC框架通常要调用实现类中的execute方法,但框架在编写时无法知道实现类。在Java中可以通过反射机制直接去调用应用实现类中的execute方法,代码如下
实现对动态的调用,最直接方法是动态生成字节码,并加载到JVM中执行
Class cationClass = Class.forName;  调用本地方法,使用调用者所在的ClassLoader来加载创建出的Class对象
Method method = actionClass.getMethod("execute",null);校验Class是否为public类型,以确定类的执行权限,若不是public类型的,直接抛出SecurityException
扫描方法集合列表中是否有相同方法名及参数类型的方法,若有,则复制生成一个新的Method对象返回;如果没有,则继续扫描父类、父接口中是否有该方法;如果仍然没有找到的话,抛出NoSuchMethodException,代码:
Object action = actionClass.newInstance();
method.invoke(action,null);
执行过程和上一步基本类似,只是在生成字节码时方法改为了invoke,其调用目标改为了传入对象的方法
综上,执行一段反射执行的代码后,在debug查看Method对象中的MethodAccessor对象引用(参数为-Dsun.reflect.noInflation = true,否则要默认执行15次反射调用才能动态生成字节码)
JVM内存管理
除直接调用System.gc外,触发Full GC执行的情况有以下4种:
 1、旧生代空间不足
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误
java.lang.OutOfMemoryError:java heap space
为避免以上两种状况引起的Full GC,调优时尽量做到让对象在Minor GC阶段被回收,让对象在新生代多存活一段时间以及不要创建过大的对象和数组
2、Parmanet Generation空间满
存放的为一些class的信息等,当系统中要加载的类、反射的类和调用方法过多时,PG可能会被沾满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么会抛出如下错误信息
java.lang.OutOfMemoryError:PermGen space
为避免PG沾满造成Full GC现象,可采用的方法为增大PG空间或转为使用CMS GC
3、CMS GC时出现promotion failed和concurrent mode failure
promotion failed是在进行Minor GC时,survivor space放不下,对象只能放入旧生代,而此时旧生代也放不下造成的; concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的
应对措施:增大survivor space、旧生代空间或调低触发并发GC的比率,但在JDK5、6版本中有可能会由于JDK的bug导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime = 5来避免
4、统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
较为复杂的触发情况,HotSpot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,直接触发Full GC
例如程序第一次触发Minor GC后有6MB的对象晋升到旧生代,当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC
当新生代采用PSGC时,方式有所不同,PSGC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC之后,PSGC会检查此时旧生代的剩余空间是否大于6MB,若小于,则触发对旧生代的回收
Real-Time JDK
新的内存管理机制
提供了两种内存区域:Immortal内存区域和Scoped内存区域
Immortal内存区域用于保留永久的对象,这些对象仅在应用结束运行时才会释放内存,对于支持需要缓存的系统非常重要
Scoped内存区域用于保留临时的对象,位于scope中的对象在scope退出时,这些对象所占用的内存会被直接回收,类似于栈上分配
Immortal和Scoped内存区域均不受GC管理,因此基于这两个内存区域来编写的应用不必担心GC会造成暂停的现象
允许Java应用直接访问物理内存
在保证安全的情况下,Real-Time JDK允许Java应用直接访问物理内存,并不像之前需通过native code才能访问。意味着可以直接将对象放入物理内存,并非JVMheap中
JVM线程资源同步及交互机制
线程资源同步机制
int i = 0;
public int getNextId(){
return i++;
}
1、JVM首先在main memory(JVM堆)给i分配存储场所,并存储其值0
2、线程启动后,会自动分配一片working memory区(操作数栈),当线程执行到return i++时,JVM不是一个步骤可以完成。i++
动作在JVM中分为装载i、读取i、进行i++操作、存储i、写入i五个步骤才得以完成
装载i
线程发起一个装载i的请求给JVM线程执行引擎,接受请求后会向main memory发起一个read i的指令,当read i执行完毕后,一段时间线程会将i的值从main memory区复制到working memory区中
读取i  此步负责从main memory中读取i
进行i+1操作  由线程完成
存储i   将i+1的值赋给i,然后存储到working memory中
写入i  一段时间后i的值会写回到main memory,只能多个线程在这个时间段内同时执行了操作,就会出现获取i值相同的现象
JVM把working memory的操作数分为了:use、assign、load、store、lock、unlock。对于wrking memory的操作的指令由线程发出,对于main memory的操作分为read、write、lock、unlock
use 由线程发起,需要完成将变量的值从working memory中复制到线程执行引擎中
assign  由线程发起,需要完成将变量值复制到线程的working memory中,如a=i,此时线程会发起一个saaign动作
load  由线程发起,需要完成将main memory中read到的值复制到 working memory中,并等待main memory通过write动作写入此值
read  由main memory发起,负责从 main memory中读取变量的值
write  由 main memory发起,负责将 working memory的值写入到main memory中
lock  由线程发起,同步操作main memory,给对象加上锁
unlock  由线程发起,同步操作main memory,去除对象的锁
JVM中保证以下操作是顺序的
1、同一个线程上的操作一定是顺序执行的
2、对于main memory上的同一个变量的操作一定是顺序执行的,即不可能两个请求同时读取变量值
3、对于加了锁的main memory上的对象操作,一定是顺序执行的,即两个以上加了lock的操作,同时肯有只有一个是在执行的
性能调优
调优前衡量系统现状,包括目前系统的的请求次数、响应时间、资源消耗等信号,如A系统目前95%的请求响应时间为1s
调优目标根据用户所能接受的响应速度或系统锁拥有的机器以及所支持的用户量指定出来的,通常会设出调优目标:95%的请求要在500ms内返回,再找出造成目前系统性能不足的最大瓶颈点,找出后,结合一些工具找出造成瓶颈点的代码。再分析需求场景,然后结合一些优化的技巧制定优化的策略。优化策略可简可繁,选择其中收益比(优化后的预期效果/优化需要付出的代价)最高的优化方案,进行优化
寻找性能瓶颈预期
CPU消耗分析
在Linux中,CPU主要用于中断、内核、用户进程的任务处理,优先级为中断>内核>用户进程
上下文切换
为每个线程分配一定的执行时间,当到达执行时间,线程中有IO阻塞或高优先级线程要执行时,Linux将切换执行的线程,在切换时要存储目前线程的执行状态,并恢复要执行的线程的状态,此过程称为上下文切换
运行列队
每个CPU核都维护一个可运行的线程队列,例如一个4核的CPU,Java应用中启动了8个线程,且这8个线程都处于可运行状况,那么在分配平均的情况下每个CPU中的运行队列里就会有两个线程
利用率
CPU利用率为uCPU在用户进程、内核、中断处理、IO等待以及空闲5个部分使用百分比,这5个值是用来分析CPU消耗情况的关键指标,建议用户进程的CPU消耗/内核CPU消耗的比率在65%~70%/30%~35%左右
在Linux中,可通过top或pidstat方式查看进程中线程CPU的消耗状况
对于Java应用而言,CPU消耗严重主要体现在us、uy两个值上;hi值变高主要为硬件中断造成的,例如网卡接收数据频繁的状况
us
首先通过Linux提供的命令找到消耗CPU严重的线程及其ID,将此线程ID转化为十六进制的值,之后通过kill-3[javapid]或jstack的方式dump出应用的Java线程信息,通过之前转化出的十六进制值找到对应的nid值的进程
Java应用造成us高的原因主要是线程一直处于可运行状态,通常这些线程在执行无阻塞、循环、正则或纯粹的计算等动作造成;另一个可能也会造成GC高的原因是频繁的GC。若每次请求都需分配较多内存,当访问量高的时候就将导致不断地进行GC,系统响应速度下降,进而造成堆积的请求更多,消耗的内存更严重
sy
当sy值高时,表示Linux花费了更多的时间在进行线程切换,Java应用造成这种现象的主要原因是启动的线程比较多,且这些线程多数都处于不断的阻塞(如锁等待,IO等待状态)和执行状态的变化过程中,导致操作系统要不断地切换执行的线程,产生大量的上下文切换
可采用的方法为通过kill -3[javapid]或jstack -1[javapid]的方式dump出Java应用程序的线程信息,查看线程的状态信息以及锁信息
文件IO消耗分析
Linux在操作文件时,将数据放入文件缓存区,直到内存不够或系统要释放内存给用户进程使用,这是Linux提升文件IO速度的一种做法,要跟踪线程文件IO的消耗,主要方法是通过pidstat来查找,输入如pidstat-d-t-p[pid]/100类似的命令即可查看线程的IO消耗状况
iostat  直接输入iostat命令,可查看各个设备的IO历史状况
当文件IO消耗过高时,对于Java应用最重要的是找到造成文件IO消耗高的代码,寻找的最佳方法为通过pidstat直接找到文件IO操作多的线程。之后结合jstack找到对应的Java代码,若没有pidstat,可直接根据jstack得到的线程信息来分析其中文件IO操作较多的线程
Java应用造成文件IO消耗严重主要是多个线程需要进行大量内容写入(如频繁的日志写入)的动作;或磁盘设备本身的处理速度慢;或文件系统慢;或操作的文件本身很大造成的
网络IO消耗分析
在Linux中可采用sar来分析网络IO的消耗状况,输入sar-n Full1L,执行后以1s为频率,总共输出两次网络IO的消耗状况,如图
内存消耗分析
swap的消耗以及物理内存的消耗,这两方面的消耗都可基于OS提供的命令来查看。Linux中可通过vmstat  -sar  top  pidstat等方式来查看swap和物理内存消耗情况
对物理内存的消耗
基于Direct ByteBuffer可以容易实现对物理内存的直接操作,如图
对JVM内存的消耗
在Java程序出现内存消耗过多、GC频繁或OutOfMemory的情况后,首先分析其所消耗的是JVM外的物理内存还是JVM heap区。若为JVM以外的物理内存,则要分析程序中线程的数量以及Direct ByteBuffer的使用情况;若为JVM heap区,则要结合JDK提供的工具或外部的工具来分析程序中具体对象堆内存占用情况
程序执行慢原因分析
1、锁竞争激烈
例如数据库连接池,通常数据库连接池提供的连接数都是有限的。假设提供的是10个,意味着同时能进行数据库操作的只有10个线程,若此时有50个线程要进行数据库操作,会造成另外40个线程处于等待状态,CPU的消耗不会高,但程序的执行仍会较慢
2、未充分使用硬件资源
例如机器上有双核CPU,但程序中都是单线程串行的操作,并没有充分发挥硬件资源的作用,此时旧可以进行一定的优化来充分使用硬件资源,提升程序的执行速度
3、数据量增长
通常也是造成程序执行慢的典型原因,例如当数据库中单表的数据从100万个上涨到1亿个后,数据库的读写速度将大幅度下降,相应的操作此表的程序的执行速度也就下降了
调优
JVM调优
主要是内存管理方面的调优,包括各个代的大小、GC策略等。由于GC动作会挂起应用线程 ,严重影响性能,调优对于应用而言至关重要,根据应用的情况选择不同的内存管理策略有时能够大幅度提升应用的性能,尤其是内存消耗较多的应用
代大小的调优
在不采用G1的情况下,通常minor GC会远快于Full GC,各个代的大小设置直接决定了minor GC和Full GC触发的时机,在代大小的调优上,最关键的参数为-Xmx  -Xms  -Xmm  -XX:SurvivorRatio  -XX:MaxTenuringThreshold
程序调优
CPU消耗严重的解决方案
CPU us高的解决方案
原因:执行线程无任何挂起动作,且一直执行,导致CPU没有机会去调度执行其他的线程,造成线程饿死线程
优化方法:对这种线程的动作增加Thread.sleep,以释放CPU的执行权,降低CPU的消耗
这种方式是以损失单次执行性能为代价的,由于降低了CPU的消耗,反而提高了总体的平均性能。对于GC频繁造成的CPU us高的现象,则要通过JVM调优或程序调优,降低GC的执行次数
CPU sy高的解决方法
主要原因是线程的运行状态要经常切换,优化方法是减少线程数。所以不是线程越多吞吐量旧越高,线程数需要设置为合理的值,根据应用情况来具体决定,同时使用线程池避免要不断地创建线程
除启动的线程过多以外,还有一个原因是线程之间锁竞争激烈,造成线程状态经常要切换,因此尽可能降低线程间的锁竞争也是常见的优化方案
文件IO消耗严重的解决方案
主要原因是多个线程在写大量的数据到同一文件,导致文件变得很大,从而写入速度越来越慢,并造成各线程激烈争抢文件锁。调优方法如下
1、异步写文件
将写文件的同步动作改为异步动作,避免应用由于写文件慢而性能下降太多,例如写日志,可以使用log4j提供的AsyncAppender
2、批量读写
频繁的读写操作对IO消耗很严重,批量操作将大幅度提升IO操作的性能
3、限流
将文件IO消耗控制到一个能接受的范围。例如通常在记录日志时会采用如下方式:log.error(errorInfo,throwable);
以上方式不做任何处理,在大量出现异常时,会出现所有的线程都在执行log.error(...),此时可采取的一个简单策略为统计一段时间内log.error的执行效率,实例代码如下
4、限制文件大小
对于每个输出的文件,都应做大小的限制,在超出最大值后可生成一个新的文件,类似log4j中RollingFileAppender的maxFileSize属性的作用
网络IO消耗严重的解决方案
主要原因是同时需要发生或接受的包太多,调优方法为进行限流,通常是限制发送packet的频率,从而在网络IO消耗可接受的情况下发送packet
对于内存消耗严重的情况
以下是一些JVMHeap内存消耗严重时常用的程序调优方法
1、释放不必要的引用
最典型的一种现象是代码中持有了不需要的对象引用,造成这些对象无法被GC,从而占据了JVM内存,如由于线程复用,Threadlocal中存放的对象未做主动释放的话则不会被GC
2、使用对象缓存池
在内存消耗严重的情况下,采用对象缓存池可大幅度提升性能,避免创建对象所消费的时间及频繁GC造成的消耗
3、采用合理的村换失效算法
如果放入太多的对象在缓存池中,反而会造成内存的严重消耗。同时由于缓存池一直对这些对象持有引用,从而造成Full GC增多,对于这种状态要合理控制缓存池大小
控制缓存池大小的问题在于当到达缓存池的最大容量后,如果要加入新的对象该如何处理,采用FLFO  LRU  LFU等这些算法可控制缓存池中的对象数目,避免缓存池中的对象数量无限上涨
4、合理使用SoftReference和weakReferen或 weakReference的方式来进行缓存,S oftReference的对象会在内存不够用的时候回收,WeakReference的对象则会在Full GC的时候回收,采用这两种方式也能一定程度上减少JVM Heap区内存的消耗
对于资源消耗不多,但程序执行慢的情况
锁竞争激烈
线程多了后,锁竞争的状况会比较明显,这时线程很容易处于等待锁的状况,从而导致性能下降以及CPU sy上升。为保证资源的一致性,多线程应用中锁的使用是不可避免的,只能尽量降低线程间的锁竞争,常见方法如下:
1、使用并发包中的类
并发包中的类多数都采用lock-free、nonblocking算法,减少了多线程情况下资源的锁竞争
2、使用Treiber算法
主要用于实现Stack代码如下
3、使用Michael-Scott非阻塞队列算法
基于CAS以及AtomicReference来实现队列的非阻塞,java.util.concurrent中的ConcurrentLinkedQueue是典型的基于Mickael_Scott实现的非阻塞队列
ConcurrentLinkedQueue在执行offer动作时,通过CAS比较拿到的tail元素是否为当前处于末尾的元素,若不是则继续循环,若是则将tail元素更新为新的元素
在执行poll动作时,通过CAS比较拿到的head元素是否为当前处于首位的元素,若不是则继续循环,若是则将head后的元素赋值给head,同时获取之前head元素中的值并返回
4、尽可能减少锁
尽可能让锁仅在需要的地方出现,通常没必要对整个方法加锁,只对需要控制的资源做加锁操作。尽可能让锁最小化,只对互斥及原子操作的地方加锁,加锁时尽可能以被保护资源的最小粒度为单位,例如一个操作中需要保护的资源只有HashMap,那么在加锁时则只sychronized(map),没有必要sychronized(this),并且可以只在对HashMap操作时才加锁
5、拆分锁
把独占锁拆分为多把锁,常见的有读、写锁拆分及类似ConcurrentHashMap中默认拆分为16把锁的方法。拆分锁很大程度上能提高读写的速度,但需要注意的是在采用拆分锁后,全局性质的操作会变得比较复杂,例如ConcurrentHashMap中size操作
6、去除读写操作的互斥锁
在修改时加锁,并复制对象进行修改,修改完毕后切换对象的引用,而读取时则不加锁,这种方式称为CopyWrite,CopyOnWriteArrayList是CopyOnWrite方法的典型实现, CopyOnWrite的好处是可以提升读的性能,适合读多写少的应用,但由于写操作时每次都要复制一份对象,会造成更多的内存消耗
未充分使用CPU
主要原因是在能并行处理的场景中未使用足够的线程,在CPU资源消耗可接受、且不会因为线程增加带来激烈竞争的场景下,应适当对处理过程进行分析,增加线程数从而能并行处理以提升系统的运行性能。但在重构为并行时,要注意控制内存消耗,而且通过重构为并行提升的性能也是有限的
未充分使用内存
未充分使用内存场景非常多,如数据的缓存、耗时资源的缓存(如数据库连接的常见、网络连接的创建等)、页面片段的缓存等,也要避免内存资源的过度使用,在内存资源消耗可接受、GC频率及系统结构(如集群环境可能会带来缓存的同步等)可接受的情况下,应充分使用内存来缓存数据,提升系统的性 能
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值