资讯专栏INFORMATION COLUMN

摘记《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》

zoomdong / 3304人阅读

摘要:第章内存区域与内存溢出异常运行时数据区域虚拟机在执行程序的过程中会把它所管理的内存划分为若干个不同的数据区域。即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

第2章 Java内存区域与内存溢出异常 2.2 运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。根据《Java虚拟机规范(Java SE 7版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:

2.2.1 程序计数器(Program Counter Register)

每条线程都需要有一个独立的程序计数器,互不影响,独立存储

较小的内存空间

记录当前线程所执行的代码的行号指示器

字节码解释器工作时通过改变程序计数器的值,来选去下一条需要执行的字节码指令

Java虚拟机规范没有规定此区域存在OOM

2.2.2 Java虚拟机栈(Java Virtual Machine Stacks)

生命周期与线程相同

描述的是Java方法执行的内存模型

每个方法在执行的同时都会创建一个栈帧(存放局部变量表、操作数栈、动态链接、方法出口等)

方法调用即栈帧的出入栈

局部变量表:基本数据类型、对象引用、returnAddress类型

64位长度的long和double类型的数据会占用2个局部变量空间(Slot)

局部变量空间在编译期分配完成;运行期间不会改变大小

Java虚拟机规范规定2种异常情况:

StackOverflowError:线程请求的栈深度 > 虚拟机所允许的深度

OutOfMemoryError:虚拟机栈动态扩展时无法申请到足够内存

2.2.3 本地方法栈(Native Method Stack)

为虚拟机调用Native方法提供服务(虚拟机栈是为虚拟机调用Java方法提供服务)

也会抛出StackOverflowError和OutOfMemoryError

2.2.4 Java堆(Java Heap)

所有线程共享

虚拟机启动时创建

存放对象实例

堆空间可以物理上不连续,逻辑上连续

OutOfMemoryError:对象实例没有被分配,且堆无法扩展

2.2.5 方法区(Method Area)

线程共享

存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

永久代:HotSpot在1.7之前把GC分代收集扩展至方法区,即用永久代实现方法区

好处:可以像管理Heap一样管理方法区

坏处:容易遇到内存溢出问题,永久代有-XX:MaxPermSize的上限

这区域的内存回收目标主要是针对常量池的回收和对类型的卸载

2.2.6 运行时常量池(Runtime Constant Pool)

方法区的一部分

用于存放编译期生成的各种字面量和符号引用,在类加载后进入存放

具有动态性,除了编译期,运行期也可以将新的常量存入(例如 String.intern())

受到方法区内存的限制

2.2.7 直接内存(Direct Memory)

并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域

但使用频繁,可能导致OutOfMemoryError

分配不会受到Java堆大小的限制,但受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制

NIO使用Native函数库直接分配对外内存,通过堆内的DirectByteBuffer对象引用该内存,因为避免了Heap与Native Heap来回复制数据,提高了性能

2.3 HotSpot虚拟机对象探秘 2.3.1 对象的创建

先检查指令参数是否在常量池中存在该类的符号引用,并检查该符号引用是否被加载、解析和初始化

若无,则执行类加载过程

垃圾收集器带压缩功能(Serial、ParNew) -> Heap是连续的 -> “指针碰撞”(Bump the Pointer)分配内存

垃圾收集器不带压缩功能(CMS) -> Heap不是连续的 -> “空闲列表”(Free List)分配内存

同步分配内存空间2种方式:

虚拟机采用CAS配上失败重试的方式保证更新操作的原子性

每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。只有TLAB用完并分配新的TLAB时,才需要同步锁定;通过-XX:+/-UseTLAB参数来设定

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)

设置对象头(Object Header)信息。包括:元数据信息、hash码、GC分代年龄信息等

执行方法,初始化对象。

2.3.2 对象的内存布局

HotSpot VM中,对象在内存中的布局:

对象头(Header)

Mark Word。存储运行时数据;如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等

类型指针。即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据(Instance Data)。对象真正存储的有效信息

对齐填充(Padding)。仅起着占位符的作用

2.3.3 对象的访问定位

以下是Java程序通过栈上的Reference来操作堆上的具体对象。

方式一:使用句柄

优势:reference存放的稳定句柄,对象移动不会影响到reference

劣势:需要在堆上开辟一块空间存放句柄信息

