JVM与性能优化
2020-05-07 18:51:01 1 举报
AI智能生成
JVM与性能优化思维导图
作者其他创作
大纲/内容
JVM与性能优化
JVM前世今生
Sun/Oracle系列的虚拟机
Sun Classic/Exact VM - 第一款商用虚拟机
HotSpot VM - 目前使用范围最广的Java虚拟机
Mobile系列 - 面向移动和嵌入式市场
其他公司的虚拟机(BEA/IBM/MICROSOFT/GOOGLE)
BEA JRockit/IBM J9 VM/BEA Liquid VM
Google Android/Dalvik VM
Apache Harmony/Microsoft JVM
其他JVM
未来Java技术的特点
模块化
混合语言
多核并行
丰富语法
x64bit
更强的GC
Java内存区域
运行时数据区域
定义:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
程序计数器Program Counter Register
定义:当前线程执行的字节码的行号指示器
特点:各线程之间独立存储,互不影响
Java栈Java Stack
定义:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程
特点:线程私有,其生命周期和线程同时存在和消亡
每一个方法打包成为一个栈帧
局部变量表
操作数栈
帧数据区
本地方法栈Native Method Stack
定义:本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单的动态链接并直接调用native方法
堆Heap
定义:Java中几乎所有的对象都在堆中分配内存空间
TLAB(ThreadLocalAllocationBuffer)
即线程本地分配缓存区,JVM事先在堆中为每个线程分配一块私有内存,避免各线程为争夺堆空间而引发竞争,分配出的对象对所有线程都是可见的
方法区Method Area
用于存储已经被虚拟机加载的类信息、常量、静态变量等数据
运行时常量池Runtime Constant Pool
定义:运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用
JDK各个版本中内存空间的变化
JDK1.6
运行时常量池位于方法区中
JDK1.7
运行时常量池由方法区移到了堆中
JDK1.8
增加了元空间(Metaspace)用于替代方法区,元空间位于直接内存(堆外内存),JDK1.7及以前存在于运行时数据区域中的方法区消失
站在线程角度分析
线程共享内存区
Java堆
方法区
线程私有内存区
虚拟机栈
本地方法栈
程序计数器
直接内存(堆外内存)
定义:不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域
特点:如果使用了NIO,这块区域会被频繁使用,在Java堆内可以用directByteBuffer对象直接引用并操作;这块内存不受Java堆大小限制,但受本机总内存限制,可以通过MaxDirectMemorySize(默认与堆内存最大值一样),所以也会出现OOM异常
JVM创建对象过程
检查加载
分配内存
划分内存
指针碰撞
内存规整
空闲列表
内存不规整
内存空间初始化(全初始化为0或NULL)
设置(对象头)
对象初始化
对象的内存布局(8byte整数倍)
对象头(Header) 12byte
对象自身的运行时数据(Markword) 8byte
类型指针(Class Pointer) 4byte
实例数据(Instance Data)
对齐填充(Padding) (补齐8byte整数倍)
对象的访问定位
使用句柄
引用指向句柄池中的句柄,由句柄指向具体对象
直接指针(HotSpot)
引用指向具体对象
堆参数设置及内存溢出排查
Java堆溢出
情景1:在某个循环里不停地为对象分配内存空间,对象过多,将堆填满了
java.lang.OutOfMemoryError: GC overhead limit exceeded
情景2:在为对象分配内存空间的时候,对象的大小大于堆大小
java.lang.OutOfMemoryError: java heap space
年轻代配置
-Xms
最小堆大小
-Xmx
最大堆大小
-XX:NewSize
年轻代初始内存大小
-XX:MaxNewSize
年轻代最大内存大小
-Xmn
同时设置年轻代初始内存和最大内存
-XX:NewRatio
年轻代和老年代的比例
-XX:SurvivorRatio
Eden和Survivor的比例,缺省为8
方法区(元空间)和运行时常量池溢出
方法区设置的空间太小程序会直接报错
java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler
虚拟机栈和本地方法栈溢出
栈空间过小会报栈溢出错误,例如无限递归
java.lang.StackOverflowError
本机直接内存溢出
直接内存分配对象的大小超过了直接内存限制大小,一般会发生在NIO网络通信中
java.lang.OutOfMemoryError: Direct buffer memory
GC和内存分配策略
垃圾回收(GC)
垃圾定位(判断对象存活)
引用计数
特点:定位快,使用方便,实现简单
缺点:无法定位相互引用的对象垃圾
可达性分析(根可达法)
GC Roots对象
方法区:类静态属性引用的对象
方法区:常量引用的对象
虚拟机栈(本地变量表)中引用的对象
本地方法栈JNI(Native方法)中引用的对象
Java中的引用类型
强引用
Java中常用的声明一个对象就是强引用
Object object = new Object();
软引用(SoftReference)
一些有用但是非必需,用软引用关联的对象;系统将要发生OOM之前,软引用对象会被GC回收
SoftReference<T> softReference = new SoftReference<T>();
弱引用(WeakReference)
一些有用(程度比软引用更低)但是非必需,用弱引用关联的对象; 软引用对象只能生存到下一次GC之前,不管内存是否足够,软引用对象都会被回收
WeakReference<T> weakReference = new WeakReference<T>();
虚引用(PhantomReference)
也叫幽灵引用,是Java引用类型中最弱的一种引用,无法获得内存对象,唯一的作用是它在被GC的时候会收到一个通知
GC算法
标记-清除算法(Mark-Sweep)
特点:算法简单、效率高
缺点:容易产生碎片或内存不连续空间
复制算法(Copying)
特点:内存规整
缺点:浪费内存空间、需要额外对数据进行拷贝
标记-整理/标记-压缩算法(Mark-Compact)
缺点:同样需要额外对数据进行拷贝
GC分代模型缺省比例1:2
年轻代(Young)缺省比例8:1:1
Eden
From Survivor
To Survivor
老年代(Old)
10种GC回收器
年轻代GC回收器
Serial
算法:复制算法(Copying)
类型:单线程收集器
适用场景:简单高效,适合内存不大的情况
ParNew
类型:并行的多线程收集器
说明:是Serial收集器的多线程版本,如果是多核心CPU,则性能一般比Serial要好,否则性能一般比Serial要差
适用场景:搭配CMS GC回收器的首选
Parellel Scavenge(吞吐量优先收集器)
说明:类似ParNew,更加关注吞吐量,达到一个可控制的吞吐量
适用场景:本身是Server级别多CPU机器上的默认GC方式,主要适合后台运算不需要太多交互的任务
老年代GC回收器
Serial Old
算法:标记-整理算法(Mark-Compact)
适用场景:Client模式下虚拟机使用
Parellel Old
说明:Parellel Scavenge收集器的老年代版本,为了配合Parellel Scavenge的面向吞吐量的特性而开发的对应组合
适用场景:在注重吞吐量以及CPU资源敏感的场合采用
CMS(Concurrent Mark Sweep)
算法:标记-清除算法(Mark-Sweep)
类型:并行与并发收集器
说明:尽可能地缩短垃圾收集时用户线程停止时间; 如果碎片过多,需要分配较大对象时内存空间不够,就会调用Serial Old回收器进行Full GC清理(CMS失败)
缺点
标记-清除算法会产生内存碎片
需要更多CPU资源
由于CMS在垃圾处理过程中是与用户线程同时存在的,因此需要为用户线程分配一定的内存空间
会产生浮动垃圾问题,需要更大的堆空间
过程
初始标记
短暂STW(Stop The World),仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
并发标记
和用户的应用程序同时进行,进行GC Roots Tracing的过程
重新标记
为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记但时间短
并发清除
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户一起工作,所有,从总体上来说,CMS收集器内存回收的过程是与用户线程一起并发执行的
适用场景:重视服务的响应速度、系统停顿时间和用户体验的互联网网站或者B/S系统。互联网后端目前CMS是主流的垃圾回收器
跨代GC回收器
G1
算法:标记-整理算法(Mark-Compact)+化整为零
特点:分代收集+空间整合
说明:JDK1.7才正式引入,采用分区回收的思维,基本不牺牲吞吐量的前提下完成低停顿的垃圾回收,可预测的停顿是其最大的优势
标记GC Roots能直接关联到的对象,速度很快,该过程会产生短暂的STW,并且都会有一次年轻代的GC
根区域扫描
扫描Survivor区可以直接到达老年代区域的对象
与CMS类似,会与应用程序同时进行
与CMS类似,将并发标记阶段未标记的垃圾进行重新标记,该过程会产生STW
独占清理
清理混合标记的内存区域,会计算各个区的存活对象比例并按比例进行排序,优先回收垃圾比例高的内存区域
并发清理
清理完全空闲的内存区域,会与应用程序同时进行,不会造成STW
适用场景:面向服务端应用的垃圾回收器,目标为取代CMS
不分代GC回收器
ZCS
算法:有色指针+加载屏障
类型:可扩展的低延迟垃圾收集器
特色
处理TB量级的堆
GC时间不超过10ms
与使用G1相比,应用吞吐量的降低不超过15%
Shenandoah
JDK测试用GC回收器
Epsilon
JVM上GC的参数设置
查看本机GC回收器
java -XX:+PrintCommandLineFlags -version
打印GC
-XX:+PrintGC
打印GC详情
-XX:+PrintGCDetails
年轻代和老年代都是用串行的GC回收器
-XX:+UseSerialGC
年轻代使用ParNew,老年代使用Serial Old
-XX:+UseParNewGC
年轻代使用Parellel Scavenge,老年代使用Serial Old
-XX:+UseParellelGC
年轻代使用Parellel Scavenge,老年代使用Parellel Old
-XX:+UseParellelOldGC
年轻代使用ParNew,老年代使用CMS
-XX:+UseConcMarkSweepGC
老年代空间超过设置值时,会启动CMS进行垃圾回收
-XX:CMSInitiatingOccupancyFraction
CMS需要进行FullGC的时候,开启内存碎片整理(无法并发,默认开启)
-XX:+UseCMSCompactAtFullCollection
设置在上一次CMS并发GC执行过后,到底还要再执行多少次Full GC才会启用整理(默认为0)
-XX:CMSFullGCsBeforeCompaction
使用G1 GC
-XX:+UseG1GC
控制最大GC停顿时间
-XX:MaxGCPauseMills
设置并行GC的线程数量
-XX:ParallelGCThread
允许垃圾回收时间占运行时间的比率
-XX:GCTimeRatio
吞吐量的倒数(整数)
例如GCTimeRatio的值为19,则允许的GC时间占运行时间的(1/(1+19)=5%)
自适应大小策略
-XX:+UseAdaptiveSizePolicy
如果开启AdaptiveSizePolicy,则每次GC后会重新计算Eden、From和To区的大小,计算依据是GC过程中统计的GC时间、吞吐量、内存占用量
内存分配与回收策略
对象优先在Eden分配
如果Eden内存空间不足,就会触发Minor GC
大对象直接进入老年代
大对象:比较典型的是很长的字符串和大型数组
大对象产生的问题
内存有空间,但是没有连续空间,会导致提前触发GC
年轻代采用复制算法,造成进行大量的内存复制
对应JVM参数
-XX:PretenureSizeThreshold
如果对象大小超过设置阈值,则直接在老年代分配内存空间
缺省为0,表示绝不会直接分配在老年代
长期存活的对象将进入老年代
超过最大分代年龄的对象将进入老年代
除CMS最大默认分代年龄是6,其他GC回收器的最大默认分代年龄是15
对于JVM参数
-XX:MaxTenuringThreshold
设置最大分代年龄,分代年龄超过该值的对象将进入老年代,最大不能超过15
动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保
如果年轻代有大量对象存活,Survivor空间不足,只要老年代的连续空间大于年轻代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则进行Full GC
内存泄露和内存溢出
内存泄露
应用程序中应该被回收的对象没有被GC回收,导致可用内存减少
内存溢出
由于内存空间不够,导致无法为对象分配内存空间
JDK监控与诊断工具
jps
虚拟机进程状况工具
列出当前机器上正在运行的JVM进程
jps -v 列出当前机器上正在运行的JVM进程号及全类名
jstat
虚拟机统计信息监视工具
例如:jstat -gc 18832 250 20(查询进程号为18832的GC信息,每250ms查询一次,一共查询20次)
jinfo
Java配置信息工具
查看和修改JVM参数
jmap
Java内存映像工具
jhat
虚拟机堆转储快照分析工具
jstack
Java堆栈跟踪工具
JConsole
Java监视与管理控制台
图形化
远程进程连接参数
-Djava.rmi.server.hostname
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port
-Dcom.sun.management.jmxremote.authenticate
-Dcom.sun.management.jmxremote.ssl
VisualVM
多合一故障处理工具
MAT监控与诊断工具
概念
浅堆(Shallow Heap)
指一个对象自身所消耗的内存
深堆(Retained Heap)
指对象的保留集中所有的对象的浅堆大小之和
使用:打开.hprof文件
选项
Incoming References
拥有对象C的引用的所有对象都称为对象C的 Incoming References
在对象上右击查看Incoming References信息
Outgoing References
对象C引用的所有对象都称为对象C的Outgoing References
在对象上右击查看Outgoing References信息
Overview
概览信息
Histogram
类的实例信息
JVM的执行子系统
Class类的本质
任何一个Class文件都对应着唯一一个类或接口的定义信息
Class文件是一组以8位字节为基础单位的二进制流
Class文件实际上并不一定以磁盘文件的形式存在
Class类文件格式
各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构体中只有两种数据类型:无符号数和表
无符号数属于基本的数据类型,即u1、u2、u4、u8,可以用来描述数字、索引引用、数量值或者按照UTI-8编码构成字符串值
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾,表用于描述有层次关系的复合数据类型,整个Class文件本质上就是一张表
Class文件格式详解
魔数与Class文件的版本
魔数(Magic Number)
每个Class文件的头4个字节成为魔数,它的唯一作用是确定这个文件是否为一个能被JVM接受的Class文件,使用魔数而不是扩展名来识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可
u4
magic
1
说明:CA FE BA BE
Class文件的版本号
紧接着魔数的4个字节存储的是Class文件的版本号,第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java版本号是45开始的,JDK1.1后的每个JDK大版本发布版本号向上加1。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,JVM也必须拒绝执行超过其版本号的Class文件
u2
minor_version
1
major_version
常量池
常量池容量计数值
常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count),与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的
constant_pool_count
常量池中主要存放两大类,字面量(Literal)和符号引用(Symbolic References),字面量比较接近于Java语言层面的常量概念,如文本字符串,声明为final的常量值等,而符号引用则属于编译原理方面的概念,包括下面三类常量:类和接口的全类名(Fully Qulified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符
cp_info
constant_pool
constant_pool_count-1
访问标志
用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等
access_flags
类索引、父类索引与接口索引集合
这三项数据来确定类的继承关系。类索引用来确定这个类的全类名,父类索引用来确定这个类的父类的全类名,由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应该是extends语句)后的接口顺序从左到右排列在接口索引集合中
this_class
super_class
interfaces_count
interfaces
字段表集合
描述接口或者类中声明的变量。字段(field)包括类级别变量以及实例级变量,而字段名称是什么、字段被定义为什么类型的数据,这些都是无法固定的,只能引用常量池中的常量来描述
字段表集合中不会列出从超类或者父接口中继承来的字段,但有可能列出原本Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段
fields_count
field_info
fields
fields_count
方法表集合
描述了方法的定义,但是方法里的Java代码,经过编译器编译成字节码指令后,存放在属性表集合中一个名为“Code”的属性里面
与字段表集合类似,如果父类方法在子类中没有重写(Override),方法表集合中就不会出现来自父类的方法信息,但同样的,有可能会出现由编译器自动添加的方法,最典型的就是类构造器“<clinit>”和实例构造器“<init>
methods_count
method_info
methods
属性表集合
存储Class文件、字段表、方法表自己的属性表集合,以用来描述某些场景专有的信息,如方法的代码就存储在Code属性表中
attributes_count
attribute_info
attributes
命令行工具
javap -verbose xxx.class
将Class文件的二进制码翻译成可阅读的形式
字节码指令
简介
JVM所有的指令均占1字节,指令数不超过256
由特定的数字代表指令
每条指令后都会带0+个参数,表示指令需要操作的数据
大多数的指令都包含了其操作所对应的数据类型,例如iload指令用于从局部变量表中加载int类型的数据到操作数栈中,而fload指令加载的则是float类型的数据
大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型
分类
加载和存储指令
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
将一个局部变量加载到操作数栈
iload、iload<N>
lload、lload<N>
fload、fload<N>
dload、dload<N>
aload、aload<N>
将一个数值从操作数栈存储到局部变量表
istore、istore<N>
lstore、lstore<N>
fstore、fstore<N>
dstore、dstore<N>
astore、astore<N>
将一个常量加载到操作数栈
bipush、sipush
ldc、ldc_w、ldc2_w
aconst_null
iconst_m1
iconst<i>、lconst<l>、fconst<f>、dconst<d>
扩充局部变量表的访问索引的指令
wide
运算或算术指令
用于对两个操作数栈上的值进行某种特定运算,把结果重新存入到操作数栈顶
加法指令
iadd、ladd、fadd、dadd
减法指令
isub、lsub、fsub、dsub
乘法指令
imul、lmul、fmul、dmul
类型转换指令
可以将两种不同的数值类型进行相互转换
JVM直接支持右侧数值类型的宽化类型转换(即小范围类型向大范围类型的安全转换)
int类型到long、float或者double类型
long类型到float、double类型
float类型到double类型
处理窄化类型转换(Narrowing Numeric Conversions)时,必须显示地使用类型转换指令来完成
i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f
创建类实例指令
new
创建数组的指令
newarray
anewarray
multianewarray
访问字段指令
getfield
putfield
getstatic
putstatic
数组存取相关指令
把一个数组元素加载到操作数栈的指令
baload、caload、saload、iaload、laload、faload、daload、aaload
将一个操作数栈的值存储到数组元素中的指令
bastore、castore、sastore、iastore、fastore、dastore、aastore
获取数组长度的指令
arraylength
检查类实例类型的指令
instanceof
checkcast
操作数栈管理指令
如同操作一个普通数据结构中的堆栈那样,JVM提供了一些用于直接操作操作数栈的指令
将操作数栈的栈顶一个或两个元素出栈
pop、pop2
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶
dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
将栈最顶端的两个数值互换
swap
控制转移指令
可以让JVM有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值
条件分支
ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmple、if_icmpge、if_acmpeq和if_acmpne
复合条件分支
tableswitch、lookupswitch
无条件分支
goto、goto_w、jsr、jst_w、ret
方法调用指令
invokevirtual
用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式
invokeinterface
用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用
invokespecial
用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
invokestatic
用于调用类方法(static方法)
invokedynamic
用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前4条调用指令的分派逻辑都固化在JVM内部,而该指令的分派逻辑是由用户所设定的引导方法决定
方法调用指令与数据类型无关
方法返回指令
是根据返回值类型区分的
ireturn
返回值是boolean、byte、char、short和int类型时使用
lreturn
freturn
dreturn
areturn
另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用
异常处理指令
在Java程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现的
同步指令
由monitorenter、monitorexit两条指令来支持synchornized关键字的语义
类加载机制
加载(Loading)
加载过程
1. 加载类的二进制流
2. 把静态存储结构转换为方法区的运行时结构
3. 生成一个代表该类的Class对象
连接(Linking)
验证(Vertification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
有且只有5种情况才必须对类进行初始化
采用后面对指令new、getstatic、putstatic、invokestatic
对类进行反射调用
初始化一个类,但是父类尚未初始化的时候,首先触发父类的初始化
指定一个执行main方法(入口类)的类
动态语言支持
卸载(Unloading)
类加载器
用途
热加载
代码保护和加解密
类层次划分
OSGi
唯一性
一个类是否唯一,由这个类本身和加载它的类加载器两个因素一起决定
系统的类加载器
启动类加载器(Bootstrap ClassLoader)
存放在<JAVA_HOME>\\lib目录中的,并且是虚拟机识别的类库加载到虚拟机内存中
由C++语言实现,是JVM的一部分
扩展类加载器(Extension ClassLoader)
存放在<JAVA_HOME>\\lib\\ext目录中的所有类库,开发者可以直接使用
由Java语言实现,是JDK的一部分
应用程序加载器(Application ClassLoader)
加载用户类路径上指定的类库,开发者可以直接使用,一般情况下,这个就是程序中默认的类加载器
自定义类加载器(Custom ClassLoader)
一般重写findClass方法,尽量避免重写loadClass方法(loadClass是实现双亲委派模型逻辑的主要方法)
双亲委派模型
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载
好处
Java类随着它的类加载器一起具备了带有优先级的层次关系,保证Java程序稳定运行
破坏双亲委派模型
线程上下文类加载器
通过Thread类中的setContextClassLoader方法设置
使用场景:JDBC
不是传统意义上的继承,而是一种委托关系
栈帧
运行时栈帧结构
局部变量表(Local Variable Table)
操作数栈(Operand Stack)
动态连接(Dynamic Linking)
返回地址(Return Address)
方法调用
解析
定义:调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析
主要包括:静态方法、私有方法、实例构造器和父类方法
分派
静态分派
定义:依据静态类型来确定执行哪个方法
原因:(1)静态类型的变化仅仅在使用时才发生,变量本身的静态类型是不会被改变,并且最终静态类型在编译期是可知的;(2)实际类型的变化是在运行期才知道,编译器在编译程序时并不知道一个对象的具体类型是什么
场景:静态分派的典型就是方法重载
动态分派
定义:依据变量的动态类型来确定执行哪个方法
场景:动态分派的典型场景就是方法重写
重写的本质:虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类没有被重写,那么子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口;如果子类重写了这个方法,子类方法表中的地址将会转换为指向子类实现版本的入口地址
编写高效优雅的Java程序
常见原则
面向对象
1. 构造器参数太多怎么办?
推荐:如果构造参数在5个及以上,或者当前前构造参数还没有超过5个,但未来会突破5个的情况下,应当使用建造者(Builder)模式
2. 不需要实例化的类应该构造器私有
3. 不要创建不必要的对象
推荐:(1)可以使用基本数据类型尽量使用基本数据类型,避免数据自动装箱;(2)经常被使用的不变对象尽量使用静态变量,避免每次使用都创建对象
4. 避免使用终结方法(Object里的finalize()方法)
推荐:释放资源尽量使用try{}finally{}语句块
5. 使类和成员的可访问性最小化
目标:高内聚、低耦合
6. 使可变性最小化
目标:保证线程安全
7. 优先使用复合优于继承
8. 接口优于抽象类
原因:Java是单继承的,继承了一个类以后就不能再继承第二个类,而实现接口不受限制;当对接口进行功能扩展的话,只需要额外定义一个实现该接口的类即可,而如果对继承扩展,除需定义继承父类的子类,还需额外修改父类中的定义
JDK骨架类设计思想:首先定义一个接口,声明一个抽象的骨架类实现接口,但是实际的业务类可以同时实现接口,又继承骨架类
方法
9. 可变参数要谨慎使用
问题:可变参数允许传0个参数;如果是参数个数在1~N之间的时候,要做单独的业务控制
推荐:如果是允许传0个参数的情况,尽量使用增强for循环来处理;如果第一个参数需要做单独处理,尽量把第一个参数单独拿出来作为一个参数使用,后面的参数再用增加for循环来进行处理
10. 返回零长度的数组或集合,尽量不要返回null
问题:如果可能返回null,则需要客户端单独处理为null的情况
11. 优先使用标准的异常
目标:追求代码的重用
JDK定义的常用异常
IlleagalArgumentExcption
调用者传入的参数非法
IllegalStateException
接受对象的状态非法
NullPointerException
空指针异常
UnsupportedOperationException
不支持的操作
例如:模版设计模式中,模版方法没有实现,会抛出该异常
通用程序设计
12. 用枚举代替int常量
13. 将局部变量的作用域最小化
目标:帮助GC;减小栈帧的大小
推荐:(1)在第一次使用的地方进行声明;(2)如果变量初始化条件不满足,尽量就不要声明
14. 精确计算,避免使用float和double
推荐:(1)转换成int或long进行计算;(2)使用BigDecimal
15. 当心字符串连接的性能
推荐:(1)尽量避免字符串连接,在输出字符串之前可以先进行判断;(2)如果需要大量字符串拼接,可以考虑使用StringBuilder或StringBuffer
16. 控制方法的大小
深入了解性能优化
常用的性能评价/测试指标
响应时间
定义:提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间
参考
打开一个站点
几秒
数据库查询一条记录(有索引)
十几毫秒
机械磁盘一次寻址定位
4毫秒
从机械磁盘顺序读取1M数据
2毫秒
从SSD磁盘顺序读取1M数据
0.3毫秒
从远程分布式换成Redis读取一个数据
0.5毫秒
从内存读取1M数据
十几微秒
Java程序本地方法调用
几微秒
网络传输2KB数据
1微秒
并发数
定义:指同一时刻,对服务器有实际交互的请求数
估算:根据在线用户数估计并发数,例如有1000用户同时在线,并发数大概在50-150之间
吞吐量
定义:对单位时间内完成的工作量(请求)的量度
场景:每分钟的数据库事务、每秒传送的文件千字节数、每分钟的Web服务器命中数
评价指标之间的关系
通常,平均响应时间越短,系统吞吐量越大;平均响应时间越长,系统吞吐量越小;但是系统吞吐量越大,未必平均响应时间越短
常用的性能优化手段
总原则
避免过早优化
进行系统性能测试
寻找系统瓶颈,分而治之,逐步优化
前端优化
浏览器/App
减少请求数
合并CSS/JS/图片/字体
使用客户端缓冲
静态资源文件缓存在浏览器
启用压缩
GZIP,但会给浏览器和服务器带来性能压力,需要权衡
资源文件加载顺序
CSS文件放在页面的最上面,JS文件放在最下面(html文件引入时)
减少Cookie传输
静态资源尽量不使用Cookie机制
给用户提示(非技术手段)
CDN(内容分发网络)加速
反向代理缓存
Web组件分离
不同静态资源使用不用域名路径,缓解因浏览器内部机制造成的相同域名静态资源的加载限制
应用性能优化
缓存
网站性能优化第一定律:优先考虑使用缓存优化性能
推论:缓存离用户越近越好
合理使用缓存的准则
频繁修改的数据,尽量不要缓存
需要缓存的数据,读写比尽量在2:1以上
缓存一定是热点数据
使用缓存,应用就要容忍一定时间的数据不一致
需考虑缓存可用性问题
事先加入缓存预热
对于缓存击穿,采取(1)加入布隆过滤器;(2)把不存在的数据缓存起来
集群
负载均衡
异步
同步和异步:关注结果消息的通信机制
阻塞和非阻塞:关注等待结果返回给调用方的状态
常见的异步
Servlet异步
线程池
消息队列
程序
代码基本
选择合适的数据结构
选择更优的算法
编写更少的代码
并发编程
充分利用CPU多核,尽量使用线程池,合理设置线程数量,尽量使用JDK提供的各种并发框架和工具
实现线程安全的类,避免线程安全问题
同步下减少锁的竞争
缩小锁的范围,减少锁的粒度,锁分段
替换独占锁,读写锁,CAS代替锁,ThreadLocal等
资源的复用
减少开销很大的系统资源的创建和销毁
单例模式
池化技术
JVM
与JIT编译器相关的优化
选择编译器类型
-client
-server
-XX:+TieredComlilation 分层编译
代码缓存相关
存放JIT编译好的汇编语言代码
-XX:ReservedCodeCacheSize=N
编译阈值
方法调用计数器
循环回边计数器
计数器半衰期
-XX:+UseCounterDecay 启用衰减计数
-XX:+CounterHalfLifeTime 控制半衰期的周期
编译线程
方法内联
默认方法编译好的字节码小于325字节才会内联
-XX:MaxFreqInlinesSize=N 设置字节码小于多少会内联
方法编译好的字节码小于35字节一定会内联
逃逸分析
GC调优
目的
GC的时间够小
GC的次数够少
调优的原则
1. 大多数的Java应用不需要调优
2. 大部分需要GC调优的不是参数问题,而是代码问题
3. GC调优是最后的手段
调优的优先级
1. 选择合适的GC回收器
2. 选择合适的堆大小
3. 选择年轻代占堆中的比例
调优的步骤
1. 监控GC的状态
2. 分析结果,判断是否需要优化
Minor GC 时间 < 50ms,频率 < 10s/次
Full GC 时间 < 1s,频率 < 10min/次
其他与GC相关的参数
打印GC详细日志
-Xlogger:logpath
指定GC Log的输出目录
-XX:+HeapDumpOnOutOfMemoryError
发生OOM错误时做堆快照
-XX:+HeapDumpPath
堆快照文件存储路径
-XX:+PrintHeapAtGC
GC时打印堆信息
-XX:+TraceClassLoading
跟踪类加载顺序
存储性能优化
尽量使用SSD
定时清理数据或者按数据的性质分开存放
结果集处理
0 条评论
下一页