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

第2章 Java内存区域与内存溢出异常

2.2 运行时数据区域

Java虚拟机在执行Java程序的过程当中会把它所管理的内存划分为若干个不一样的数据区域。根据《Java虚拟机规范(Java SE 7版)》的规定,Java虚拟机所管理的内存将会包括如下几个运行时数据区域: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分代年龄信息等
  • 执行<init>方法,初始化对象。

2.3.2 对象的内存布局

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

  • 对象头(Header)架构

    1. Mark Word。存储运行时数据;如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
    2. 类型指针。即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例。
  • 实例数据(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 生存仍是死亡

  1. 当对象被检测到没有与GC Root可达,则将会被第一次标记,若是对象没有覆盖finalize(),或者finalize()已经被调用过,则不会执行
  2. 对象进入F-Queue,稍后虚拟机自动创建Finalizer线程执行它,仅触发
  3. 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种状况必须当即对类进行“初始化”(而加载、验证、准备天然须要在此以前开始):

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,若是类没有进行过初始化,则须要先触发其初始化。生成这4条指令的最多见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,若是类没有进行过初始化,则须要先触发其初始化。
  3. 当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。
  4. 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK 1.7的动态语言支持时,若是一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则须要先触发其初始化。

7.3 类加载过程

7.3.1 加载

  1. 经过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个表明这个类的java.lang.Class对象(并无明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,可是存放在方法区里面),做为方法区这个类的各类数据的访问入口。

7.3.2 验证

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

验证的4个阶段:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理。主要目的是保证输入的字节流能正确地解析并存储于方法区以内,格式上符合描述一个Java类型信息的要求。这阶段的验证是基于二进制字节流进行的,只有经过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,因此后面的3个验证阶段所有是基于方法区的存储结构进行的,不会再直接操做字节流。
  2. 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
  3. 字节码验证:第三阶段是整个验证过程当中最复杂的一个阶段,主要目的是经过数据流和控制流分析,肯定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型作完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会作出危害虚拟机安全的事件
  4. 符号引用验证:校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动做将在链接的第三阶段——解析阶段中发生。符号引用验证能够看作是对类自身之外(常量池中的各类符号引用)的信息进行匹配性校验

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类加载器