方式二:使用直接指针

优势:reference存放的对象地址,访问速度快。

劣势:对象移动时需要更新reference。

HotSpot使用这种

2.4 实战:OutOfMemoryError异常 2.4.1 Java堆溢出

将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展

-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照

2.4.2 虚拟机栈和本地方法栈溢出

在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常(单线程下居多)

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常(多线程下居多)

不考虑虚拟机本身耗费内存、程序计数器内存(很小)
虚拟机栈和本地方法栈分配到的内存 = 进程内存 - 最大堆内存(Xmx)- 最大方法区(MaxPermSize)
所以线程数越多,单个线程内存就越小,成反比
2.4.3 方法区和运行时常量池溢出

方法区主要存放Class相关的信息,当使用例如CGLib字节码增强、动态语言时,容易导致方法区内存溢出

2.4.4 本机直接内存溢出

DirectMemory可以通过-XX:MaxDirectMemorySize进行设置,不设置则等同于Heap最大值。

Heap Dump文件中不会看见明显的异常

如果Dump文件很小,但程序有使用NIO,则可能时本机直接内存溢出

第3章 垃圾收集器与内存分配策略

本章讨论Heap内存的分配和回收

3.2 对象已死吗 3.2.1 引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

很难解决对象之间相互循环引用的问题

3.2.2 可达性分析算法
这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

可作为CG Root的对象:

虚拟机栈(栈帧中的本地变量表)中引用的对象。

方法区中类静态属性引用的对象。

方法区中常量引用的对象。

本地方法栈中JNI(即一般说的Native方法)引用的对象。

3.2.3 再谈引用

强引用:类似Object() obj = new Object();,只要存在引用,便无法进行垃圾收集

软引用:描述一些有用但非必需的对象;在系统将要内存溢出前,进行二次收集,如果还是不足,则抛出内存溢出异常。SoftReference

弱引用:描述非必需对象,只能生存到下一次垃圾收集器工作之前,不管内存是否不足。WeakReference

虚引用:无法通过其获取对象实例,作用时当对被垃圾收集时可以获取一个系统通知。

3.2.4 生存还是死亡

当对象被检测到没有与GC Root可达,则将会被第一次标记,如果对象没有覆盖finalize(),或者finalize()已经被调用过,则不会执行

对象进入F-Queue,稍后虚拟机自动建立Finalizer线程执行它,仅触发

GC对F-Queue中的对象进行二次标记,标记前如果对象和GC Root关联,则可以逃脱

所以主动调用finalize()并不能立即触发GC,它不是C++中的析构函数
3.2.5 回收方法区

永久代收集内容:

废弃常量 :常量池中没有被引用的字面量

无用类:

所有实例都被回收

ClassLoader被回收

Class对象没有被引用

3.3 垃圾收集算法 3.3.1 标记-清除算法(Mark-Sweep)

首先标记出所有需要回收的对象

在标记完成后统一回收所有被标记的对象

不足:

效率不够高,标记和清除两个效率都不高

空间问题,会产生不连续的碎片内存,

3.3.2 复制算法(Coping)

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。

一块内存用完,将存活对象复制到另一块,然后将已使用的对象清除

不用考虑碎片问题,只要移动堆顶指针,按顺序分配即可

空间利用率低

现在的商业虚拟机都采用这种收集算法来回收新生代

当复制到另一个Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)

3.3.3 标记-整理算法(Mark-Compact)

先标记需要回收的对象

再移动存活对象到一端

最后清理

3.3.4 分代收集算法(Generational Collection)

当前商业虚拟机的垃圾收集都采用“分代收集”

根据对象存活周期进行分代

新生代:复制法;大量对象存活时间短 (Eden/Survivor0/Survivor1 : 8/1/1)

老年代:标记清除法、标记整理法;存活时间长

3.4 HotSpot的算法实现 3.4.1 枚举根节点

可达性分析为保证准确性必须在一个保证一致性的快照中进行,所以导致GC进行时需要停顿所有Java线程 -- Stop The World。

CMS收集器中,枚举根节点时也是必须要停顿的。

HotSpot通过内部实现的OopMap数据结构可以快速且准确地完成GC Roots枚举,在类加载期和编译期记录下对象引用信息,方便GC扫描。

3.4.2 安全点

HotSpot只在特定位置设置引用信息 -- 安全点

