《深入理解Java虚拟机》第2版笔记(完整)

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

第1章走近Java
1.2 Java技术体系
1.Java程序设计语言、Java虚拟机、Java API类库这三部分统称为JDK(Java
Development Kit),JDK是用于支持Java程序开发的最小环境。

1.4 Java虚拟机发展史
1.4.1 Sun Classic / Exact VM
1.Exact VM因它使用准确式内存管理而得名,即虚拟机可以知道内存中某个位置的
数据具体是什么类型。

譬如内存中有一个32位的整数123456,它到底是一个
reference类型指向123456的内存地址还是一个数值为123456的整数,虚拟机
将有能力分辨出来,这样才能在GC的时候准确判断堆上的数据是否还可能被使用。

由于使用了准确式内存管理,Exact VM可以抛弃以前Classic VM基于handler
的对象查找方式,这样每次定位对象都烧了一次间接查找的开销,提升执行性能。

1.4.2 Sun HotSpot VM
1.HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,
然后通知JIT编译器以方法为单位进行编译。

如果一个方法被频繁调用,或方法中
有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。

1.4.6 Apache Harmony / Google Android Dalvik VM
1.Dalvik VM并不是一个Java虚拟机,它没有遵循Java虚拟机规范,不能直接执行
Java的Class文件,使用的是寄存器架构而不是JVM中常见的栈架构。

但是它与Java又有着千丝万缕的联系,它执行的dex(Dalvik Executable)文件可以通过
Class文件转化而来,使用Java语法编写应用程序,可以直接使用大部分的Java API 等。

1.5 展望Java技术的未来
1.函数式编程的一个重要优点就是这样的程序天然地适合并行运行。

第2章Java内存区域与内存溢出异常2.2 运行时数据区域
2.2.4 Java堆
1.此内存区域(Java堆)的唯一目的就是存放对象实例,几乎所有的对象实例都在这
里分配内存。

2.2.5 方法区
1.方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存
储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

2.3 HotSpot虚拟机对象探秘
2.3.1 对象的创建
1.选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾
收集器是否带有压缩整理功能决定。

因此,在使用Serial、ParNew等带Compact 过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于
Mark-Sweep算法的收集器时,通常采用空闲列表。

2.另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在
Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。

哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB 用完并分配新的TLAB时,才需要同步锁定。

2.3.2 对象的内存布局
1.HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数
据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为”Mark Word”。

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

2.3.3 对象的访问定位
1.使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被
移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

2.4 实战:OutOfMemoryError异常
2.4.1 Java堆溢出
1.保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象。

2.重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏
(Memory Leak)还是内存溢出(Memory Overflow)。

2.4.2 虚拟机栈和本地方法栈溢出
1.如果使用虚拟机默认参数,栈深度在大多数情况下达到1000~2000完全没有问题,
对于正常的方法调用(包括递归),这个深度应该完全够用了。

但是,如果是建立
过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就
只能通过减少最大堆和减少栈容量来换取更多的线程。

2.4.3 方法区和运行时常量池溢出
1.在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也
是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java
堆上,所以必然不是同一个引用,将返回false。

