1.jvm运行时数据结构:
程序计数器
java虚拟机栈
本地方法栈
堆
方法区
(1) 程序计数器:记录当前线程正在执行字节码指令的地址(如果正在执行的是java方法,
该计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行
的是Native方法,该计数器的值为空(Undefined))。该区域是线程私
有的,生命周期随着线程的创建而创建,随着线程的消亡而消亡。读取
指令举例:如顺序、分支、循环、跳转、异常处理、线程恢复等。
(2)Java虚拟机栈:即我们所说的栈,线程私有,由栈帧组成。它代表的是执行逻辑
定义:虚拟机栈描述的是java方法执行的内存模型。每个方法执行时都会创建一个栈帧
用于存储局部变量表,操作数栈、动态链接、方法出口等信息,每个方法被调用
直至调用完成的过程,就对应一个栈帧在虚拟机中从入栈到出栈的过程。例如:
函数1对应栈帧1、函数2对应栈帧2、函数3对应栈帧3、函数4对应栈帧4;函数1
调用函数2、函数2调用函数3、函数3调用函数4;当函数1被调用时时栈帧1入栈、
函数2被调用时栈帧2入栈、函数3被调用时栈帧3入栈、函数4被调用时栈帧4入栈;
当函数4执行完栈帧4出栈并执行函数3;函数3执行完栈帧3出栈并执行函数2;函
数2执行完栈帧2出栈并执行函数1;函数1执行完栈帧1出栈。当前正在执行的函数
锁对应的帧一定是位于栈底的帧。
栈帧包括:局部变量表、操作数栈、动态链接、方法出口信息等
局部变量表:局部变量表是一组变量值存储空间,用于存放方 法参数和方法内
部定义的局部变量。它存放是8种基础类型+方法的返回类型+引用。
在Java程序被编译为Class文件时,就在方法的Code属性的
max_locals数据项中确定了方法所需要分配的最大局部变量表的容
量。
Class Person{
private int a=0;//整条语句放在堆上(=左边和右边的,因为线程不安全所以放堆上,下同)
private String s="123";//整条语句放在堆上 private Person p;//堆上
private static int a;//方法区 public void f(String s//局部变量表) { int a;//局部变量表
int b=0;//整条语句放在局部变量表
String s="123"//s放在局部变量表,“123”放在常量池 String 比较特殊 dobble s;//局部变量表
Person p//局部变量表=new Person("zhansan");//堆
} }
动态链接:即用到某个类才加载到内存
静态链接:所有类都加载无论是否用到
操作数栈:Java 虚拟机的指令是从操作数栈中而不是从寄存器
中取得操作数的,因此它的运行方式是基于栈的而
不是基于寄存器的。如下例:
比如 a = 1 + 2 iload_0 //将 1 压入操作数栈 iload_1 //将 2 压入操作数栈 iadd //从操作数栈中弹出 1、2,将算出的值 3 压入操作数栈 istore_2 //把 3 从操作数栈中弹出,保存到本地变量区
(3)本地方法栈:和虚拟机栈类似,区别在与虚拟机栈为虚拟机执行java方法时服务,
而本地方法栈则为虚拟机使用到的Native方法服务,本地方法被执
行的时候,在本地方法会创建一个栈帧,用于存放局部变量表、操
作数栈、动态链接、出口信息。
(4)堆:它存放是的数据,new 的东西比如new String()、new Person()、new 数组,
都是存放在堆上。
此内存区域的唯一目的就是存放对象实例,Java虚拟机规范中的描述是:“所
有对象实例以及数组都要在堆上分配”,但是随着JIT编译器的发展与逃逸分析
技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,
所有对象都分配在堆上也渐渐变得不是那么绝对了。该区域是线程共享的,垃
圾回收的主要场所,Java虚拟机规范规范规定:Java堆可以处于物理上不连续
的内存空间中,只要逻辑上连续即可。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出
OutOfMemoryError异常。注:实例变量的值是放在堆空间的。
(5)方法区:堆的一个逻辑部分,线程共享,存放已经被虚拟机加载的类信息(元数据)、
常量、静态变量、即时编译器编译后的代码等。JDK1.8叫Meta Space(元数
据空间)对于 HotSpot该区域叫做永久代,但在未来的版本中将被废弃。
及时编译器:即JIT。Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚
拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认
定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟
机将会把这些代码编译成与本地平台相关的机器码,并进行各种层
次的优化,完成这个任务的编译器称为即时编译器(Just In Time
Compiler,下文统称JIT编译器)
热点代码:如下例,for循环里面的就属于热点代码
for(i=0;i<10000;i++){
}
编译和解释:编译型语言在编译过程中生成目标平台的指令,解释型语言在运行过
程中才生成目标平台的指令。
看一道面试题:
public static void main(String[] args) { String s1="hello";//s1在局部变量表,hello在常量池 String s2=new String("hello");//堆 System.out.println(s1==s2); System.out.println(s1.equals(s2)); }
结果是fase、true,原因看下图
2.判断对象已死吗?
(1)判断对象已死的两种算法
(a)引用计数法
堆中的每一个对象都会有一个引用计数值,每当有一个地方引用它时,计数器值
加1;当应用失效时,计数器值减1;任何时刻计数器值为0对就时不可能在被使
用的。在执行垃圾回收时,那些引用计数值为0的对象将会被回收引用计数法是
个不错的算法,效率很高,但是它无法解决对象间相互循环引用的问题,如下例:
/** * 执行后,objA和objB会不会被GC呢? */ public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过 */ private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; //假设在这行发生了GC,objA和ojbB是否被回收 System.gc();
在testGC()方法中,对象objA和objB都有字段instance,赋值令objA.instance=objB
及objB.instance=objA,除此之外这两个对象再无任何引用,实际上这两个对象都
已经不能再被访问,但是它们因为相互引用着对象方,异常它们的引用计数都不为
0,于是引用计数算法无法通知GC收集器回收它们
(b)可达性分析算法。
在主流的商用语言(Java,C#)中都是通过可达性分析算法来判断对象是否存活的。
这个算法的基本思路就是通过一系列称为“GC Root”的对象作为始点,从这些节点开
始向下搜索,搜索所走过的路称为引用链(Reference Chain),当一个对象到
GC Roots没有任何引用链相接(不可达)时,则证明此对象时不可用的。如下图所
示,object5、object6、object7虽然相会关联,但是它们到GC Root是不可达的,所
以将会被判定为是可回收的对象。
在Java中,可作为GC Root的对象包括下面几种:
(a)虚拟机栈(栈帧中的本地变量表)中引用的对象。
(b)本地方法栈中JNI(即一般说的Native方法)引用的对象
(c)方法区内类静态属性引用的对象和常量引用的对象。
变成不可达:自己理解是栈帧出栈。
必须满足下面两个条件:
(1)对象不可达。
(2)经历两次标记
如果进行垃圾回收的时候发现一个对象没有在GC Root链上,那么就需要进行
两次的标记过程,如果当前发现没有关联在GC Root链上,那么就会进行第一次标
记,如果此时对象的finalize()方法没有被覆盖或该方法已经被虚拟机调用过,那么
此时将被标记为没有必要执行,此时该对象会被放入“即将回收”集合,否则就会放
入F-Queue的对象中等待执行finalize()方法,如果在此方法中对象将自己与GC Root
链上的任何一个对象关联(譬如把自己(this)赋值给某个类变量或者对象的成员变
量),那么就会被移出”即将回收“集合。
对象在经历可达性分析算法后发现没有与其相连接的引用链,那他将被第一次标
记并进行一次筛选,筛选的条件是此对象是否有必要执行finalizfe()方法。当对象没有
覆盖finalized()方法,或者finalized()方法已经被虚拟机调用过,虚拟机会将这两种情
况都视为“没有必要执行”。
哪些对象可作为GC Root:
"JAVA虚拟机栈中的本地变量引用对象; 方法区中静态变量引用的对象;方法区中常
量引用的对象; 本地方法栈中JNI引用的对象”
人话版GC Root:
tracing gc的基本思路是,以当前存活的对象集为root,遍历出他们(引用)关联的所
有对象(Heap中的对象),没有遍历到的对象即为非存活对象,这部分对象可以gc
掉。这里的“初始存活的对象集”就是GC Roots。
选"JAVA虚拟机栈中的本地变量引用对象; 方法区中静态变量引用的对象;方法区中常
量引用的对象; 本地方法栈中JNI引用的对象”这些对象的依据是什么?
答:当前栈帧中的引用型变量;静态变量引用的对象;本地方法栈JNI的引用对象,很
明显是当前存活对象。所以他们比如是GC Roots的一部分。方法区中的常量引用
对象,在当前可能存活,因此,也可能是GC roots的一部分。还有其他一些对象也
可能是GC Roots的一部分,比如被classloader加载的class对象,monitor的对象,
被JVM持有的对象等等,这些都需要视当前情况而定。
3.垃圾回收算法
(1)标记清除算法:分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,
清除阶段回收被标记的对象所占用的空间,该算法最大的问题是内存
碎片产生严重。
(2)复制算法:为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容
量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后
将尚存活的对象复制到另一块上去,把已使用的内存清掉。这种算法虽然
实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压
缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降
低。
(3)标记压缩算法:标记阶段和标记算法相同,标记后不是清理对象而是将存活的对象移
动到内存的一端,然后清除边界外的对象。
(4)分代算法:分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活
的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代
(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是
每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时
都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。新生代
采用复制算法,老年代采用标记压缩算法。
(5)分区算法:将整个内存分为N个小的独立空间,每个小空间都可以独立使用,这样细粒
度控制一次回收多少个小空间和哪些个小空间,而不是对整个空间进行GC,
从而提升性能。
3.垃圾回收器
用一张图来表示下面各种垃圾收集器的工作区域,然后在分别介绍各种垃圾收集器。
并行收集器(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状
态。
并发收集器(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可
能会交替执行),用户程序在继续运行,而垃圾收集程序运行
于另一个CPU上。其中CMS是并发收集器、G1是并发且并行的
收集器。
(1)Serial收集器:新生代收集器,是一个单线程的收集器,采用的是“复制算法”。它的
“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去
完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其它
所有工作线程,直到它收集结束。
(2)ParNew收集器:是Serial收集器的多线程版本,采用的是“复制算法”。除了使用多线
程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参
数、收集算法、Stop The World、对象分配规则、回收策略等都与
Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的
代码。ParNew收集器除了多线程收集之外,其它与Serial收集器相比
并没有太多创新之处,但它却是运行在Server模式下的虚拟机中首选
的新生代收集器,其中一个与性能无关但很重要的原因是,除了Serial
收集器外,目前只有它能与CMS收集器配合工作。
使用-XX:+UseParNewGC开启并行回收器,此时老年代默认使用串行回收器
ParnNew回收器使用“-XX:ParallelGCThreads参数指定”工作线程数量,一般最好和计
算机的CPU数量相当。
(3)Parallel Scavenge收集器:
是工作在新生代的垃圾回收器,使用了复制算法,也是多线程独占(独占回收器:
GC来的时候应用停顿只执行GC)形式的收集器,它的特点是关注系统的吞吐量,
如下两个参数控制系统的吞吐量:
-XX:MaxGCPauseMills:设置最大垃圾回收停顿时间,将此值减少可以减少GC垃圾
会减少垃圾收集的停顿时间,但是会导致GC频繁,重而增加了垃圾回收的总时间,
降低了吞吐量,所以需要根据实际情况设置该值。
-XX:GCTimeRatio:设置吞吐量大小,他是一个0到100之间的整数,默认情况下他的
取值是99,那么系统将花费不超过1/(1+n)的时间用于垃圾回收,也就是
1/(1+99)=1%的时间。
-XX:+UseAdaptiveSizePollcy:打开自适应模式,在这种模式下新生代的大小、eden、
from/to的比例,以及晋升到老年代的对象年龄参数会
自动调整,以达到堆大小、吞吐量和停顿时间之间的平
衡点。
吞吐量的计算方法:吞吐量=运行用户代码的时间 / (运行用户代码时间+垃圾收集的时间)
(4)Serial Old收集器:
是Serial收集器的老年代版本,它同样是一个单线程的收集器,使用“标记-整理”算法。
这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那
么它主要还有两大用途:
一种用途在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用。
另一种用途就是作为CMS收集器的后备方案,在并发收集器发生
Concurrent Mode Failure时使用。
(5)Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
这个收集器在JDK1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器
一直处于比较尴尬大的状态。原因是如果新生代选择了Parallel Scavenge收集器,老年
代除了Serial Old收集器外别无选择。由于老年代Serial Old收集器在服务端性能上的拖
累,使用Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大的效果。直到
Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合。
-XX:+UseParallelOldGC进行设置使用
-XX:+ParallelGCThreads:设置垃圾回收时的线程数量。
(6)CMS回收器:
工作在老年代的回收器,使用的是标记清除法,是一种以获取最短回收停顿时间为目标
的收集器。主要关注的是系统的停顿时间。CMS并不是独占的回收器,也就是说CMS回
收的过程中,应用程序仍然在不停的工作,又会有新的垃圾不断产生,所以在使用CMS
过程中应该确保应用程序的内存足够可用,CMS不会等到应用程序饱和的时候在去回收
圾,而是到达某个阈值的时候就去回收,回收的阈值可以通过指定的参数来设置
-XX:CMSInitiatingOccupancyFraction来指定,默认值是68,也就是说当老年代的使用率
达到68%的时候会执行CMS垃圾回收,如果内存使用增长的很快,在CMS过程中出现了
内存不足的情况,CMS回收就会失败,虚拟机将启用老年代串行回收器进行垃圾回收,
这会导致应用程序中断,直到垃圾回收完成后才会正常工作,这个过程GC停顿时间可能
过长,所以设置-XX:CMSInitiatingOccupancyFraction要根据实际情况。
如下两个参数:
-XX:UseCMSCompactAtFullCollection:使CMS完成之后进行碎片整理。
-XX:CMSFULLGCsBeforeCompaction:设置多少次CMS回收后对内存进行一次压缩。
运作过程:初始标记、并发标记、重新标记、并发清除。有的版本叫(另外CMS收集
器比较详细的步骤查看:https://blog.csdn.net/zqz_zqz/article/details/70568819)
初始标记、重新标记这两个步骤仍需“Stop The World”。初始标记仅仅只是标记一下
GC Root能直接关联到的对象(不包括间接到达的哈,没有直接关联的对象在并发标记
阶段标记哈),速度很快;并发标记阶段就是进行GC Roots Tracing的过程;重新标记
则是为了修正并发标记期间因用户程序继续而导致标记产生变动的那一部分对象的标记
记录,这个时间的停顿时间一般会比初始标记阶段稍长些,但远比并发标记短。在这个
阶段暂停所有用户线程,重新扫描堆中的对象,进行可达性分析,标记活着的对象,特
别需要注意一点,这个阶段是以新生代中对象为根来判断对象是否存活的;并发清除,
这个阶段主要是清除那些没有标记的对象并且回收空间,由于CMS并发清理阶段用户线
程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标
记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这
一部分垃圾就称为“浮动垃圾”。
缺点:
(A)CMS收集器对CPU资源非常敏感(其实应该说它比较消耗cpu资源)。在并
发阶段,它虽然不会导致用户线程停顿,但是会因为占用一部分CPU资源而
导致应用程序变慢。CMS默认启动的回收器线程数量是 (CPU数量+3) / 4,
也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU
资源,并且随着CPU数量的增加而下降。但是当CPU的数量不足时(譬
如2个),CMS对用户程序的影响就可能变得很大。为了应付这种情况,
虚拟机提供了一种称为“增量式并发收集器”(i-CMS)的CMS收集器变种,
就时在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少线
程的独占资源的时间。在目前版本中i-CMS已经被声明为“deprecated”,即
不在提倡用户使用。
(B)CMS处理器无法处理浮动垃圾。
(C)基于“标记-清除”算法实现的收集器,会产生大量额空间碎片,会出现并发
模式失败的情况,并发模式失败就要执行内存整理(内存压缩),这种情
况下会导致CMS比Parallel Scavenge更慢(通常情况下CMS比
Parallel Scavenge要快)。
参数:
-XX: +UseCMSCompactAtFullCollection:用于CMS收集器顶不住要进行
FullGC时开启内存碎片合并整理的过程(默认开启)。内存整理的过程是
无法并行的,空间碎片的问题没有了,但停顿时间不得不变长。
-XX: CMSFullGCBeforeCompaction:这个参数用于设置执行多少次不压
缩的Full GC后,跟着来一次带压缩的(默认是0,表示每次进入Full GC
的时候都进行碎片整理)。
(4)G1回收器:
是JDK1.7中提出的基于“标记-整理”算法的垃圾回收器,它的使命是取代CMS
回收器,可建立可预测的停顿时间模型属于分代垃圾回收器 区分新生代和老
年代,依然有eden和from、to区,它不要求新生代、老年代、eden、from、
to区空 间都 连续,使用了分区算法。据说JDK1.7之后使用的是G1回收算法,
不过有待考证,目前不能确定,但是在
JDK1.7里它还不太成熟。
G1收集器将整个Java堆划分为大小相等的独立区域(Region),虽然还
保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它
们都是一部分Region(不需要连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划
地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个堆里面的垃圾
堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),
在后台维护一个优先列表,每次根据允许的回收时间,优先回收价值最大
的Region。
特点:
并行和并发:G1充分利用多CPU、多核环境下的硬件优势,使用多个CPU
来缩短Stop-The-World停顿的时间,部分其它收集器原本需要
停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的
方式让Java程序继续执行。
分代收集:存在Eden、from、to。不需要其它收集器的配合就能独立管理
整个GC堆,可以自己处理堆中的对象。
空间整合:重整体来看是基于“标记-整理”算法实现的;局部(两个Region
之间)来看是基于“复制”算法的实现的,但这两种算法都不会
产生内存碎片。
可预测停顿:能建立可预测的停顿时间模型。能让使用者明确指定一个长
度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超
过N毫秒。(这点是G1相对于CMS的另外一大优势,低停
顿是G1和CMS共同的关注点,但G1除了追求低停顿外,还
能建立可预测的停顿时间模型)
执行过程:如果不计算维护Remembered Set的操作,G1收集器的运作
大致可划分为以
下几个步骤:
初始标记、并发标记、并行标记、最终标记、筛选回收。
初始标记仅仅是标记一下GC Root能直接关联到的对象,并
且修改TAMS(Next Top at Mark Start)的值,让下一阶段
用户程序并发运行时,能在正确可用的Region中创建对象,
这阶段需要停顿但时耗时短;并发标记是从GC Root开始对
堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,
但可与用户程序并发执行;最终标记是为了修正在并发标记期
间因用户程序继续运作而导致标记产生变动的那一部分标记记
录;筛选回收阶段首先要对各个Region的回收价值和成本进行
排序,根据用户所期望的GC停顿时间来制定回收计划,根据
Sun公司透露的信息来看,这个阶段其实可以做到与用户程序
一起并发执行。
配置:
使用:-XX:+UseG1GC应用G1收集器
使用:-XX:MaxGCPauseMillis指定最大停顿时间
使用:-XX:ParallelGCThreads设置并行回收的线程数量。
关于G1的其它介绍,看书。
(5)各版本JDK默认的垃圾回收器
jdk1.7 默认垃圾收集器
Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器
Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
-XX:+PrintCommandLineFlagsjvm参数可查看默认设置收集器类型
-XX:+PrintGCDetails亦可通过打印的GC日志的新生代、老年代名称判断
4.如何判断对象已死
必须满足下面两个条件:
(1)对象不可达。
(2)经历两次标记
如果进行垃圾回收的时候发现一个对象没有在GC Root链上,那么就
需要进行两次的标记过程,如果当前发现没有关联在GC Root链上,那么
就会进行第一次标记,如果此时对象的finalize()方法没有被覆盖或该方法
已经被虚拟机调用过,那么此时将被标记为没有必要执行,此时该对象
会被放入“即将回收”集合,否则就会放入F-Queue的对象中等待执行
finalize()方法,如果在此方法中对象将自己与GC Root链上的任何一个对
象关联(譬如把自己(this)赋值给某个类变量或者对象的成员变量),
那么就会被移出”即将回收“集合。
对象在经历可达性分析算法后发现没有与其相连接的引用链,那他将
被第一次标 记并进行一次筛选,筛选的条件是此对象是否有必要执行
finalizfe()方法。当对象没有覆盖finalized()方法,或者finalized()方法已经
被虚拟机调用过,虚拟机会将这两种情况都视为“没有必要执行”。
哪些对象可作为GC Root:
"JAVA虚拟机栈中的本地变量引用对象; 方法区中静态变量引用的对象;
方法区中常量引用的对象; 本地方法栈中JNI引用的对象”
人话版GC Root:
tracing gc的基本思路是,以当前存活的对象集为root,遍历出他们
(引用)关联的所有对象(Heap中的对象),没有遍历到的对象即为
非存活对象,这部分对象可以gc掉。这里的“初始存活的对象集”就是
GC Roots。选"JAVA虚拟机栈中的本地变量引用对象; 方法区中静
态变量引用的对象;方法区中常量引用的对象; 本地方法栈中JNI
引用的对象”这些对象的依据是什么?
答:当前栈帧中的引用型变量;静态变量引用的对象;本地方法栈
JNI的引用对象,很明显是当前存活对象。所以他们比如是
GC Roots的一部分。方法区中的常量引用对象,在当前可能存
活,因此,也可能是GC roots的一部分。还有其他一些对象也可
能是GC Roots的一部分,比如被classloader加载的class对象,
monitor的对象,被JVM持有的对象等等,这些都需要视当前情
况而定。
5.减少GC开销的编程技巧
http://www.importnew.com/10472.html
(1)不要显式调用System.gc()
此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它
会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。
(2)尽量减少临时对象的使用
临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于
减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时
间,减少了主GC的机会。
(3)尽量使用StringBuffer,而不用String来累加字符串
由于String是固定长的字符串对象,累加String对象时,并非在一个
String对象中扩增,而是重新创建新的String对象,如
Str5=Str1+Str2+Str3+Str4,这条语句执行过
程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的
String对象,但这
些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。
(4)计划好List的容量
像ArrayList这样的动态集合用来存储一些长度可变化数据的基本结
构。ArrayList和一些其他的集合(如HashMap、TreeMap),底层
都是通过使用Object[]数组来实现的。而String(它们自己包装在
char[]数组中),char数组的大小是不变的。那么问题就出现了,
如果它们的大小是不变的,我们怎么能放item记录到集合中去呢?
答案显而易见:分配更多的数组。所以,无论什么时候,尽可能
的给List或者Map分配一个初始容量,就像这样。
(5)能用基本类型如Int,long,就不用Integer,Long对象
基本类型变量占用的内存资源比相应对象占用的少得多,如果没有
必要,最好使用基本变量。
(6)尽量少用静态对象变量
静态变量属于全局变量,不会被GC回收,它们会一直占用内存。
(7)对象不用时最好显式置为Null
一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显
式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效
率。
(8)分散对象创建或删除的时间(个人觉得这个似乎不太现实)
集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大
量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或
整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一
样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从
而大大增加了下一次创建新对象时强制主GC的机会。
6. 关于OutOfMemoryError和StackOverflowError
程序计数器:唯一一个在Java虚拟机规范中没有规定任何OutOfMemmoryError情况的
区域。
Java虚拟机栈:会抛出StackOverflowError和OutOfMemoryError两种异常
本地方法栈:会抛出OutOfMemmoryErro异常
堆、方法区都会抛出OutOfMemoryError异常
发生OutOfMemoryError的原因:概括的说就是发生申请到足够多的内存的时候。
具体原因有如下几点:
-- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
-- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
-- 代码中存在死循环或循环产生过多重复的对象实体;
-- 使用的第三方软件中的BUG;
-- 启动参数内存值设定的过小;
6.jvm优化
https://www.cnblogs.com/csniper/p/5592593.html
https://tech.meituan.com/jvm_optimize.html
http://www.importnew.com/3146.html:“成为Java GC专家”系列
(1)内存泄漏及解决方法
系统崩溃前的征兆:
(a)每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左
右,FullGC的时间也由之前的0.5s延长到4、5sFullGC的次数
越来越多,最频繁时隔不到1分钟就进行一次FullGC年老代的
内存越来越大并且每次FullGC后年老代没有内存被释放。
为什么崩溃前垃圾回收的时间越来越长?
答:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存
标记、清除(复制),标记部分只要内存大小固定时间是
不变的,变的是复制部分,因为每次垃圾回收都有一些回
收不掉的内存,所以增加了复制量,导致时间延长。所以,
垃圾回收的时间也可以作为判断内存泄漏的依据
(b)FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次
FullGC。为什么Full GC的次数越来越多?
答:因为内存的积累,逐渐耗尽了年老代的内存,导致新对象
分配没有更多的空间,从而导致频繁的垃圾回收。
(c)年老代的内存越来越大并且每次FullGC后年老代没有内存被释
放。为什么年老代占用的内存越来越大?
答:因为年轻代的内存无法被回收,越来越多地被Copy到年老
代。
解决方案:
(a)生成堆的dump文件
(b)分析dump文件
(c)分析内存泄漏
(2)性能调优
性能调优包括诸多,JVM调优是最后一步。例如,CPU长期不足3%,
系统吞吐量不够,针对8core×16G、64bit的Linux服务器来说,是严
重的资源浪费。在CPU负载不足的同时,偶尔会有用户反映请求的
时间过长,我们意识到必须对程序及JVM进行调优,可以重下面几
个方面进行:
(a)线程池:解决用户响应时间长的问题。
(b)连接池
(c)JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞
吐量。VM调优要记住如下原则:
(i)多数的Java应用不需要在服务器上进行GC优化。
如下面这种情况是不需要优化的:
Minor GC执行时间不到50ms;
Minor GC执行不频繁,约10秒一次;
Full GC执行时间不到1s;
Full GC执行频率不算频繁,不低于10分钟1次;
(ii)多数导致GC问题的Java应用,都不是因为我们参数设置
错误,而是代码问题;
(iii)在应用上线之前,先考虑将机器的JVM参数设置到最优
(最适合)。
(iv)减少创建对象的数量。
(v)减少使用全局变量和大对象;
(vi)GC优化是到最后不得已才采用的手段;
(vii)在实际使用中,分析GC情况优化代码比优化GC参数要
多得多;
GC优化目标:
GC的时间足够的小
GC的次数足够的少
发生Full GC的周期足够的长
前两个目前是相悖的,要想GC时间小必须要一个更小的
堆,要保证GC次数足够少,必须保证一个更大的堆,我
们只能取其平衡。
最终,GC优化的目的有两个
将转移到老年代的对象数量降低到最小;
减少full GC的执行时间;
GC优化的一般套路:
(i)针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、
最大值,为了防止垃圾收集器在最小、最大之间收缩堆而
产生额外的时间,我们通常把最大、最小设置为相同的值
(ii)年轻代和年老代将根据默认的比例(1:2)分配堆内存,
可以通过调整二者之间的比率NewRadio来调整二者之间
的大小,也可以针对回收代,比如年轻代,通过
-XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,
为了防止年轻代的堆收缩,我们通常会把
-XX:newSize -XX:MaxNewSize设置为同样大小
(iii)年轻代和年老代设置多大才算合理?这个我问题毫无疑问
是没有答案的,否则也就不会有调优。我们观察一下二者
大小变化有哪些影响更大的年轻代必然导致更小的年老代,
大的年轻代会延长普通GC的周期,但会增加每次GC的时
间;小的年老代会导致更频繁的Full GC更小的年轻代必然
导致更大年老代,小的年轻代会导致普通GC很频繁,但每
次的GC时间会更短;大的年老代会减少Full GC的频率如何
选择应该依赖应用程序对象生命周期的分布情况:如果应用
存在大量的临时对象,应该选择更大的年轻代;如果存在相
对较多的持久对象,年老代应该适当增大。但很多应用都没
有这样明显的特性,在抉择时应该根据以下两点(仅包含A
和B):
(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对
象,JVM的默认比例1:2也是这个道理
(B)通过观察应用一段时间,看其他在峰值时年老代会占
多少内存,在不影响Full GC的前提下,根据实际情况
加大年轻代,比如可以把比例控制在1:1。但应该给
年老代至少预留1/3的增长空间
(C)GC优化的一些经验之谈
年轻的大小的选择:
响应时间优先应用
年轻代尽可能的设大,知道接近系统的最低响应时间的
限制(根据实际情况的选择),在此种情况下,年轻代
收集发生的频率也是最小的,同时,减少到达年老代的
对象。
吞吐量优先的应用
年轻代尽可能的设置大,可能达到Gbit的程度。因为对响
应时间没有要求,垃圾收集可以并行进行。避免设置过小,
当新生代设置过小时会导致:
YGC次数更加频繁
可能导致YGC对象直接进入旧生代,如果此时旧生代
满了,会发出FGC。
年老代大小的选择
响应时间优先的应用
年老代使用并发收集器,所以其大小需要小心设置,一般要考
虑并发会话率和会话持续时间等一些参数,如果堆设置小了,
可能会造成内存碎片,高回收频率以及应用暂停而使用传统的
标记清楚方式:如果堆大了,则需要较长的收集时间,最优化
的方案,一般需要参考一下数据获得并发垃圾收集信息,持久
代并发收集次数、传统GC信息、华仔年轻代和年老代回收商
的时间比例。
吞吐量优先的应用
一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年
老代。原因是,这样可以尽可能回收掉大不符短期对象,减少
中期对象,而年老代尽存放长期存活对象。
(iv)在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收
集算法: -XX:+UseParallelOldGC ,默认为Serial收集
(v)线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调
用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K
就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以
产生更多的线程,但这实际上还受限于操作系统。
(vi)可以通过下面的参数打Heap Dump信息
-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamp
-Xloggc:/usr/aaa/dump/heap_trace.txt
通过下面参数可以控制OutOfMemoryError时打印堆的信息
-XX:+HeapDumpOnOutOfMemoryError
(d)程序算法:改进程序逻辑算法提高性能。
七 Java 应用性能调优实践
参考文章:
https://www.ibm.com/developerworks/cn/java/j-lo-performance-tuning-practice/index.html
Java应用性能的瓶颈点非常多,比如磁盘、内存、网络 I/O 等系统因素,
Java应用代码,JVM GC,数据库,缓存等。笔者根据个人经验,将 Java 性
能优化分为4 个层级:应用层、数据库层、框架层、JVM 层,下图所示:
围绕 Java 性能优化,有两种最基本的分析方法:现场分析法和事后分
析法。现场分析法通过保留现场,再采用诊断工具分析定位。现场分析对
线上影响较大,部分场景(特别是涉及到用户关键的在线业务时)不太合
适。事后分析法需要尽可能多收集现场数据,然后立即恢复服务,同时针
对收集的现场数据进行事后分析和复现。下面我们从性能诊断工具出发,
分享搜狗商业平台在其中的一些案例与实践
1. 性能诊断
性能诊断一种是针对已经确定有性能问题的系统和代码进行诊断,
还有一种是对预上线系统提前性能测试,确定性能是否符合上线要求。
本文主要针对前者,后者可以用各种性能压测工具(例如JMeter)进
行测试,不在本文讨论范围内。针对 Java 应用,性能诊断工具主要
分为两层:OS层面和 Java 应用层面(包括应用代码诊断和 GC 诊断)
(1)OS诊断
OS 的诊断主要关注的是 CPU、Memory、I/O 三个方面。
(a)CPU
CPU 主要关注平均负载(Load Average),CPU 使用
率,上下文切换次数(Context Switch)。
平均负载和CPU使用率
通过TOP命令可以查看系统的平均负载和CPU使用率
平均负载(load average)
是指某段时间内占用CPU时间的进程和等待cpu时间
的进程数,这里等待CPU时间的进程是指等待被唤醒
的进程,不包括处于wait状态进程。上图中系统的平
均负载有三个数字:63.66,58.39,57.18,分别表示
过去 1 分钟、5 分钟、15 分钟机器的负载。按照经验,
若数值小于 0.7*CPU 个数,则系统工作正常;若超过
这个值,甚至达到 CPU 核数的四五倍,则系统的负载
就明显偏高。图 2 中 15 分钟负载已经高达 57.18,1
分钟负载是 63.66(系统为 16 核),说明系统出现负
载问题,且存在进一步升高趋势,需要定位具体原因
了。
CPU使用率
参考文章:
https://www.cnblogs.com/shengs/p/5148284.html
https://www.cnblogs.com/gongchixin/articles/7998054.html
- 弄清楚一些概念
用户空间(User space)和内核空间(Kernel space)
虚拟存储空间(内存,原文是“虚拟内存”,但
是我根据其它资料判断这种叫法是不正确的)
被操作系统划分成两块:内核空间和用户空间,
内核空间是内核代码运行的地方(操作系统、
驱动程序),用户空间是用户程序代码运行的
地方(应用程序)。为了安全,它们是隔离的,
即使用户的程序崩溃了,内核也不受影响。当
进程运行在内核空间时就处于内核态,当进程
运行在用户空间时就处于用户态。
Kernel space 可以执行任意命令,调
用系统的一切资源;User space 只能执行简单
的运算,不能直接调用系统资源,必须通过系
统接口(又称 system call),才能向内核发出
指令。
注:一般说来,一个进程在CPU上运行可以有
两种运行模式,既可在用户模式下运行,
又可在内核模式下运行
str = "my string" // 用户空间
x = x + 2
file.write(str) // 切换到内核空间
y = x + 4 // 切换回用户空间
上面代码中,第一行和第二行都是简单的赋
值运算,在 User space 执行。第三行需要写入
文件,就要切换到 Kernel space,因为用户不能
直接写文件,必须通过内核安排。第四行又是赋
值运算,就切换回 User space。
- CPU使用率的各项参数解释
%us:表示用户空间程序的cpu使用率(没有通过nice
调度)计算公式:
(User time + Nice time)/CPU时间*100%
用户使用CPU的进程包括:cpu运行常规用户进
程,cpu运行niced process,cpu运行实时进程。
一个linux进程可以在用户方式下执行,也可以
在系统(内核)方式下执行,当一个进程在内
核代码中运行时,我们称其处于内核态;当一
个进程正在执行用户自己的代码时,我们称其
处于用户态,在用户方式下执行时,进程在它
自己的应用程序代码中执行,不需要内核资源
来进行计算、管理内存或设置变量。
%sy:表示系统空间的cpu使用率,包括内核进程和其他
需要访问内核资源的进程所消耗的cpu资源,但主
要还是内核进程。系统使用cpu的进程包括:用于
系统调用,用于I/O管理(中断和驱动),用于内
存管理(paging and swapping),用于进程管理
(context switch and process start)。如果一个进
程需要内核资源,它必须执行一个系统调用,并
由此切换到系统方式从而使该资源可用。
计算公式:
(System time + Hardirq time +Softirq time)
/
CPU时间*100%
%ni:表示调整过nice值的进程消耗掉的CPU 时间。
ni就是nice,通过nice值可以调整进程的优先级。
如果系统中没有进程被调整过nice值,那么ni就
显示为 0。
计算公式:(Nice time)/CPU时间*100%
%id:系统处于idle(空闲)状态的CPU使用率。
简单的说idle是一个进程,其pid号为 0。其前身
是系统创建的第一个进程,也是唯一一个没有通
过fork()产生的进程。在smp系统中,每个处理
器单元有独立的一个运行队列,而每个运行队列
上又有一个idle进程,即有多少处理器单元,就
有多少idle进程。系统的空闲时间,其实就是指
idle进程的"运行时间"。idle进程pid==o,也就是
init_task。
计算公式:%id=(Idle time)/CPU时间*100%
%wa:cpu运行时在等待io上的CPU使用率,IO主要包括
block I/O,raw I/O,VM-paging/swapins。如果
在wait运行时至少有一个未完成的磁盘I/O,该事
件就归为I/O等待时间,对磁盘的I/O请求会导致
调用的进程阻塞(或睡眠),直到请求完成为止,
一旦进程的I/O请求完成,该进程就放入运行队列
中。如果I/O很快完成,该进程可以使用更多的
cpu时间。
计算公式:(Waiting time)/CPU时间*100%
%hi和s%:这两个值表示系统处理中断消耗的时间。中
断分为硬中断和软中断,hi 表示处理硬中断
消耗的时间,si 表示处理软中断消耗的时间。
硬中断是硬盘、网卡等硬件设备发送给 CPU
的中断消息,当 CPU 收到中断消息后需要进
行适当的处理(消耗 CPU 时间)。软中断是由
程序发出的中断,最终也会执行相应的处理
程序(消耗 CPU 时间)。
硬中断计算公式:
(Hardirq time)/CPU时间*100%
软中断计算公式:
(Softirq time)/CPU时间*100%
%st:被虚拟机偷走的cpu。只有Linux在被当作虚拟机
运行时,这才有意义。另外一种解释:指当前
CPU被强制(involuntary wait )等待另外虚拟的
CPU处理完毕时花费的时间,此时 hypervisor 在
为另一个虚拟处理器服务
计算公式:(Steal time)/CPU时间*100%
扩展,top命令各列的意思
- 如何显示多各CPU的详细信息?
top命令默认情况下显示的是cpu的综合数据,
如果我们要看各个CPU的情况,按数字键1就可
以了。
- top命令如何切换单位显示?
top命令默认显示内存的单位为kb,我们可以通
过按小写字母e切换为mb
- top命令排序
P:以占据CPU百分比排序M:以占据内存百分
比排序。
T:累积占用CPU时间排序首部信息。
- top命令增加列,执行top命令,按f键在按a键。
top命令默认情况下仅显示比较重要的:PID、
USER、PR、NI、VIRT、RES、SHR、S、
%CPU、%MEM、TIME+、COMMAND 列,
按 f 键显示全部列
PID:进程id
PPID:父进程id
RUSER:Real user name
UID:进程所有者的用户id
USER:进程所有者的用户名,简单说就是以
哪个用户权限启动的进程。
GROUP:进程所有者的组名
TTY:启动进程的终端名。不是从终端启动的
进程则显示为 ?
PR:进程优先级。PR 的值是以 Linux 内核
的视角看到的进程执行的优先级。
NI:nice值。负值表示高优先级,正值表示
低优先级。nice是从用户视角看到的进
程执行优先级
P:最后使用的CPU,仅在多CPU环境下有
意义
%CPU:上次更新到现在的CPU时间占用百
分比
TIME:进程使用的CPU时间总计,单位秒
TIME+:进程使用的CPU时间总计,单位
1/100秒
%MEM:进程使用的物理内存百分比
VIRT:进程使用的虚拟内存总量,单位kb。
VIRT=SWAP+RES
SWAP:进程使用的虚拟内存中,被换出的
大小,单位kb。
RES:进程使用的、未被换出的物理内存大
小,单位kb。RES=CODE+DATA
CODE:可执行代码占用的物理内存大小,
单位kb
DATA:可执行代码以外的部分(数据段+栈)
占用的物理内存大小,单位kb
SHR:表示进程使用的共享内存大小,单位
kb
nFLT:页面错误次数
nDRT:最后一次写入到现在,被修改过的
页面数。
S:进程状态。S的值有如下几种:
D=不可中断的睡眠状态
R=运行
S=睡眠
T(TASK_TRACED)=跟踪/停止
Z(EXIT_ZOMBIE)=僵尸进程
COMMAND:命令名/命令行
WCHAN:若该进程在睡眠,则显示睡眠中
的系统函数名
Flags:任务标志,参考 sched.h
上下文切换
- 需要了解的一些概念
上下文切换就是进程切换。也可以理解为线程切换从正在
运行的进程中收回处理器,然后再使待运行进程来占用处
理器。 这里所说的从某个进程收回处理器,实质上就是
把进程存放在处理器的寄存器中的中间数据找个地方存起
来,而把处理器的寄存器腾出来让其他进程使用。现在
linux是大多基于抢占式,CPU给每个任务一定的服务时
间,当时间片轮转的时候,需要把当前状态保存下来,同
时加载下一个任务,这个过程叫做上下文切换。时间片轮
转的方式,使得多个任务利用一个CPU执行成为可能,但
是保存现场和加载现场,也带来了性能消耗。进程(上下
文)切换过高,会导致CPU像个搬运工,频繁在寄存器和
运行队列直接奔波,更多的时间花在了线程切换,而不是
真正工作的线程上。直接的消耗包括CPU寄存器需要保存
和加载,系统调度器的代码需要执行。间接消耗在于多核
cache之间的共享数据。
从一个进程的运行转到另一个进程上运行,这个过程中经
过下面这些变化(看不懂的话就去百度):
* 保存处理机上下文,包括程序计数器和其他寄存器。
* 更新PCB信息
* 把进程的PCB移入相应的队列,如就绪、在某事件阻塞
等队列。
* 选择另一个进程执行,并更新其PCB。
* 更新内存管理的数据结构。
* 恢复处理机上下文。
对于抢占式操作系统,引起上下文切换的原因:
* 当前任务的时间片用完之后,系统CPU正常调度下
一个任务。
* 当前任务碰到IO阻塞,调度线程将挂起此任务,继
续下一个任务。
* 多个任务抢占锁资源,当前任务没有抢到,被调度
器挂起,继续下一个任务。
* 用户代码挂起当前任务,让出CPU时间。
* 硬件中断
监测Linux的应用的时候,当CPU的利用率非常高,但是
系统的性能却上不去的时候,不妨监控一下“线程/进程”
的切换,看看是不是进程切换导致的overhead过高。
- vmstat命令详解
通过vmstat命令我们可以监测上下文切换次数,但是
vmstat命令还有很多功能,我们一并介绍。
vmstat命令是最常见的Linux/Unix监控工具,可以展现
给定时间间隔的服务器的状态值,包括服务器的CPU使用率、
内存使用,上下文切换次数、虚拟内存、交换情况、IO读写
情况。相比top,通过vmstat可以看到整个机器的CPU内存、
IO的使用情况,而不是单单看到各个进程的CPU使用率和内
存使用率。
- 参数介绍
vmstat工具的使用是通过两个数字参数来完成的,第
一个参数是采样的时间间隔数,单位是秒,第二个参
数是采样的次数,如:
root@vm-199:~# vmstat 2 1 procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- r b swpd free buff cache si so bi bo in cs us sy id wa 0 0 97640 53884 192800 578212 0 0 3 20 1 12 1 2 93 3
2表示每隔两秒采集一次服务器状态,1表示只采集一
次。实际上,在应用过程中,我们会在一段时间内一
直监控,不想监控直接结束vmstat就行了,例如:
root@vm-199:~# vmstat 2 procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- r b swpd free buff cache si so bi bo in cs us sy id wa 0 1 97640 50348 192812 578352 0 0 3 20 1 12 1 2 93 3 0 0 97640 54636 192812 578360 0 0 0 14 126 147 2 4 86 8 0 3 97640 53908 192816 578356 0 0 0 62 96 110 2 4 86 9 0 0 97640 54156 192816 578360 0 0 0 14 113 118 3 5 83 11 0 0 97640 53908 192816 578360 0 0 0 16 107 103 2 3 87 9
- 命令介绍:
* Linux 内存监控vmstat命令输出分成六个部分:
(1)进程procs: r:在运行队列中等待的进程数 。 b:等待io的进程数 。 (2)Linux 内存监控内存memoy: swpd:现时可用的交换内存(单位KB)。 free:空闲的内存(单位KB)。 buff: 缓冲区的内存数(单位:KB)。 cache:被用来做为高速缓存的内存数(单位:KB)。 (3) Linux 内存监控swap交换页面 si: 从磁盘交换到内存的交换页数量,单位:KB/秒。 so: 从内存交换到磁盘的交换页数量,单位:KB/秒。 (4)Linux 内存监控 io块设备: bi: 发送到块设备的块数,单位:块/秒。 bo: 从块设备接收到的块数,单位:块/秒。 (5)Linux 内存监控system系统: in: 每秒的中断数,包括时钟中断。 cs: 每秒的环境(上下文)转换次数。 (6)Linux 内存监控cpu中央处理器: cs:用户进程使用的时间 。以百分比表示。 sy:系统进程使用的时间。 以百分比表示。 id:中央处理器的空闲时间 。以百分比表示。
* 每个参数的具体意思:
类别 | 项目 | 含义 | 说明 |
Procs(进程) | r | 表示了正在执行和等待cpu资源的 任务个数,当这个值超过了cpu个数, 就会出现cpu瓶颈。 一般负载超过了3就比较高,超过 了5就高,超过了10就不正常了,服 务器的状态很危险。 这个值也和top的负载有关系,top的负载类似每秒的运行队 列。如果运行队列过大,表示你的CPU很繁忙,一般会造成CPU使用率很高。 | |
b | 等待IO的进程数量,即阻塞进程数。 |
| |
Memory(内存) | swpd | 已使用虚拟的内存大小,单k |
|
free | 空闲内存大小。 我的机器内存总共8G,剩余3415M。 |
| |
buff | 已用的buff大小,对块设备的读写进行缓冲 |
| |
cache | 已用的cache大小,文件系统的cache |
| |
inact | 非活跃内存大小,即被标明可回收的内存,区别于free和active | 具体含义见:概念补充(当使用-a选项时显示) | |
active | 活跃的内存大小 | 具体含义见:概念补充(当使用-a选项时显示) | |
Swap | si | 每秒从交换区写入内存的大小( Swap可以表示虚拟内存,但它只是 说明交换内存来自虚拟内存,它并不 代表整个虚拟内存,Linux中VIRT才是 虚拟内存 ), 单位:kb/s 如果这个值大于0,表示物理内存不够用或者内存泄露了,要查找耗内存进程解决掉。 |
|
so | 每秒从内存写到交换区的大小( Swap可以表示虚拟内存,但它只是 说明交换内存来自虚拟内存,它并不 代表整个虚拟内存,Linux中VIRT才是 虚拟内存 ) 如果这个值大于0,同上。 |
| |
IO | bi | 每秒从块设备读取的块数 注意:磁盘属于块设备,但是还 有其它块设备,所以这个 值一般表示“每秒从磁盘读 取的块数” | 现在的Linux版本块的大小为1024bytes |
bo | 每秒写入块设备的块数 同上,一般表示磁盘 |
| |
system | in | 每秒CPU的中断数,包括时钟中断 | 这两个值越大,会看到由内核消耗的cpu时间会越多,所以这两个值越小越好。 |
cs | 每秒上下文切换数 例如我们调用系统函数,就要进行上下文切换,线程的切换,也要进程上下文切换,这个值要越小越好,太大了,要考虑调低线程或者进程的数目,例如在apache和nginx这种web服务器中,我们一般做性能测试时会进行几千并发甚至几万并发的测试,选择web服务器的进程可以由进程或者线程的峰值一直下调,压测,直到cs到一个比较小的值,这个进程和线程数就是比较合适的值了。系统调用 也是,每次调用系统函数,我们的代码就会进入内核空间,导致上下文切换,这个是很耗资源,也要尽量避免频繁调用系统函数。上下文切换次数过多表示你的CPU大部分浪费在上下文切换,导致CPU干正经事的时间少了,CPU没有充分利用,是不可取的。 | ||
CPU(以百分比表示) | Us | 用户进程执行消耗cpu时间(user time) | us的值比较高时,说明用户进程消耗的cpu时间多,但是如果长期超过50%的使用,那么我们就该考虑优化程序算法或其他措施了 |
Sy | 系统进程消耗cpu时间(system time) | sys的值过高时,说明系统内核消耗的cpu资源多,这个不是良性的表现,我们应该检查原因。 | |
Id | 空闲时间(包括IO等待时间) 一般来说,id + us + sy = 100,一般我认为id是空闲CPU使用率,us是用户CPU使用率,sy是系统CPU使用率。 |
| |
wa | 等待IO时间 | Wa过高时,说明io等待比较严重,这可能是由于磁盘大量随机访问造成的,也有可能是磁盘的带宽出现瓶颈。 |
(b)Memroy
从操作系统角度,内存关注应用进程是否足够,可以使用
free –m 命令查看内存的使用情况。通过 top 命令可以查看进程使
用的虚拟内存 VIRT 和物理内存 RES,根据公式
VIRT = SWAP + RES 可以推算出具体应用使用的交换分区
(Swap)情况,使用交换分区过大会影响 Java 应用性能,可以将
swappiness 值调到尽可能小。因为对于Java 应用来说,占用太多
交换分区可能会影响性能,毕竟磁盘性能比内存慢太多。
free命令
使用free时,要加-h参数,这样会使界面更友好,如下图
如果需要持续观察内存,这时候可以使用-s选项:
上面的命令每隔 3 秒输出一次内存的使用情况。
centos7界面
第一行:
total:内存总大小。
used:使用了多少内存。
free:除了buff/cache剩余了多少内存。
shared:共享内存。
buff/cache:缓冲、缓存区内存数。
available:真实剩余的可用内存数。
相要弄明白available就需要弄明白
free 与 available(available只有
Centos7有,Centos6没有)在
free 命令的输出中,有一个free 列,
同时还有一个 available 列。这二
者到底有何区别?free 是真正尚未
被使用的物理内存数量。至于
available 就比较有意思了,它是从
应用程序的角度看到的可用内存数
量。Linux内核为了提升磁盘操作的
性能,会消耗一部分内存去缓存磁盘
数据,就是我们介绍的buffer 和
cache。所以对于内核来说,buffer
和cache都属于已经被使用的内存。
当应用程序需要内存时,如果没有足
够的 free 内存可以用,内核就会从
buffer 和 cache中回收内存来满足应
用程序的请求。所以从应用程序的角
度来说,
available = free + buffer + cache。
请注意,这只是一个很理想的计算方
式,实际中的数据往往有较大的误差。
关系:total = used + free + buff/cache
关系:available 包含 free 和 buff/cache 剩余
部分,则是真实剩余内存
第二行:交换分区的使用情况。如果交换分区使用完
了,就要注意是否要加内存,或者内存泄漏
了。
centos6界面
第一行:
total:内存总大小。
used:使用了多少内存。
free:剩余了多少内存。
shared:共享内存。
buffers:缓冲区内存数。
cached:缓存区内存数。
关系:total = used + free
第二行:
(-buffers/cache) used内存数:1.4G (指的
第一部分Mem行中的
used – buffers – cached)
(+buffers/cache) free内存数:2.3G (指的
第一部分Mem行中的
free + buffers + cached)
第三行:交换分区的使用情况。如果交换分区使用
完了,就要注意是否要加内存,或者内存
泄漏了。
扩展:buffers/cached
先来提一个问题: buffer 和 cache 应该
是两种类型的内存,但是 free 命令为什么会
把它们放在一起呢?要回答这个问题需要我
们做些准备工作。让我们先来搞清楚 buffer
与 cache 的含义。
buffer 在操作系统中指 buffer cache,
中文一般翻译为 "缓冲区"。要理解缓冲区,必
须明确另外两个概念:"扇区" 和 "块"。扇区是
设备的最小寻址单元,也叫 "硬扇区" 或"设备
块"。块是操作系统中文件系统的最小寻址单元,
也叫 "文件块" 或 "I/O 块"。每个块包含一个或
多个扇区,但大小不能超过一个页面,所以一
个页可以容纳一个或多个内存中的块。当一个
块被调入内存时,它要存储在一个缓冲区中。
每个缓冲区与一个块对应,它相当于是磁盘块
在内存中的表示:
注意,buffer cache 只有块的概念而没有文件的
概念,它只是把磁盘上的块直接搬到内存中而不
关心块中究竟存放的是什么格式的文件。
cache 在操作系统中指 page cache,中文
一般翻译为 "页高速缓存"。页高速缓存是内核实
现的磁盘缓存。它主要用来减少对磁盘的 I/O 操
作。具体地讲,是通过把磁盘中的数据缓存到物
理内存中,把对磁盘的访问变为对物理内存的访
问。页高速缓存缓存的是内存页面。缓存中的页
来自对普通文件、块设备文件(这个指的就是
buffer cache 呀)和内存映射文件的读写。
页高速缓存对普通文件的缓存我们可以这样
理解:当内核要读一个文件(比如 /etc/hosts)时,
它会先检查这个文件的数据是不是已经在页高速
缓存中了。如果在,就放弃访问磁盘,直接从内
存中读取。这个行为称为缓存命中。如果数据不
在缓存中,就是未命中缓存,此时内核就要调度
块 I/O 操作从磁盘去读取数据。然后内核将读来
的数据放入页高速缓存中。这种缓存的目标是文
件系统可以识别的文件(比如 /etc/hosts)。页高
速缓存对块设备文件的缓存就是我们在前面介绍
的 buffer cahce。因为独立的磁盘块通过缓冲区
也被存入了页高速缓存(缓冲区最终是由页高速
缓存来承载的)。
到这里我们应该搞清楚了:无论是缓冲区还
是页高速缓存,它们的实现方式都是一样的。缓
冲区只不过是一种概念上比较特殊的页高速缓存
罢了。
那么为什么 free 命令不直接称为 cache 而
非要写成 buff/cache? 这是因为缓冲区和页高速
缓存的实现并非天生就是统一的。在 linux 内核
2.4 中才将它们统一。更早的内核中有两个独立
的磁盘缓存:页高速缓存和缓冲区高速缓存。前
者缓存页面,后者缓存缓冲区。当你知道了这些
故事之后,输出中列的名称可能已经不再重要
了。
交换空间(swap space) swap space 是磁
盘上的一块区域,可以是一个分区,也可以是
一个文件。所以具体的实现可以是 swap 分区
也可以是 swap 文件。当系统物理内存吃紧时,
Linux 会将内存中不常访问的数据保存到 swap
上,这样系统就有更多的物理内存为各个进程
服务,而当系统需要访问 swap 上存储的内容
时,再将 swap 上的数据加载到内存中,这就
是常说的换出和换入。交换空间可以在一定程
度上缓解内存不足的情况,但是它需要读写磁
盘数据,所以性能不是很高。
/proc/meminfo 文件
其实 free 命令中的信息都来自于
/proc/meminfo 文件。/proc/meminfo 文件包
含了更多更原始的信息,只是看起来不太直观:
centos6界面
(c)I/O
I/O包括磁盘I/O和网络 I/O,一般情况下磁盘更容易出现 I/O
瓶颈。通过iostat可以查看磁盘的读写情况,通过 CPU 的
I/O wait 可以看出磁盘 I/O 是否正常。如果磁盘 I/O 一直处于很
高的状态,说明磁盘太慢或故障,成为了性能瓶颈,需要进行应
用优化或者磁盘更换。
(i)不加选项执行iostat
单独执行iostat,显示的结果为从系统开机到当前执行时
刻的统计信息。
[patrickxu@vm1 ~]$ iostat Linux 2.6.32-279.19.3.el6.ucloud.x86_64 (vm1) 06/11/2017 _x86_64_ (8 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 0.08 0.00 0.06 0.00 0.00 99.86 Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn vda 0.45 0.29 8.10 6634946 183036680 vdb 0.12 3.11 30.55 70342034 689955328
整体分为3部分
第一部分:"Linux 2.6.32-279.19.3.el6.ucloud.x86_64 (vm1)
06/11/2017 _x86_64_ (8 CPU)" 指示系统版本、
主机名和当前日期
第二部分:avg-cpu处,总体cpu使用情况统计信息,对于多
核cpu,这里为所有cpu的平均值
第三部分:Device:各磁盘设备的IO统计信息
avg-cpu处各列的意思:
%user CPU在用户态执行进程的时间百分比。
%nice CPU在用户态模式下,用于nice操作,所占
用CPU总时间的百分比
%system CPU处在内核态执行进程的时间百分比
%iowait CPU用于等待I/O操作占用CPU总时间的
百分比
%steal 管理程序(hypervisor)为另一个虚拟进程提
供服务而等待虚拟CPU的百分比
%idle CPU空闲时间百分比
Device处各列的意思
tps:每秒向磁盘设备请求数据的次数,包括读、写请求,
为rtps与wtps的和。出于效率考虑,每一次IO下发后
并不是立即处理请求,而是将请求合并(merge),这
里tps指请求合并后的请求计数。
Blk_read/s:每秒从设备读取的数据量,单位是块(扇区
块相当于内核为2.4及更高版本的扇区,因
此大小为512字节。对于较旧的内核,块的
大小不确定)。
Blk_wrtn/s:每秒写入设备的数据量,单位是块(扇区)。
- 若 %iowait 的值过高,表示硬盘存在I/O瓶颈
- 若 %idle 的值高但系统响应慢时,有可能是CPU等待分配
内存,此时应加大内存容量
- 若 %idle 的值持续低于1,则系统的CPU处理能力相对较
低,表明系统中最需要解决的资源是 CPU
Blk_read:取样时间间隔内读扇区总数量
Blk_wrtn:取样时间间隔内写扇区总数量
(ii)指定采样时间间隔与采样次数
我们可以以”iostat interval [count] ”形式指定iostat命
令的采样间隔和采样次数:
[patrickxu@vm1 ~]$ iostat -d 2 3 Linux 2.6.32-279.19.3.el6.ucloud.x86_64 (vm1) 06/12/2017 _x86_64_ (8 CPU) Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn vda 0.45 0.29 8.10 6634946 183051408 vdb 0.12 3.11 30.55 70342034 689955328 Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn vda 0.00 0.00 0.00 0 0 vdb 0.00 0.00 0.00 0 0 Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn vda 1.50 0.00 12.00 0 24 vdb 0.00 0.00 0.00 0 0
以上命令输出Device的信息,采样时间为1秒,采样
2次,若不指定采样次数,则iostat会一直输出采样信息。
(iii)以kB为单位显示读写信息(-k选项)/以mB为单位显示读写
信息(-m选项)
我们可以使用-k选项,指定iostat的部分输出结果以
KB为单位,而不是以扇区数为单位:
[patrickxu@vm1 ~]$ iostat -d -k Linux 2.6.32-279.19.3.el6.ucloud.x86_64 (vm1) 06/12/2017 _x86_64_ (8 CPU) Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn vda 0.45 0.15 4.05 3317473 91525980 vdb 0.12 1.56 15.27 35171017 344977664
以上输出中,kB_read/s、kB_wrtn/s、kB_read和
kB_wrtn的值均以kB为单位,相比以扇区数为单位,这
里的值为原值的一半(1kB=512bytes*2)
(iv)更详细的io统计信息(-x选项)
linux # iostat -x -k -d 1 Linux 2.6.16.60-0.21-smp (linux) 06/13/12 …… Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util sda 0.00 9915.00 1.00 90.00 4.00 34360.00 755.25 11.79 120.57 6.33 57.60
以上各列的含义如下:
rrqm/s:每秒对该设备的读请求被合并次数,文件
系统会对读取同块(block)的请求进行合并
wrqm/s:每秒对该设备的写请求被合并次数
r/s:每秒完成的读次数
w/s:每秒完成的写次数
rkB/s:每秒读数据量(kB为单位)
wkB/s:每秒写数据量(kB为单位)
avgrq-sz:平均每次IO操作的数据量(扇区数为单
位)
avgqu-sz:平均等待处理的IO请求队列长度
await:平均每次IO请求等待时间(包括等待时间和
处理时间,毫秒为单位)
svctm:平均每次IO请求的处理时间(毫秒为单位)
%util 采用周期内用于IO操作的时间比率,即IO
队列非空的时间比率
对于以上示例输出,我们可以获取到以下信
对于以上示例输出,我们可以获取到以下信息:
- 每秒向磁盘上写30M左右数据(wkB/s值)
- 每秒有91次IO操作(r/s+w/s),其中以写操作为主
体
- 平均每次IO请求等待时间为120.57毫秒,处理时
间为6.33毫秒
- 等待处理的IO请求队列中,平均有11.79个请求
驻留
(v)实际查看时,一般结合着多个选项查看: 如
iostat -dxm 3
[root@yg-uhost724 ~]# iostat -dxm 5 Linux 2.6.32-279.19.16.el6.ucloud.x86_64 (yg-uhost724) 06/12/2017 _x86_64_ (24 CPU) Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await svctm %util sda 0.69 29.89 1.36 21.52 0.05 0.20 23.04 0.04 1.96 0.29 0.67 sdb 682.88 1811.86 77.94 417.73 2.97 8.71 48.27 0.01 0.03 0.10 4.89 dm-14 0.00 0.00 0.03 41.47 0.00 0.16 8.00 0.01 0.12 0.02 0.10 dm-0 0.00 0.00 0.00 0.00 0.00 0.00 8.00 0.00 7.24 0.07 0.00 dm-2 0.00 0.00 0.00 0.00 0.00 0.00 8.00 0.00 0.07 0.04 0.00 dm-3 0.00 0.00 0.00 0.98 0.00 0.00 8.00 0.00 3.57 0.22 0.02 dm-5 0.00 0.00 0.18 50.51 0.00 0.20 8.00 0.03 0.44 0.01 0.07 dm-1 0.00 0.00 1.50 5.51 0.01 0.02 8.00 0.03 4.04 0.04 0.03 dm-6 0.00 0.00 123.23 1042.56 0.48 4.07 8.00 0.01 0.01 0.02 1.90 dm-4 0.00 0.00 635.74 1069.59 2.48 4.18 8.00 0.00 0.04 0.01 2.39 sdc 7.23 106.32 0.52 0.94 0.03 0.42 627.54 0.02 13.32 17.58 2.58 Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await svctm %util sda 0.00 15.00 0.40 17.80 0.00 0.10 10.90 0.00 0.10 0.10 0.18 sdb 82.60 11967.00 414.60 2500.20 1.94 56.51 41.07 12.07 4.14 0.07 21.02 dm-14 0.00 0.00 0.00 0.20 0.00 0.00 8.00 0.00 0.00 0.00 0.00 dm-0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 dm-2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 dm-3 0.00 0.00 0.00 1.80 0.00 0.01 8.00 0.00 0.11 0.11 0.02 dm-5 0.00 0.00 0.00 2.00 0.00 0.01 8.00 0.00 0.00 0.00 0.00 dm-1 0.00 0.00 0.00 2.40 0.00 0.01 8.00 0.01 4.17 1.25 0.30 dm-6 0.00 0.00 0.00 45.20 0.00 0.18 8.00 0.11 2.45 0.55 2.48 dm-4 0.00 0.00 497.20 14415.80 1.94 56.31 8.00 72.87 4.83 0.01 19.14 sdc 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
(a)jstack
jstack 命令通常配合 top 使用,通过 top -H -p pid 定位 Java 进程和线程,再利用
jstack -l pid 导出线程栈。由于线程栈是瞬态的,因此需要多次dump,一般3次dump,
一般每次隔5s就行。将top定位的Java线程pid 转成 16 进制,得到Java线程栈中的nid,
可以找到对应的问题线程栈。
下例通过 top –H -p 查看运行时间较长 Java 线程
其中的线程 24985 运行时间较长,可能存在问题,转成 16 进制后,通过 Java 线程栈
找到对应线程 0x6199 的栈如下,从而定位问题点,如下图所示。
(b)JProfiler
(c)jstat
用于对GC进行诊断,可打印 GC 详细信息,Young GC 和 Full GC 次数,堆信息等其
命令格式为:jstat –gcxxx -t pid <interval> <count>,如下图所示:
(d)jmap
jmap打印Java进程堆信息jmap –heap pid。通过 jmap –dump:file=xxx pid可dump堆
到文件,然后通过其它工具进一步分析其堆使用情况。
(3)一些调优案例
https://blog.csdn.net/u010862794/article/details/78020231
8.java虚拟机的类加载机制
推荐阅读文章:http://blog.itpub.net/31561269/viewspace-2222522/
类重被加载到虚拟机内存中开始,到拆卸出内存为止,它的整个生命周期包括:加载、验证
、准备、解析、初始化、使用、拆卸7个阶段。
(1)加载
(a)通过类的全限定名来获取此类的二进制字节流
(b)将这个类的静态存储结构转换为方法去的运行时数据结构
(c)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区的这个类的各
种数据的访问入口。
(2)验证
这一阶段的目的是保证Class字节流中包含的信息复合虚拟机的要求,并且不会
危害虚拟机的自身安全。
(a)文件格式验证
(b)元数据验证
(c)字节码验证
(3)准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量使用
的内存都将在方法区中进行分配。
这个阶段有两个容易产生混淆的概念需要强调一下,首先这个时候进行内存分
配只包括类变量(被static修饰的变量)而不包括实例变量,实例变量将会随着对象
被实例化时随着对象一起被分配在java堆中。其次,这个时候所说的初始值通常时
数据类型的零值,假设一个对象的类变量定义为:
public static int value=123;
那变量在准备阶段的初始值为0而不是123,因为这时候尚位执行任何java方法,
而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()
方法之中,所以把value赋值为123的动作将在初始化的时候才会执行。
(4)解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
(5)初始化
初始化阶段是才真正开始执行类中定义的程序代码(或者说是字节码)的
阶段。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段则
根据程序员通过程序制定的主观计划去初始化类变量和其它资源,或者重另外
一个角度来讲,初始化阶段是执行类构造器<clinit>方法的过程。
9. 类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这
个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个
动作的代码模块称为”类加载“
先了解一下Java类有几种形态
-- 源码(静态)
-- 字节码(静态)
-- 运行态(动态)
由源码到字节码靠javac;由字节码到运行态靠classloader
三种类加载器
-- 启动类加载器(Bootstrap ClassLoader):
这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的
路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib
目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用
户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即
可,如代码清单7-9所示为java.lang.ClassLoader.getClassLoader()方法的代码片段
-- 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher $ExtClassLoader
实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路
径中的所有类库,开发者可以直接使用扩展类加载器。
-- 应用程序类加载器(Application ClassLoader):这个类加载器由
sun.misc.Launcher $App-ClassLoader实现。由于这个类加载器是ClassLoader中的
getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户
类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没
有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己
定义的类加载器。这些类加载器之间的关系一般如下图所示。
上图中展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents
Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有
自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,
而是都使用组合(Composition)关系来复用父加载器的代码。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的
类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,
无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,
因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由
各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的
ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保
证,应用程序也将会变得一片混乱。如果读者有兴趣的话,可以尝试去编写一个与rt.jar类库中已有
类重名的Java类,将会发现可以正常编译,但永远无法被加载运行[2]。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试
加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所
有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加
载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
源码分析记录:
1. 使得同一时刻只允许一个类可以被加载(synchronized锁住整个代码)。
2. 没有被加载过的才会进行加载(会判断类是否被加载过)
3.父类能加载的绝对不交给字类加载(递归)
双亲委任模型就是出于安全考虑,父类能加载的就不给子类加载,比如我们写了一个java.util.List,
是不会被加载的,因为父类已经加载了rt.jar下的java.util.List
源码如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
双亲委派模型
器”
StackOverflowError