程序只有在安全点才会停下来执行GC

选定标准“是否具有让程序长时间执行的特征”,即指令序列复用,例如:方法调用、循环跳转、异常跳转等

安全点位置选定还需考虑GC时让所有线程都进入此

抢先式中断:在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。(现在几乎不采用)

主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的

3.4.3 安全区域

在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

3.5 垃圾收集器

3.5.1 Serial

最基本、发展历史最悠久的收集器

它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束 -- Stop The World

默认Client模式下新生代收集器

3.5.2 ParNew

Serial的多线程版本

许多Server模式下首选的新生代收集器

除了Serial收集器外,目前只有它能与CMS收集器配合工作

使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

默认开启的收集线程数与CPU的数量相同

可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

3.5.3 Parallel Scavenge

是一个新生代收集器,使用复制算法,并行的多线程收集器

关注的维度不同

CMS考虑停顿时间,适合交互多的程序;

Parallel Scavenge考虑吞吐量,适合高效利用CPU时间的后台程序

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

-XX:MaxGCPauseMillis控制最大垃圾收集停顿时间,参数是>0的毫秒数,如果停顿时间减小,吞吐量降低,收集次数增加。

-XX:GCTimeRatio直接设置吞吐量大小,大于0且小于100的整数,垃圾收集时间占总时间的比率,相当于是吞吐量的倒数

-XX:+UseAdaptiveSizePolicy GC自适应调节策略,内存管理调优过程由虚拟机完成,这是与ParNew最大的区别

3.5.4 Serial Old

Serial的老年版本

单线程,使用“标记-整理”算法

Client模式下的虚拟机使用

3.5.5 Parallel Old

Parallel Scavenge收集器的老年代版本

使用多线程和“标记-整理”算法

在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old

3.5.6 CMS (Concurrent Mark Sweep)

以获取最短回收停顿时间为目标

“标记-清除”算法

初始标记(CMS initial mark):Stop The World,仅标记一下GC Roots能直接关联到的对象

并发标记(CMS concurrent mark):进行GC RootsTracing的过程,可与用户线程一起工作

重新标记(CMS remark):Stop The World,修正并发标记期间因用户程序运作导致标记变动的对象标记记录,时间稍长于初始标记,远小于并发标记

并发清除(CMS concurrent sweep):可与用户线程一起工作

缺点:

对CPU资源非常敏感,并发阶段会占用一部分线程导致应用变慢,总吞吐量降低

CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生。

产生碎片空间可能无法存放当前对象,导致进行Full GC

浮动垃圾:并发清除时用户线程还在运行,可能在标记过程后产生部分垃圾,只能留到下次GC时清除。

3.5.7 G1

面向服务端应用的垃圾收集器

并行与并发:使用多CPU来缩短Stop The World

分代收集:可以独立管理整个GC堆

空间整合:整体是基于“标记—整理”算法,局部(两个Region之间)是基于“复制”算法,保证不会产生碎片

可预测的停顿:它将整个Java堆划分为多个大小相等的独立区域(Region),跟踪各个Region的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,

每个Region内部维护一个Remmbered Set来记录对象引用信息,后面可以不用通过全堆扫描来收集垃圾

G1的运作步骤:

初始标记(Initial Marking):标记GC Root到直接关联的对象,修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。

并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

最终标记(Final Marking):修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

3.5.9 垃圾收集器参数总结

3.6 内存分配与回收策略 3.6.1 对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

-XX:+PrintGCDetails:在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况

-Xms20M、-Xmx20M、-Xmn10M这3个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代

-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

3.6.2 大对象直接进入老年代

大对象:需要大量连续内存空间的Java对象;例如:很长的字符串以及数组

经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间

-XX:PretenureSizeThreshold:令大于这个设置值的对象直接在老年代分配,只对Serial和ParNew两款收集器有效

3.6.3 长期存活的对象将进入老年代

对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,对象年龄设为1

对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁

当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中

对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置

3.6.4 动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

3.6.5 空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC

在JDK 6 Update 24之后,这个测试结果会有差异,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略

JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

第4章 虚拟机性能监控与故障处理工具 jps:虚拟机进程状况工具

JVM Process Status Tool

使用频率最高的JDK命令行工具

jps[options][hostid]

jps可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名

jstat:虚拟机统计信息监视工具

JVM Statistics Monitoring Tool

