根据阅读 《深入理解java虚拟机》 加上一点自己的理解。
JVM运行时数据区域
- 程序计数器:
程序计数器是一个较小的内存空间,他可以看成是当前线程所执行的字节码的行号指示器。
每条线程都会有自己独自的程序计数器,这样,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 - java虚拟机栈
java虚拟机栈也是线程私有的。
虚拟机栈描述的是java方法执行的内存模型:每个方法在执行时都会创建一个栈帧,用于储存局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用到执行完成,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
在JVM规范中,对这个区域规定了两种异常状况:
1:如果线程请求的栈深度大于虚拟机运行的深度,将抛出StackOverflowError异常。
2:如果虚拟机的栈可以动态扩展,但是扩展也无法申请足够大的内存,则抛出OutOfMemoryError异常 - 本地方法栈
本地方法栈与java虚拟机栈锁发挥的作用是非常相似的。
java虚拟机栈执行java方法。
本地方法栈则为虚拟机使用的Native方法服务。(native method 跨语言使用)
有的虚拟机 会把java虚拟机栈和本地方法栈合二为一。 - 堆
对于绝大部分应用来说,java堆(heap)是java虚拟机所管理的内存中最大的一块。
java堆是被所有线程共享的一块内存区域。此内存区域的唯一目的就是存放内存实例。
java堆是垃圾回收管理的主要区域。 - 方法区(永久代)
方法区与java堆一样,是各个线程共享的内存区域。
它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译的代码等数据。
在HotSpot虚拟机开发者来说,虚幻把方法区称为永久代,原因是设计团队把GC分带收集扩展至方法区。 - 运行时常量池(方法区一部分)
常量池用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。 - 直接内存
直接内存并不是JVM运行时数据区域。也不是java虚拟机规范中定义的内存区域,但是这部分也频繁使用。
JDK1.4中新增加了NIO ,它可以使用Native函数库直接分配堆外内存,然后通过存储在java堆中的DirecByte
Buffer对象作为这块内存的引用直接操作,这样可以提高性能。
对象
对象的内存布局
对象在内存中可以分为3块区域
1,对象头(Header):用于存储对象自身运行时数据,如哈希码,GC分带年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
2,实例数据(Instance Data):这部分数据是真正有效的信息。也是程序代码中所定义的各种类型字段内容。
无论是父类中继承下来的,还是子类中定义的,都要记录下来。
3,对齐填充(Pdding):这部分并不是必然存在的。也没有特别的含义,它仅仅起着占位符的作用,由于HotSpotVM的自动内存管理要求对象起始地址必须是8字节的整数倍对象的创建
new对象 >> 是否能在常量池中定位到一个类的符号引用并且检查这个符号引用代表的类是否已经被加载,解析和初始化 >> 如果没有,那必须执行类加载过程 >> 类加载检查通过以后,虚拟机给新生对象分配内存对象的访问定位
java栈中存的只是引用,访问堆中对象的具体位置需要取决于虚拟机实现,目前主流的访问方式有两种。
1:句柄访问
2:直接指针访问(HotSpot使用的是指针访问)
垃圾收集器与内存分配策略
说到GC,可以从三个地方来地方来思考。哪些内存需要回收?什么时候回收?如何回收?
- 哪些内存需要回收
可以说再也不被引用的,已经死亡的对象会成为回收的对象
HotSpot使用的不是引用计数算法,所以当java中对象循环引用,也会被GC回收。
看下边
public class ReferenceObj {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
}
/**
* 在运行这个单元测试的时候 添加参数 -XX:+PrintGCDetails 可以查看GC日志
* 可以看到这样也是被回收的。
*/
@Test
public void testGC(){
ReferenceObj objA = new ReferenceObj();
ReferenceObj objB = new ReferenceObj();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
HotSpot使用的是可达性分析算法:
这个算法的基本思想是通过一些“GC Roots”的对象做为起点,从这些节点开始向下搜索,搜索所过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的。
GC Roots的对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,native方法引用的对象
在JDK1.2之后,java 对引用的概念进行了扩充,将引用分为强引用,软引用,弱引用,虚引用 四种
软引用、弱引用和虚引用处理
四种引用类型的概念
这两个链接对四种引用类型讲解的非常好。
-
如何回收-垃圾回收算法
这个算法有两个不足,一是效率问题,标记和清除的效率都不是很高。另一个是空间问题,标记清除之后会产生大量不连续的内存随便,空间碎片太多会导致以后在程序运行时存放比较大的对象时找不到足够连续的内存而又进行垃圾回收。
1,标记-清除(Mark-Sweep)算法:首先标记出来所有需要回收的对象,然后统一回收被标记的对象。
2,复制(Copying)算法:它讲可用内存按容量分为大小相等的两块,每次使用其中的一块,当这一块的内存用完了。就将还存活的对象复制到另一块上边,然后把已使用全部清除掉。这样每次都是对半个区域进行回收,不用考虑内存碎片的复杂情况。
非常值得一说的是,新生代的对象98%是朝生夕死,所以复制算法并不需要1:1的比例来划分空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一个Survivor。当回收时,将Eden和Survivor还存活的对象一次性复制到另一块Survivor空间上。最后清理掉刚才用过的Eden和Survivor。HotSpot VM默认Eden和Survivor的大小比例是8:1。也就是说每次只有10%的空间被浪费。但是我们不能保证每次回收都有不多于10%的对象存活。当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)
上边说完了对象存活判定的算法和垃圾收集的算法。HotSpotVM实现这些算法时必须需对算法的执行效率有严格的考量,才能保证虚拟机的高效运行。
4,枚举根节点
在运行这些算法的时候必须在一个能确保一致性的快照中进行。
也就是GC的时候必须停顿所有java执行的线程,(SUN将这件事情成为“Stop The Word”)
详细73页。
-
垃圾收集器
上面有7种收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。
1,Serial(单线程)收集器:Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,它不仅只会使用一个CPU或者一条收集线程去完成垃圾收集作,而且必须暂停其他所有的工作线程(用户线程),直到它收集完成。
Serial收集器是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,简单高效,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率,因此是运行在Client模式下的虚拟机的不错选择(比如桌面应用场景)。2,ParNew(多线程)收集器
ParNew收集器其实就是serial收集器的多线程版本,使用复制算法。除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。
ParNew收集器是运行在Service模式下虚拟机中首选的新生代收集器,其中一个与性能无关的原因就是除了Serial收集器外,目前只有ParNew收集器能与CMS收集器配合工作。
PreNew收集器在单CPU环境中绝对没有Serial的效果好,由于存在线程交互的开销,该收集器在超线程技术实现的双CPU中都不能一定超过Serial收集器。默认开启的垃圾收集器线程数就是CPU数量,可通过-XX:parallelGCThreads参数来限制收集器线程数
在使用-XX:+useConcMarkSweepGC参数后 默认的就是ParNew收集器。
也可以使用-XX:+UseParNewGC来强制选择。3,Parallel Scavenge(多线程回收GC)收集器
Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
短停顿时间适合和用户交互的程序,体验好。高吞吐量适合高效利用CPU,主要用于后台运算不需要太多交互。
Parallel Scavenge收集器提供了两个参数来精确控制吞吐量:1.最大垃圾收集器停顿时间(-XX: MaxGCPauseMillis 大于0的毫秒数,停顿时间小了就要牺牲相应的吞吐量和新生代空间),2.设置吞吐量大小(-XX:GCTimeRatio 大于0小于100的整数,默认99,也就是允许最大1%的垃圾回收时间)。
还有一个参数表示自适应调节策略(GC Ergonomics)(-XX:UseAdaptiveSizePolicy)。就不用手动设置新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)今生老年代对象大小(-XX:PretenureSizeThreshold),会根据当前系统的运行情况手机监控信息,动态调整停顿时间和吞吐量大小。也是其与PreNew收集器的一个重要区别,也是其无法与CMS收集器搭配使用的原因(CMS收集器尽可能地缩短垃圾收集时用户线程的停顿时间,以提升交互体验)。4,Serial Old(单线程GC)收集器
Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。
如果在Service模式下使用:1.一种是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,因为那时还没有Parallel Old老年代收集器搭配;2.另一种就是作为CMS收集器的后备预案,在并发收集发生Concurrent Model Failure时使用。5,Serial Old(多线程GC)收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,JDK1.6才提供。
由于之前有一个Parallel Scavenge新生代收集器,,但是却无老年代收集器与之完美结合,只能采用Serial Old老年代收集器,但是由于Serial Old收集器在服务端应用性能上低下(毕竟单线程,多CPU浪费了),其吞吐量反而不一定有PreNew+CMS组合。
6,CMS(并发GC)收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是HotSpot虚拟机中的一款真正意义上的并发收集器,第一次实现了让垃圾回收线程和用户线程(基本上)同时工作。用CMS收集老年代的时候,新生代只能选择Serial或者ParNew收集器。
CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:
初始标记-并发标记-重新标记-并发清除
CMS优点:并发收集,低停顿
CMS缺点:在并发收集的时候回使程序变慢,无法处理浮动垃圾,因为使用标记-清除算法会出现空间碎片
详细83页。
7,G1收集器
G1(Garbage First)收集器是JDK1.7提供的一个新的面向服务端应用的垃圾收集器,其目标就是替换掉JDK1.5发布的CMS收集器。其优点有:
1.并发与并行:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短停顿(Stop The World)时间。
2.分代收集:G1不需要与其他收集器配合就能独立管理整个GC堆,但他能够采用不同方式去处理新建对象和已经存活了一段时间、熬过多次GC的老年代对象以获取更好收集效果。
3.空间整合:从整体来看是基于“标记-整理”算法实现,从局部(两个Region之间)来看是基于“复制”算法实现的,但是都意味着G1运行期间不会产生内存碎片空间,更健康,遇到大对象时,不会因为没有连续空间而进行下一次GC,甚至一次Full GC。
4.可预测的停顿:降低停顿是G1和CMS共同关注点,但G1除了追求低停顿,还能建立可预测的停顿模型,可以明确地指定在一个长度为M的时间片内,消耗在垃圾收集的时间不超过N毫秒
5.跨代特性:之前的收集器进行收集的范围都是整个新生代或老年代,而G1扩展到整个Java堆(包括新生代,老年代)。
那么是怎么实现的呢?
1.如何实现新生代和老年代全范围收集:其实它的Java堆布局就不同于其余收集器,它将整个Java堆划分为多个大小相等的独立区域(Region),仍然保留新生代和老年代的概念,可是不是物理隔离的,都是一部分Region(不需要连续)的集合。
2.如何建立可预测的停顿时间模型:是因为有了独立区域Region的存在,就避免在Java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收可以获得的空间大小和回收所需要的时间的经验值),后台维护一个优先队列,根据每次允许的收集时间,优先回收价值最大的Region(Garbage-First理念)。因此使用Region划分内存空间以及有优先级的区域回收方式,保证了有限时间获得尽可能高的收集效率。
3.如何保证垃圾回收真的在Region区域进行而不会扩散到全局:由于Region并不是孤立的,一个Region的对象可以被整个Java堆的任意其余Region的对象所引用,在做可达性判定确定对象是否存活时,仍然会关联到Java堆的任意对象,G1中这种情况特别明显。而以前在别的分代收集里面,新生代规模要比老年代小许多,新生代收集也频繁得多,也会涉及到扫描新生代时也会扫描老年代的情况,相反亦然。解决:G1收集器Region之间的对象引用以及新生代和老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(分代的例子中就检查是否老年代对象引用了新生代的对象),如果是则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中,当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可避免全堆扫描。
忽略Remembered Set的维护,G1的运行步骤可简单描述为:
①.初始标记(Initial Marking)
②.并发标记(Concurrenr Marking)
③.最终标记(Final Marking)
④.筛选回收(Live Data Counting And Evacution)
1.初始标记:初始标记仅仅标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新的对象。这阶段需要停顿线程,不可并行执行,但是时间很短。
2.并发标记:此阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,此阶段时间较长可与用户程序并发执行。
3.最终标记:此阶段是为了修正在并发标记期间因为用户线程继续运行而导致标记产生变动的那一份标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这段时间需要停顿线程,但是可并行执行。
4.筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。
如果现有的垃圾收集器没有出现任何问题,没有任何理由去选择G1,如果应用追求低停顿,G1可选择,如果追求吞吐量,和Parallel Scavenge/Parallel Old组合相比G1并没有特别的优势。 -
看GC日志
GC 日志开头的 [GC 和 [FullGC 说明了这次垃圾收集的停顿类型,如果有FullGC 说明这次是产生了Stop The Word的。(FullGC 一般是因为担保失败之类的问题或者调用了System.gc())
日志中[PSYoungGen 指的是 用的Parallel Scavenge 收集器,收集的 新生代(包括伊甸园区和幸存区)
以第一行GC日志为例:
512K->456K(1024K) 表示的是 GC前该内存区域中已使用容量->GC后该内存区域已使用容量(该内存区域总容量)
512K->456K(130560K) 表示的是 GC前该java堆中已使用容量->GC后该java堆已使用容量(该java堆总容量)
0.0252655 secs 是这次GC的总时间
Times: user=0.00 sys=0.00, real=0.03 secs 指的是 和linux中time命令输出的一致
分别是用户态消耗的时间,内核态消耗的时间,操作从开始到结束的墙钟时间 内存分配与回收策略
1,对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发生一次MinorGC(发生在新生代的GC,因为java对象大多都具备快速死亡的特点,所以MinorGC非常频繁,一般回收速度也比较快)
下边代码 分配3个2M的对象和一个4M的对象,当创建3个2M对象后,第四个4M对象无法放入新生代,所以进行了一次MinorGC,这次GC是Eden+survivor1区拷贝到survivor2区,因为survivor2区的大小只有1M,所以通过分配担保(JDK1.6后默认开启)把6M的对象分配给了老年代,第四个4M对象又存进了 新生代。
private static final int _1M = 1024*1024;
/**
*JVM参数 :-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* @param args
*/
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1 = new byte[2 * _1M];
allocation2 = new byte[2 * _1M];
allocation3 = new byte[2 * _1M];
System.out.println("在这里进行了GC");
allocation4 = new byte[4 * _1M];
}
2,大对象直接存入老年代
如果有大对象存入新生代。就会导致在Eden区及两个Survivor区发生大量的复制,甚至通过内存担保复制到老年代,所以虚拟机提供了一个-XX:pretenureSizeThreshold 参数,令大于这个值的对象直接存放到老年代。如果有大量的短命大对象,是很影响JVM效率的。
3,长期存活的对象将进入老年代
通过上边两点,知道担保分配和大对象进入老年代,还有一种情况会进入老年代。
虚拟机给每个对象定义了一个年龄(Age)计数器。如果对象在Eden出生并经过一次MinorGC后仍然存活,并且能被Survivor容纳的话,则增加一岁,当她的年龄达到了15岁(或者通过-XX:MaxTenuringThreashold 设置)他就会晋升到老年代。
4,动态对象年龄判定
为了能更好的适应不同程序的内存情况,如果Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或者等于该年龄的对象也可以进入老年代,无需等到MaxTenuringThreshold 中要求的年龄.
虚拟机性能监控与故障处理工具
jps :虚拟机进程状况工具
他的功能和linux上的ps类似,可以列出正在运行的虚拟进程,并显示虚拟机执行主类以及这些线程本地虚拟机的唯一IP。
命令格式:jps [options ] [ hostid ]
[options]选项 :
-q:仅输出VM标识符,不包括classname,jar name,arguments in main method
-m:输出main method的参数
-l:输出完全的包名,应用主类名,jar的完全路径名
-v:输出jvm参数jstat:虚拟机统计信息监控工具
这个命令主要是对GC 的统计。
命令格式 jstat [option vmid [inrterval[s|ms] [count] ]]
如 jstat -gc 15421 1000 10
查看进程为15421的垃圾收集情况,1秒打印一次 打印10次
-class :类加载器
-compiler : JIT)
-gc :GC堆状态
-gccapacity : 各区大小
-gccause : 最近一次GC统计和原因
-gcnew : 新区统计
-gcnewcapacity : 新区大小
-gcold : 老区统计
-gcoldcapacity : 老区大小
-gcpermcapacity : 永久区大小
-gcutil : GC统计汇总
-printcompilation : HotSpot编译统计
jstat命令查看jvm的GC情况jinfo:java配置信息工具
jinfo的作用是实时查看或修改JVM参数
命令格式 jinfo [ option ] pid
no option : 输出全部的参数和系统属性
-flag name : 输出对应名称的参数
-flag [+|-]name : 开启或者关闭对应名称的参数
-flag name=value : 设定对应名称的参数
-flags : 输出全部的参数
-sysprops : 输出系统属性jmap:java内存映像工具
命令格式 jmap [ option ] pid
-finalizerinfo:打印正等候回收的对象的信息
Attaching to process ID 12143, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 20.0-b11
Number of objects pending for finalization: 0 (等候回收的对象为0个)
-heap :打印heap的概要信息,GC使用的算法,heap的配置及wise heap的使用情况.
using parallel threads in the new generation. ##新生代采用的是并行线程处理方式
using thread-local object allocation.
Concurrent Mark-Sweep GC ##同步并行垃圾回收
Heap Configuration: ##堆配置情况
MinHeapFreeRatio = 40 ##最小堆使用比例
MaxHeapFreeRatio = 70 ##最大堆可用比例
MaxHeapSize = 2147483648 (2048.0MB) ##最大堆空间大小
NewSize = 268435456 (256.0MB) ##新生代分配大小
MaxNewSize = 268435456 (256.0MB) ##最大可新生代分配大小
OldSize = 5439488 (5.1875MB) ##老生代大小
NewRatio = 2 ##新生代比例
SurvivorRatio = 8 ##新生代与suvivor的比例
PermSize = 134217728 (128.0MB) ##perm区大小
MaxPermSize = 134217728 (128.0MB) ##最大可分配perm区大小
Heap Usage: ##堆使用情况
New Generation (Eden + 1 Survivor Space): ##新生代(伊甸区 + survior空间)
capacity = 241631232 (230.4375MB) ##伊甸区容量
used = 77776272 (74.17323303222656MB) ##已经使用大小
free = 163854960 (156.26426696777344MB) ##剩余容量
32.188004570534986% used ##使用比例
Eden Space: ##伊甸区
capacity = 214827008 (204.875MB) ##伊甸区容量
used = 74442288 (70.99369812011719MB) ##伊甸区使用
free = 140384720 (133.8813018798828MB) ##伊甸区当前剩余容量
34.65220164496263% used ##伊甸区使用情况
From Space: ##survior1区
capacity = 26804224 (25.5625MB) ##survior1区容量
used = 3333984 (3.179534912109375MB) ##surviror1区已使用情况
free = 23470240 (22.382965087890625MB) ##surviror1区剩余容量
12.43827838477995% used ##survior1区使用比例
To Space: ##survior2 区
capacity = 26804224 (25.5625MB) ##survior2区容量
used = 0 (0.0MB) ##survior2区已使用情况
free = 26804224 (25.5625MB) ##survior2区剩余容量
0.0% used ## survior2区使用比例
concurrent mark-sweep generation: ##老生代使用情况
capacity = 1879048192 (1792.0MB) ##老生代容量
used = 30847928 (29.41887664794922MB) ##老生代已使用容量
free = 1848200264 (1762.5811233520508MB) ##老生代剩余容量
1.6416783843721663% used ##老生代使用比例
Perm Generation: ##perm区使用情况
capacity = 134217728 (128.0MB) ##perm区容量
used = 47303016 (45.111671447753906MB) ##perm区已使用容量
free = 86914712 (82.8883285522461MB) ##perm区剩余容量
35.24349331855774% used ##perm区使用比例
-histo[:live] :打印每个class的实例数目,内存占用,类全名信息. VM的内部类名字开头会加上前缀”*”. 如果live子参数加上后,只统计活的对象数量.
-permstat :打印classload和jvm heap长久层的信息. 包含每个classloader的名字,活泼性,地址,父classloader和加载的class数量. 另外,内部String的数量和占用内存数也会打印出来.
-F :强迫.在pid没有相应的时候使用-dump或者-histo参数. 在这个模式下,live子参数无效.
jmap命令详解
- jhat:虚拟机堆转储快照分析工具
jhat 配合 jmap -dump 使用,是来分析生成的快照,不推荐使用
- jstack:Java堆栈跟踪工具
jstack 命令用户生成虚拟机当前时刻的线程快照,用来查看死锁,死循环,请求外部资源导致长时间等待的问题
命令格式jstack[option] vmid
-F:当正常舒服的请求不被响应时,强制输出线程堆栈
-l : 除堆栈外,显示关于锁的附加信息
虚拟机类加载机制
-
类加载时机
类从被加载到虚拟机内存中开始,到卸载出内存位置,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载。
上图中,验证,解析,准备统称为连接,
加载,验证,准备,初始化,卸载这个顺序是一定的。。
而解析阶段则不一定,他在某些情况下可以在初始化阶段之后在开始,这是为了支持java语言的运行时绑定。虚拟机严格规定了有且只有5种情况立即对类进行‘初始化’
1:使用new关键字实例化对象,读取或者设置一个类的静态字段,调用一个类的静态方法。
2:使用反射的时候,如果累没有进行过初始化。则先进行初始化
3:当初始化一个雷的时候,则先需要触发父类的初始化
4:当虚拟机启动时,会初始化需要执行的主类(包含main方法)
5:当使用JDK1,7的动态语言支持时,如果一个MethodHandle实例最后解析结果是REF_getStatic,REF_putStatic,REF_invokeStatic的语法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发初始化
除上述情况外,都不会进行初始化,下边有三个被动引用(不会触发初始化)的例子
1,子类调用父类的静态字段(不是常量),不会对子类进行初始化
2,通过数组定义引用类,不会触发对此类的初始化
Person[] persons = new Personp[10];
3,调用常量,也不会对该类进行初始化。
-
类加载过程
加载是类加载过程的第一个阶段,
1:通过一个类的全限定名来获取定义此类的二进制字节流
因为要求并不算具体,所以从哪读取相当的开放,如:
从zip包中读取,从网络上读取,运行时计算生成,JSP,,。。。。
2:将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3:在内存中生成一个代表这个类的java.lang.Class对象,做为方法区这个类的各种数据访问接口对于数组类而言,情况就有所不同,数组类不需要类加载器去完成,她是由java细腻及直接创建,但数组类与类加载器仍有很密切的关系,因为数组类的元素类型还是要靠类加载器去创建,
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些数据类型的外部接口。
加载阶段与连接阶段的部分内容,是交叉进行的。加载阶段尚未完成,连接阶段可能已经开始。
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全。
1:文件格式的验证。
是否以魔数0XCAFEBABE开头
主次版本号是否在当前虚拟机处理范围之内。
常量池的常量中是否有不被支持的常量类型。
指向常量的各种索引值是否有执行不存在的常量活不符合类型的常量。
CONSTANT_Utf-8_info型的常量中是否有不符合UTF-8编码的数据
Class文件中哥哥部分及文件本身是否有被删除的或附加的其他信息。
......
2:元数据验证
第二阶段验证是对字节码描述的信息进行语义分析,以保证其描述信息符合Java语言规范的要求、
这个类是否有父类
这个类的父类是否继承了不被允许继承的类
这个类是不是抽象类,是否实现了其父类或接口中要求实现的方法
类中的字段,方法是否与父类产生矛盾,
3:字节码验证
第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流的控制和控制流分析,确定程序语义是合法,符合逻辑的。
保证任何时刻操作数栈的数据类型与执行代码序列都能配合工作。
保证跳转指令不会跳转到方法体意外的字节码指定上
保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类对象。
4:符号引用验证
最后一个阶段的校检发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在链接的第三阶段-解析阶段发生,符号引用验证可以看做是对类的自身以外的信息进行校检,
符号引用中通过字符串描述的全限定名是否能找到对应的类
在指定类中是否存在符合方法的字段面熟以及简单名称锁描述的方法和字段
符号引用中的类,字段,方法的访问性(private,public...)是否可以被当前类访问准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量锁使用的内存都将在方法区中进行分配,这时候进行内存分配的仅包括类变量(被static修饰的变量)而不包括实例变量。解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用的目标不一定在内存中,直接引用的目标一定在内存中。初始化
类初始化阶段是类加载的最后一步,前面的类加载过程中,除了在家在阶段用户应用程序可以通过自定义类加载器参与之外,其余动作由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中java程序代码。
- 类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码称为类加载器。
类加载器设计之初是为了满足java Applet的需求开发的。虽然这个需求现在已经死掉,但是,类加载器却在类层次划分,OSGI,热部署,代码加密等领域大放光彩
1,类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,
比较两个类是否相等,只有在这两个类是由同一个类加载器加载出来的才有意义。
2,双亲委派模型
从java虚拟机的角度来讲,只有两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader)这个类由c++实现,是虚拟机一部分,另一种就是所有其他的类加载器,这些加载器由java实现,独立于虚拟机外部。并且全部继承于java.lang.ClassLoader。从开发者角度来看,可以用到常见的3种系统提供的类加载器
①启动类加载器
这个类加载器负责将存放在lib目录中的,并且是虚拟机识别的类加载到虚拟机内存中,启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派个引导类加载器,那么直接用null代替。
②扩展类加载器
这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载lib/ext目录下的,或者被java.ext.dirs系统变量所指定的路径中所有的类库,开发者可以直接使用扩展类加载器。
③应用程序加载器
这个类加载器由sun,misc.Launcher$AppClassLoader实现,由于这个类加载器时ClassLoader中的getSysClassLoader()方法的返回值,所以一般也成为系统类加载器,他负责加载用户类路径上所指定的类库,开发者也可以直接使用这个类加载器,如果应用程序中没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
双亲委派模型的工作过程:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求给父类去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器无法完成加载,子加载器才会尝试自己去完成。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如 Object类,它存放在rt.jar中,无论哪一个类加载器要加载这个类,都会委派给最顶端的启动类加载器去加载。因此Object类在程序中永远都是同一个类。
如果没有使用双亲委派。并且放在classpath中,那系统会出现不同的Object类,应用程序就会变乱。
//双亲委派模型实现
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;
}
}
,,,,,,,,,,,,,,,,,,,,,,,