而JDK 1.7(以及部分其他虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。

第3章垃圾收集器与内存分配策略
3.1 概述
1.程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。

2.每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区
域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

3.2 对象已死吗
3.2.2 可达性分析算法
1.在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。

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

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

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

3.2.4 生存还是死亡
1.这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,
这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导
致整个内存回收系统崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍
后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()
中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将
被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被
回收了。

3.4 HotSpot算法实现
3.4.2 安全点
1.所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准
进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生
Safepoint。

2.现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。

3.4.3 安全区域
1.所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态
或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方
去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。

2.安全区域是指在一段代码片段中,引用关系不会发生变化。

在这个区域中的任意地
方开始GC都是安全的。

我们也可以把Safe Region看做是被扩展了的Safepoint。

3.5 垃圾收集器
1.Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、
不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参
数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。

3.5.1 Serial收集器
1.Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。

3.5.3 Parallel Scavenge收集器
1.Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器
的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge
收集器的目标则是达到一个可控制的吞吐量(Throughput)。

所谓吞吐量就是CPU 用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量= 运行用户代码时间/ (运行用户代码时间+ 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

3.5.6 CMS收集器
1.其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。

初始标记仅
仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户
程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间按一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

2.实践证明,增量时的CMS收集器效果很一般,在目前版本中,i-CMS已经被声明
为“deprecated”,即不再提倡用户使用。

3.要是CMS运行期间预留的内存无法满足程序需要,就会出现一次”Concurrent
Mode Failure“失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

3.5.7 G1收集器
1.G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收
所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。

3.6 内存分配与回收策略
3.6.2 大对象直接进入老年代
1.最典型的大对象就是那种很长的字符串以及数组(笔者列出的例子中的byte[]数组
就是典型的大对象)。

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

第4章虚拟机性能监控与故障处理工具
4.2 JDK的命令行工具
4.2.4 jmap: Java内存映像工具
1.又或者在Linux系统下通过Kill -3命令发送进程退出信号”吓唬“一下虚拟机,也
能拿到dump文件。

4.3 JDK的可视化工具
4.3.1 JConsole: Java监视与管理控制台
1.readBytes方法检查到流没有更新时会立刻归还执行令牌,这种等待只消耗很小的
CPU资源。

2.没有归还线程执行令牌的动作,会在空循环上用尽全部执行时间直到线程切换,这
种等待会消耗较多CPU资源。

3.造成死锁的原因是Integer.valueOf()方法基于减少对象创建次数和节省内存的考
虑,[-128, 127]之间的数字会被缓存,当valueOf()方法传入参数在这个范围之内,将直接返回缓存中的对象。

也就是说,代码中调用了200次Integer.valueOf()方法一共就只返回了两个不同的对象。

第5章调优案例分析与实战
5.2 案例分析
5.2.1 高性能硬件上的程序部署策略
1.我们仅仅需要保障集群具备亲合性,也就是均衡器按一定的规则算法(一般根据
SessionID分配)将一个固定的用户请求永远分配到固定的一个集群节点进行处理
即可,这样程序开发阶段就基本不用为集群环境做什么特别的考虑了。

5.2.3 堆外内存导致的溢出错误
1.大家知道操作系统对每个进程能管理的内存是有限制的,这台服务器使用的32位
Windows平台的限制是2GB,其中划了1.6GB给Java堆,而Direct Memory内存并不算入1.6GB的堆之内,因此它最大也只能在剩余的0.4GB空间中分出一部分。

在此应用中导致溢出的关键是:垃圾收集进行时,虚拟机虽然会对Direct Memory 进行回收,但是Direct Memory却不能像新生代、老年代那样,发现空间不足了就通知收集器进行垃圾回收,它只能等待老年代满了后Full GC,然后“顺便地”
帮它清理掉内存的废弃对象。

5.2.4 外部命令导致系统缓慢
1.执行这个shell脚本是通过Java的Runtime.getRuntime().exec()方法来调用的。

这种调用方式可以达到目的,但是它在Java虚拟机中是非常消耗资源的操作,即使外部命令本身能很快执行完毕,频繁调用时创建进程的开销也非常可观。

Java虚拟
机执行这个命令的过程时:首先克隆一个和当前虚拟机拥有一样环境变量的进城,再用这个新的进城去执行外部命令,最后再退出这个进城。

如果频繁执行这个操作,系统的消耗会很大,不仅是CPU,内存负担也很重。

5.2.6 不恰当数据结构导致内存占用过大
1.在HashMap<Long,Long>结构中,只有Key和Value所存放的两个长整型数据
是有效数据,共16B(2×8B)。

这两个长整型数据包装成ng.Long对象之
后,就分别具有8B的Mark Word、8B的Klass指针,在加8B存储数据的long 值。

在这两个Long对象组成Map.Entry之后,又多了16B的对象头,然后一个
8B的next字段和4B的int型的hash字段,为了对齐,还必须添加4B的空白填
充,最后还有HashMap中对这个Entry的8B的引用,这样增加两个长整型数字,实际耗费的内存为(Long(24B)×2)+Entry(32B)+HashMapRef(8B)=88B,空间效率为16B/88B=18%,实在太低了。

看完HashMap的源码之后,就全都明白了
5.3 实战:Eclipse运行速度调优
5.3.3 编译时间和类加载时间的优化
1.编译时间是指虚拟机的JIT编译器编译热点代码的耗时。

我们知道Java语言为了实
现跨平台的特性,Java代码编译出来后形成的Class文件中存储的是字节码
(ByteCode),虚拟机通过解释方式执行字节码命令,比起C/C++编译成本地二进制代码来说,速度要慢不少。

5.3.4 调整内存设置控制垃圾收集频率
1.由于我们做的测试是在测程序的启动时间,所以类加载和编译时间在这项测试中的
影响力被大幅度放大了。

2.严格来说,不包括正在执行native代码的用户线程,因为native代码一般不会改
变Java对象的引用关系,所以没有必要挂起它们来等待垃圾回收。

3.Eclipse启动时,Full GC大多数是由于老年代容量扩展而导致的,由永久代空间扩
展而导致的也有一部分。

第6章类文件结构
6.2 无关性的基石
1.Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的
二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。

6.3 Class文件的结构
1.当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若
干个8位字节进行存储。

2.无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使
用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。

6.3.1 魔数与Class文件的版本
1.很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或者jpeg
等在文件头中都存有魔数。

2.Class文件的魔数的获得很有“浪漫气息”,值为:0xCAFEBABE(咖啡宝贝?),
这个魔数值在Java还称做“Oak”语言的时候(大约是1991年前后)就已经确定下来了。

6.3.2 常量池
1.在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这
样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达
“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。

6.3.6 方法表集合
1.方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中的
一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。

2.特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为
返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。

6.3.7 属性表集合
1.方法参数(包括实例方法中的隐藏参数“this”)、显式异常处理器的参数
(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。

2.Javac编译器会根据变量的作用域来分配Slot给各个变量使用。

3.通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的
访问,然后在虚拟机调用实例方法时自动传入此参数。

因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。

4.此时x的值复制一份副本到最后一个本地变量表的Slot中。

5.不要与前面刚刚讲解完的异常表产生混淆。

Exceptions属性的作用是列举出方法中
可能抛出的受查异常(Checked Exceptions),也就是方法描述时在throws关键字后面列举的异常。

6.start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量
及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。

7.目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按
照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者
ng.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法(类构造器,class init)中进行初始化。

8.所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和
ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器<init>方法和类构造器<clinit>方法。

9.省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而是在编
译阶段将一系列的验证类型(Verification Types)直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程。

10.运行期无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同
等对待,例如运行期做反射时无法获得泛型信息。

Signature属性就是为了弥补这个缺陷而增设的,现在Java的API能够获取泛型类型,最终的数据来源也就是这个属性。

6.4 字节码指令简介
1.由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包
含操作数,只有一个操作码。

6.4.10 同步指令
1.方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回
操作之中。

2.当方法调用时,调用指令就会检查方法的ACC_SYNCHRONIZED访问标志是否被
设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程(Monitor)。

3.同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java
虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized 关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。

6.5 公有设计和私有实现
1.将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT
代码生成技术)。

第7章虚拟机类加载机制
7.2 类加载的时机
1.实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,
这两个类在编译成Class之后就不存在任何联系了。

7.3 类加载的过程
7.3.1 加载
1.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

7.3.2 验证
1.这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流
才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

7.3.3 准备
1.这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配
的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

7.4 类加载器
1.虚拟机设计团队把类加载阶段中的”通过一个类的全限定名来获取描述此类的二进
制字节流“这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。

7.4.1 类与类加载器
1.对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟
机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

2.两行输出结果中,从第一句可以看出,这个对象确实是类
org.fenixsoft.classloading.ClassLoaderTest实例化出来的对象,但从第二句可以发现,这个对象与类org.fenixsoft.classloading.ClassLoaderTest做所属类型检查的时候却返回了false,这是因为虚拟机中存在了两个ClassLoaderTest类,一个
是由系统应用程序类加载器加载的,另外一个是由我们自定义的类加载器加载的,
虽然都来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时结
果自然为false。

7.4.2 双亲委派模型
1.扩展类加载器(Extension ClassLoader):这个加载器由
uncher$ExtClassLoader实现,它负责加载
<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路
径中的所有类库,开发者可以直接使用扩展类加载器。

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

3.Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

例如类
ng.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各
种类加载器环境中都是同一个类。

相反,如果没有使用双亲委派模型,由各个类加
载器自行去加载的话,如果用户自己编写了一个称为ng.Object的类,并放在程序的ClassPath中,那系统中将出现多个不同的Object类,Java类型体系中
最基础的行为也就无法保证,应用程序也将会变得一片混乱。

如果读者有兴趣的话,可以尝试去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。

4.双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现
双亲委派的代码都集中在ng.ClassLoader的loadClass()方法之中,逻辑清晰易懂:先检查是否已被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。

如果父类加载失败,抛出
ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

protected synchronized Class<?> loadClass(String name, boo lean resolve) throws ClassNotFoundException {
Class c = findLoadedClass(name);
if (c==null) {
try {
if (parent!=null)
c = parent.loadClass(name, false);
else
c = findBootstrapClassOrNull(name);
} catch (ClassNotFoundException e)
{}
if (c==null)
findClass(name);
}
if (resolve)
resolveClass(c);
return c;
}
7.4.3 破坏双亲委派模型
1.为了向前兼容,JDK 1.2之后的ng.ClassLoader添加了一个新的protected
方法findClass(),在此之前,用户去继承ng.ClassLoader的唯一目的就是
为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。

2.JDK 1.2之后已不再提倡用户再去覆盖loadClass()方法,而应当把自己的类加载器
逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

3.Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、
JAXE和JBI等。

4.OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。

每一个程序
模块(OSGi中成为Bundle)都有一个自己的类加载器,当需要更换一个Bundle 时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

5.OSGi中对类加载器的使用是很值得学习的,弄懂了OSGi的实现,就可以算是掌握
了类加载器的精髓。

第8章虚拟机字节码执行引擎
8.2 运行时栈帧结构
8.2.1 局部变量表
1.Java虚拟机规范中没有明确规定reference类型的长度,它的长度与实际使用32
还是64位虚拟机有关,如果是64位虚拟机,还与是否开启某些对象指针压缩的优化有关,这里暂且只取32位虚拟机的reference长度。

2.由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot
是否为原子操作,都不会引起数据安全问题。

相关文档
最新文档