可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据

jstat[option vmid[interval[s|ms][count]]]

interval:查询间隔
count:次数

#每250毫秒查询一次进程2764垃圾收集状况,一共查询20次
jstat -gc 2764 250 20

jinfo:Java配置信息工具

Configuration Info for Java

实时地查看和调整虚拟机各项参数

jinfo[option]pid

# 查询CMSInitiatingOccupancyFraction参数值
$ jinfo -flag CMSInitiatingOccupancyFraction 13435
-XX:CMSInitiatingOccupancyFraction=-1
jmap:Java内存映像工具

Memory Map for Java

生成堆转储快照(一般称为heapdump或dump文件)

其他方式获得dump文件:

-XX:+HeapDumpOnOutOfMemoryError:OOM异常出现之后自动生成dump文件

-XX:+HeapDumpOnCtrlBreak:使用[Ctrl]+[Break]键让虚拟机生成dump文件

kill -3:发送进程退出信号“吓唬”一下虚拟机,也能拿到dump文件

jhat:虚拟机堆转储快照分析工具

JVM Heap Analysis Tool

与jmap搭配使用,来分析jmap生成的堆转储快照

功能较简陋

jstack:Java堆栈跟踪工具

Stack Trace for Java

生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)

定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等

jstack[option]vmid

第5章 调优案例分析与实战 5.2 案例分析 高性能硬件上的程序部署策略

在大多数网站形式的应用里,主要对象的生存周期都应该是请求级或者页面级的,会话级和全局级的长生命对象相对很少。只要代码写得合理,应当都能实现在超大堆中正常使用而没有Full GC,这样的话,使用超大堆内存时,网站响应速度才会比较有保证。

堆外内存导致的溢出错误

垃圾收集进行时,虚拟机虽然会对Direct Memory进行回收,但是Direct Memory却不能像新生代、老年代那样,发现空间不足了就通知收集器进行垃圾回收,它只能等待老年代满了后Full GC,然后“顺便地”帮它清理掉内存的废弃对象。否则它只能一直等到抛出内存溢出异常时,先catch掉,再在catch块里面“大喊”一声:"System.gc()!"。要是虚拟机还是不听(譬如打开了-XX:+DisableExplicitGC开关),那就只能眼睁睁地看着堆中还有许多空闲内存,自己却不得不抛出内存溢出异常了。而本案例中使用的CometD 1.1.1框架,正好有大量的NIO操作需要使用到Direct Memory内存。

从实践经验的角度出发,除了Java堆和永久代之外,我们注意到下面这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进程最大内存的限制。

Direct Memory:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或者OutOfMemoryError:Direct buffer memory。

线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或者OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。

Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出IOException:Too many open files异常。

JNI代码:如果代码中使用JNI调用本地库,那本地库使用的内存也不在堆中。

虚拟机和GC:虚拟机、GC的代码执行也要消耗一定的内存。

外部命令导致系统缓慢

Java的Runtime.getRuntime().exec()方法,首先克隆一个和当前虚拟机拥有一样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程。如果频繁执行这个操作,系统的消耗会很大,不仅是CPU,内存负担也很重

第7章 虚拟机类加载机制 7.2 类加载的时机

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的

解析可以在初始化之后,为了支持Java的运行时绑定(动态绑定)

因为各个阶段都是相互交叉地混合式进行,所以不一定按顺序完成

虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

7.3 类加载过程 7.3.1 加载

通过一个类的全限定名来获取定义此类的二进制字节流。

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

在内存中生成一个代表这个类的java.lang.Class对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),作为方法区这个类的各种数据的访问入口。

7.3.2 验证

目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证的4个阶段:

文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求

字节码验证:第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件

符号引用验证:校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验

7.3.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中

7.3.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

7.3.5 初始化

真正开始执行类中定义的Java程序代码(或者说是字节码)。

初始化阶段是执行类构造器<clinit>()方法的过程,初始化类变量和其他资源

7.4 类加载器

类加载器在虚拟机外部

7.4.1 类与类加载器

每一个类加载器,都拥有一个独立的类名称空间;例如:两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

7.4.2 双亲委派模型(Parents Delegation Model)

从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现[1],是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

从Java开发人员角度可以大致细分程3种:

启动类加载器(Bootstrap ClassLoader)[不能直接使用]

<JAVA_HOME>lib

-Xbootclasspath指定目录

虚拟机识别的类库

扩展类加载器(Extension ClassLoader),[可直接使用]

<JAVA_HOME>libext

java.ext.dirs系统变量指定的类库

应用程序类加载器(Application ClassLoader),[可直接使用]

ClassLoader中的getSystemClassLoader()方法的返回值

加载用户类路径(ClassPath)上所指定的类库

程序中默认的类加载器

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

  protected synchronized Class loadClass(String name, boolean resolve)
      throws ClassNotFoundException {
    // 首先判断该类型是否已经被加载
    Class c = findLoadedClass(name);
    if (c == null) {
      // 如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
      try {
        if (parent != null) {
          // 如果存在父类加载器,就委派给父类加载器加载
          c = parent.loadClass(name, false);
        } else {
          // 如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
          c = findBootstrapClass0(name);
        }
      } catch (ClassNotFoundException e) {
        // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
        c = findClass(name);
      }
    }
    if (resolve) {
      resolveClass(c);
    }
    return c;
  }
第9章 类加载及执行子系统的案例与实战 9.2.1 Tomcat:正统的类加载器架构

主流Java Web服务器要解决的问题:

部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离

部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。

服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。

Tomcat的目录结构:

/common/*:类库可被Tomcat和所有的Web应用程序共同使用。

/server/*:类库可被Tomcat使用,对所有的Web应用程序都不可见。

/shared/*:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。

/WebApp/WEB-INF/*:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

灰色:JDK默认加载器

每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73623.html

相关文章

  • 学习JVM必看书籍

    学习JVM的相关资料 《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》 showImg(https://segmentfault.com/img/bVbsqF5?w=200&h=200); 基于最新JDK1.7,围绕内存管理、执行子系统、程序编译与优化、高效并发等核心主题对JVM进行全面而深入的分析,深刻揭示JVM的工作原理。以实践为导向,通过大量与实际生产环境相结合的案例展示了解...

    shaonbean 评论0 收藏0
  • 从小白程序员一路晋升为大厂高级技术专家我看过哪些书籍?(建议收藏)

    摘要:大家好,我是冰河有句话叫做投资啥都不如投资自己的回报率高。马上就十一国庆假期了,给小伙伴们分享下,从小白程序员到大厂高级技术专家我看过哪些技术类书籍。 大家好,我是...

    sf_wangchong 评论0 收藏0
  • 深入理解Java虚拟》(一)Java虚拟发展史

    摘要:虚拟机发展史注本文大部分摘自深入理解虚拟机第二版作为一名开发人员,不能局限于语言规范,更需要对虚拟机规范有所了解。虚拟机规范有多种实现,其中是和中所带的虚拟机,也是目前使用范围最广的虚拟机。世界第一款商用虚拟机。号称世界上最快的虚拟机。 Java虚拟机发展史 注:本文大部分摘自《深入理解Java虚拟机(第二版)》 作为一名Java开发人员,不能局限于Java语言规范,更需要对Java虚...

    张春雷 评论0 收藏0
  • 我的2016年Java书单

    摘要:相对于电子书,我更喜欢纸质版的书籍。过去的年一共阅读过本技术书,下面对这些书做一个小结。源码深度解析这本书是年购买的,年是第四次阅读。必知必会数据库的复习书籍,内容浅显易懂。 相对于电子书,我更喜欢纸质版的书籍。我喜欢在拿到新书时记录购买时间、地点、开始阅读的时间、第一次看完的时间,算是一种学习的记录。过去的2016年一共阅读过15本技术书,下面对这些书做一个小结。 《深入理解Java...

    Scholer 评论0 收藏0
  • 报道帖——给 Segmentfault 朋友们的电子书

    摘要:一直都挺喜欢这个社区的,给人的第一感觉就是比较的专业正式,社区内氛围不错,各种文章的质量也很好,并且帮助了我很多。很开心能够来到这里,记录自己的成长,希望自己能够多活跃一下,无论是在问答上面还是写作上面。 一直都挺喜欢 Segmentfault 这个社区的,给人的第一感觉就是比较的专业正式,社区内氛围不错,各种文章的质量也很好,并且帮助了我很多。很开心能够来到这里,记录自己的成长,希望...

    cnsworder 评论0 收藏0

发表评论

0条评论

zoomdong

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<