最近一直在看《深刻理解Java虚拟机》第三版,无心中发现了第三版是最近才发行的,据说讲解的JDK版本升级,新增了近50%的内容。java
这种神书,看懂了,看进去了,真的看的很快,并无想象中的晦涩难懂,毕竟是公认的经典,做者书面描述能力确定了得。虽然这种书,不会让你的代码能力立刻提高,可是真正的让你知其然,还知其因此然。等遇到了这方面的问题,确定不会像无头苍蝇同样,一头雾水,起码有必定的思路。更多Java、计算机方面的一些好书正在路上,今年必定要好好地提高一下内功。程序员
不过,好比第五章的内容,调优实战,没有充足的实战经验和一些大型项目经验,虽然说一些地方能看懂做者在说什么,可是没有一个本身有过经验的实际场景去代入,理解的仍是不够充分。算法
固然看一次确定不能消化完整,虽然在看的时候就在有道笔记上作了一些笔记,可是仍是上传到博客园吧,就当水一篇博客啦。编程
1.程序计数器:是当前线程所执行的字节码的行号指示器。就是经过改变这个行号指示器的值来选取下一个须要执行的字节码指令,从而能够实现循环、跳转、分支、异常处理等基础功能。Java虚拟机的多线程是经过线程间的轮流切换、粉配处理器执行时间来实现的,因此为了让线程切换后恢复到正确的执行位置,每一个线程的计数器是独立的,互不影响,包括主线程。若是线程执行的是Java代码,计数器记录的是字节码的行号,若是执行的是本地方法,计数器为空。Undefined。这个区域不会报内存溢出异常。数组
2.Java虚拟机栈:其也是线程私有的,生命周期与线程相同,其描述的是Java执行的线程内存模型。每一个方法被执行时会建立一个栈帧(一种数据结构),用于存储局部变量表、操做数栈、动态连接、方法出口等信息。(其中动态连接是:在类加载机制中,解析步骤会把符号引用转为直接引用,还有一部分会在执行过程当中才变成直接引用,这就是动态连接)。栈帧的入栈到出栈就是一个方法完整的执行过程。重点是虚拟机栈中的局部变量表,其存放的是基本数据类型、对象引用、returnAddress类型(一条字节码的地址)。当线程请求的栈的深度大于虚拟机容许的深度,会报StackOverflowError。当栈拓展时没法申请到足够的内存会报OutOfMemoryError。缓存
3.本地方法栈:其与Java虚拟机栈做用类似,只是Java虚拟机栈为Java方法服务,而本地方法栈为本地方法服务。也有上面两种异常。安全
4.Java堆:虚拟机管理的内存中最大的一块。Java堆是全部线程共享的,其惟一目的就是存放对象实例。一个对象的建立,其引用放在栈,实例放在堆。Java堆是垃圾收集器管理的内存区域,所以有的人称他为GC堆。GC相关内容后面再记。不管堆这么划分,其存储的都只是对象的实例,细分的目的只是为了更好的回收内存和分配内存。当Java堆没法完成实例分配,堆也没法拓展,会报OutOfMemoryError。服务器
5.方法区:与Java堆同样,是线程共享的内存区域。用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。若是方法区没法知足内存分配需求,将会报OutOfMemoryError异常。数据结构
对象头部分包括两类信息,第一类信息存储对象自己的运行时的数据,如:哈希码、GC分代年龄、线程持有的锁、锁状态、偏向时间戳、偏向线程ID等。多线程
另外一部分是类型指针,即经过这个指针肯定这是哪一个类的实例。若是是数组,还会存储数组的长度。
实例数据部分是对象真正存储的有效信息,即咱们定义的各类类型的字段内容。
对其填充没什么实际意义,HotSopt的内存管理要求任何对象的大小必须是8的整倍数,对象头已经被设计为8的整倍数,可是实例数据就不必定了,这时候就须要对其填充来补全。
对象的访问定位:
Java堆是存放实例数据的,Java栈上的reference数据是存放这个实例的引用的。而这个引用主流有两种实现方法:
1.使用句柄,Java堆可能会划分一块内存空间来做为句柄池,reference存储的就是对象句柄的地址,这个句柄包含了对象的实例数据和具体地址信息。优势:不用改变reference,若是GC让对象地址改变,只改变句柄中的地址就好了。
2.使用指针直接访问,reference存储的就是对象的地址,若是只是访问对象自己的话,就不须要像句柄那样再次访问一个地址了。优势:不用二次指针定位。
程序计数器、Java虚拟机栈、本地方法栈这三个区域都是随着线程生而生、随着线程灭而灭。这几个区域不须要考虑太多垃圾回收的问题,方法执行完 了、线程结束了,内存天然就回收了。因此重点Java堆和方法区,他们是共享的区域,因此不会随着线程生灭,也有着不少不肯定性。
垃圾收集器在对堆进行回收以前,确定要判断对象实例是否还有没有用,若是在Java程序中一个对象没有任何做用,其天然就须要被回收。下面两种就是目前主流的方法
1.引用计数算法,在对象中添加一个引用计数器,每当一个对象被引用时,计数器加一,当引用失效时,计数器减一。计数器会0时,就是不可能再被使用了。这种算法不少应用都在使用,好比FlashPlayer、Python等。可是在Java中有个问题,引用计数算法却不能解决,就是循环引用。好比两个对象A和B互相引用,即便它们都为null了,或者都没用了,可是它们的计数器依旧不会0。可是Java虚拟机任然能够回收它们,说明Java虚拟机不是使用的这种算法。
2.可达性分析算法,经过一系列称为“GC Roots”的根对象做为起始节点集,注意是集,不仅是一个节点。从这些节点出发,走过的路径称为“引用链”,而在这条引用链上的对象,都是不须要回收的存活对象,而不在引用链上的就须要被回收。能看成GC Root的对象有不少种,主要有:全部已经被加载的类、线程当前栈帧的引用、同步锁持有的对象等。具体还要哪些能够看P70。当一个对象不在引用链上时,不表明必定就会被回收。一个对象被回收至少要被标记两次,第一次被标记后,会进行一次筛选,判断这个对象是否有必要执行finalize()方法,若是这个方法以前已经执行过一次,或者这个对象没有重写这个方法,那么就表明须要被回收。若是这个对象重写了finalize方法,且以前没有被执行过,他就会被放到F_Queue队列中,等待执行这个方法,这个方法是有时间限制的,防止执行太过缓慢对系统形成威胁。这时候,这个对象能够在finalize方法里完成自救,即把本身从新和引用链上的某个对象链接起来。
Java虚拟机规范没有强制要求这一区域的垃圾收集工做,主要缘由是性价比过低,即判断条件高,回收空间少。方法区主要是回收常量和再也不使用的类型。对于常量,若是任何对象都没有引用这个常量,那么它就能够回收了。对于一个类型是否须要回收,判断起来就要复杂许多:1.该类的全部实例被回收。2.加载该类的类加载器被回收。3.没有经过反射使用该类。
上一节说到了该如何判断哪些对象须要被回收,这一节讲讲该如何回收这些对象。在这以前须要了解一下分代收集理论,不少垃圾收集器就是基于这个理论去设计的。分代即把Java堆划分出几个不一样的区域,而后根据对象年龄分配到不一样的区域(年龄是指熬过垃圾收集器的次数)。主要有两个大区域,新生代和老年代。新生代区域的对象,会被频繁回收,若是在新生代熬过必定回收次数后,就会被放到老年代。这样划分的好处就是能够根据不一样区域对象的消亡特征设计不一样的垃圾收集算法。这就有了后面要说到的一些算法。可是还存在一个隐性的问题,就是跨代引用,即新生代和老年代之间存在引用,那么咱们以前说到的GC Roots就不得不包含一些老年代的对象了,加大了一些额外的开销。有一个跨代引用的假说,若是两个对象之间存在引用,它们应该是共存亡的,出现不一样代的引用是极少数的,因此也不必去为了那极少数,而加大一些额外开销。
具体的算法描述能够看书,P77开始。
1.标记-清除算法:经过可达性分析后,找出须要回收的对象,并标记上,对标记的对象进行回收。也能够反过来,标记不须要回收的对象,对没有标记的对象进行回收。主要有两个缺点:一个是执行效率不稳定,其执行效率是随着清楚对象的增长而下降的。一个是会产生空间碎片,由于回收的对象也许是零零散散的,致使空余内存空间也是零散的。
2.标记-复制算法:把内存空间分为两个相等部分,一次只用其中一个部分,每次把存活对象依次规整的放到另外一部分,而后再把已使用的那一部分总体回收。其最大的缺点就是将可用内存缩小了一半,空间浪费的太多了。后来IBM有项研究代表,新生代的对象98%熬不过第一次回收,因此针对这一特色,在标记-复制算法的基础上,把新生代分为一个Eden区、两个Survivor区。其内存占比为8:1:1。每次分配内存只使用一个Eden区和一个Survivor区。把这两个区的存活对象复制到另外一个Survivor区中,而后再清除Eden和已使用的Survivor。
3.标记-整理算法:这个算法通常用在老年代,标记过程和标记-清除算法同样,可是在清除阶段会把存活对象移到内存一端,而后直接清除到边界之外的内存。可是在移动过程当中会STW(即暂停用户线程)。整理和不整理都有好处和坏处,因此侧重点不一样,就有了Parallel Scavenge收集器和CMS收集器。
Serial(Serial Old)收集器:如同其名字同样,他是个单线程收集器,意味着它在工做时,用户线程必须中止,也就是常说的STW。在新生代中它采用的是标记-复制算法,在老年代中采用的是标记-整理算法。虽然这个线程是最基础、历史最悠久的收集器,可是相比较于其余单线程的收集器,它依旧是很是优秀的,在HotSpot虚拟机客户端模式下(Server启动慢,编译更彻底,编译器是自适应编译器,效率高,针对服务端应用优化,在服务器环境中最大化程序执行速度而设计;Client启动快速,内存占用少,编译快,针对桌面应用程序优化,为在客户端环境中减小启动时间而优化),新生代的默认收集器就是它。
ParNew收集器:只在新生代中,其只是Serial的多线程版本,其余的与Serial没有太多区别。依旧须要STW,它在新生代采用的是标记-复制算法。是服务端虚拟机新生代的首选收集器。
Parallel Scavenge(Parallel Old)收集器:Parallerl Scavenge是一款新生代收集器,采用的是标记-复制算法。它也是多线程的,可是与其余收集器不一样的是它更关注吞吐量(用户代码运行时间/用户代码运行时间+垃圾收集时间)。适合在后台运算而不须要太多交互的任务。Parallel Old是老年代版本,采用的是标记-整理算法。也是注重吞吐量的多线程垃圾收集器。
CMS收集器:老年代的收集器,注重的是减小STW的时间,基于标记-清除算法。其过程相比其余收集器更为复杂,大概分为四步:1.初始标记,根据GC Roots找到直接关联的对象。2.并发标记,根据初始标记阶段的对象找到更为完整的关联对象。3.从新标记,因为并发标记是和用户线程并发的,在这个过程不免会出现一些新的可回收对象。4.并发清理,因为采用的是标记-清除算法,不须要移动对象,因此能够和用户线程并发进行。虽然这是HotSpot追求低停顿时间的一次成功尝试,可是也有一些缺点,好比:并发清除阶段会产生新的垃圾、标记-清除算法产生的内存空间碎片、CMS默认的回收线程是(处理器核心数量+3)/4,对处理器敏感。
G1收集器:是一款主要面向服务端应用的垃圾收集器,做用于整个Java堆,是具备里程碑式意义的。虽然G1依旧保留新生代和老年代的几率,但它们再也不是固定的,而是把Java堆划分红多个大小相等的Region区,每一个Region区能够根据须要扮演新生代和老年代中的角色,总体来看,他采用的是标记-整理算法,可是在两个Region之间,采用的是标记-复制算法。用户能够设定收集停顿模型,会优先回收价值收益最大的那些Region。其工做过程分为初始标记、并发标记、最终标记、筛选回收。前三个阶段与CMS相似,筛选回收阶段会对各个Region的回收价值和成本排序,根据前面用户的设定来制定回收计划。除了并发标记阶段,其他三个阶段也是须要STW的。G1被称为里程碑式的设计一个重要缘由就是,设计者的思想从原来一次性把垃圾收集干净,到只是回收的速度比分配速度快就好了,这样在知足需求的状况下,性能也获得了很大的提高。
总结:从名字看,除了G1做用于整个Java堆,CMS做用于老年代,其他五款均可以根据名字判断出做用于老年代仍是新生代。根据以前垃圾收集算法的特色,老年代多用标记-整理算法,新生代多用标记-复制,除了CMS,其做用于老年代是标记-清除算法。而做用在服务端仍是客户端,因为服务器多核心CPU较为常见,因此多线程收集器用在服务端更好。
为了方便描述,首先定义一个三色标记,白色就是可达性分析中还未被标记的对象,若是从始至终都是白色那就是须要回收的对象。黑色就是已被标记,它的引用也被扫描过的对象。灰色就是已被标记,可是它的引用还未被扫描的对象。
在并发标记时,对象之间的引用可能会不停变更,当同时出现这两种状况时,原本在引用链上的对象会丢失:1.黑色对象增长了一个到对象A的引用。2.灰色对象删除了到对象A的引用。若是一个对象同时出现这两个状况就会丢失。为何要同时出现呢?由于已被标记的对象不会回头去检查,而正在被标记的对象若是引用发生变更会立刻生效。因此针对这两种状况,只要解决其中一条就不会出现对象丢失的问题。1.增量更新,破坏的就是第一条,黑色对象新增一个引用就会被记录下来,等并发标记结束后,再以记录的对象为根从新扫描一边。2.原始快照:破坏第二条。灰色对象删除一条引用就将这个灰色对象记录下来,并发标记结束后再以记录的对象为根,扫描一边。
垃圾收集器三个重要的指标:内存占用、吞吐量和延迟。内存占用和吞吐量随着硬件性能的提高,帮助了软件很多,不须要那么关注这两点,随着硬件的提高这两项指标也会随着提高。可是延迟不同,延迟也就是STW的时间,随着内存条的容量愈来愈大,Java堆可用的内存也愈来愈大,意味着须要回收的空间也愈来愈大,那么STW也就越久。
Shenandoah收集器:是一款非官方的垃圾收集器,是由RedHat公司开发的项目,受到来自Sun公司的排斥,因此在正式商用版的JDK中是不支持这个收集器的,只有在OpenJDK才有。虽然没有拥有正统血脉,可是在代码上它相较于ZGC更像是G1的继承者,在不少阶段与G1高度一致,甚至共用了一部分源码,但相较于G1又有一些改进。最主要有三个改进:
1.支持并发标记-整理算法。
2.默认不适用分代收集,Shennandoah和G1同样使用Region分区,可是在Shennandoah中并无Region会去扮演新生代或者老年代。
3.G1中存储引用关系的记忆集占用了大量的内存空间,在Shennandoah改用为链接矩阵,具体能够看P107。
Shennandoah收集工做过程大概能够分为9个步骤:
1.初识标记:与G1同样,标记处与GC Roots直接关联的对象,STW。
2.并发标记:与G1相同,根据上一步的对象,完整标记出可达对象。
3.最终标记:也与G1同样,利用原始快照的方法标记出上个阶段变更的对象,还会在这个阶段统计出回收价值最高的Region,组成一个回收集。
4.并发清理:这个阶段会清理整个Region区一个存活对象都没有的区域,因此能够并发进行。
5.并发回收:将回收集中存活的对象复制一份到其余未被使用的Region区中。
6.初始引用更新:并发回收阶段复制后,还需修正到复制后的新地址,但这个阶段并未作什么具体操做,只是至关于一个集合点,确保并发回收阶段全部线程都完成了本身的复制工做。
7.并发引用更新:这个阶段才是真正修正引用的阶段。
8.最终引用更新:上一步只是修正了Java堆中对象的引用,还要修正存在于GC Roots的引用,最后一次短暂的暂停,只与GC Roots数量有关。
9.并发清理:通过了并发回收的复制和引用修正,会收集中的Region就能够彻底清理了。
再说说Shennandoah的一个特色,也就是前面说到的并发标记-整理算法。整理阶段能够细分为5,6,7,8四个步骤。其最大的一个问题就是,在复制或者在修正引用的时候用户线程可能正在使用这个对象。原来有个解决相似问题的方案,就是保护陷阱,大概过程就是当用户线程访问到对象就地址后,会进入一个异常处理器中,由该处理器转发到新的地址。而在Shennandoah中用的是一种相对更好的方案:转发指针,就是在每一个对象前面加个新的引用字段,当不处于并发移动的状况下,该引用指向本身,并发移动了的话就指向新地址。
ZGC收集器:ZGC的目标和Shennandoah类似,都但愿在不影响吞吐量的状况下,将停顿时间限制在10毫秒之内。ZGC也是基于Region布局的,还并未支持分代收集,但其Region有大中小三个类型:
1.小型Region容量固定为2MB,用于放置小于256KB的小对象。
2.中型Region固定容量为32MB,用于放置大于等于256KB,小于4MB的对象。
3.大型Region容量不固定,但必定是2的整倍数,用于存放大于4MB的对象。
ZGC在实现并发整理时用到了染色指针,以前的的收集器若是想在对象中额外存储一些信息,大多会在对象头里存储,好比转发指针。再就是以前说到的可达性分析中的三色标记,其只是表达了对象引用的状况,跟对象自己的数据没任何关系,因此染色指针就是把这些标记信息记录在引用对象的指针上。指针为何还能存储信息呢?这就要说到系统架构了,具体看P114,染色指针只支持64位系统,而AMD64架构中只支持到了52位,而各大操做系统又有本身的限制,染色指针在Linux支持的46位指针宽度中,拿出4位存储这些标记信息,因此使用了ZGC进一步压缩了本来46位的地址空间,从而致使了ZGC能管理的内存不能超过4TB,在今天看来,4TB的内存依旧很是充足。
染色指针的三大优点:
1.一旦某个Region的存活对象被移走后,这个Region当即就能被回收从新利用,而Shennandoah须要一个初始引用更新,等待全部线程复制完毕。
2.染色指针能够大幅度减小在垃圾收集过程当中内存屏障的使用数量(后面过程当中的第五步提到),一部分功能就是由于染色指针把信息存储在指针上了,还有一部分缘由就是ZGC还并未支持分代收集,因此也不存在跨代引用。
3.染色指针在将来能够拓展,记录更多信息,前面说到在64位系统中,Linux只用到了46位,还要18位未被开发。还有一个问题就是染色指针从新定义指针中的几位,操做系统 是否支持,虚拟机也只是一个进程而已,这里就用到了虚拟内存映射,具体看P116。
ZGC工做过程大概能够分为如下几步:
1.初始标记:与以前几个收集器同样,找到GC Roots的直接关联对象。
2.并发标记:标记出完整的可达对象,与G1和Shennandoah不一样的是,它是在指针上作更新而不是对象头。
3.最终标记:和Shennandoah同样。
4.并发预备重分配:这个阶段须要根据特定的查询条件统计出本次收集过程要清理哪些Region。这里的分配集不是像G1那样按收益优先的回收集,分配集只是决定了里面的对象会被复制到新的Region,这里的Region要被释放了。
5.并发重分配:这个过程要把分配集中的对象复制到新的Region中,并为分配集中的每一个Region维护一个转发表,得益于染色指针的帮助,能够仅从引用上就能够得知某个对象是否在分配集上,若是在复制时,一个用户线程访问了分配集中的对象就会被内存屏障截获,而后根据转发表将访问转发到新的对象上,并修正这个线程访问该对象的引用,这个过程称为指针的自愈。
6.并发重映射:这个阶段要修正整个堆中指向重分配集中旧对象的全部引用。这个阶段比较特殊,由于它不是迫切须要去执行的,上个阶段的自愈过程就是针对某一对象的引用修正,因此即便没有这一步也不会出现问题,只是第一次自愈有个转发过程会稍慢一点,后面也都正常了。正由于这种不迫切,ZGC巧妙的把这步工做合并到了并发标记过程中,由于并发标记也须要遍历全部对象,这一步也须要修正全部旧对象的引用。
ZGC的一大问题就是其暂时尚未分代收集,这限制了它能承受的对象分配速率不会过高。若是长时间的回收速率比不上分配速率,产生的浮动垃圾愈来愈多,可分配的空间也愈来愈小了。因此要从根本上解决这个问题仍是要引入分代收集,让新生代专门去存储这些频繁回收建立的对象。
一个类从被加载到卸载出内存,一共要通过七个阶段:加载-链接(包括:验证-准备-解析)-初始化-使用-卸载。
一个类何时进行加载并无强制约束,可是初始化有且只有六种状况下才能进行,如使用new关键字实例化对象时、读取一个静态字段时、经过反射调用一个类时、子类要初始化时父类必须也初始化等,详细的见P264.总结来讲,就是对类型的主动引用,才会去进行初始化。用到的时候才去初始化,这也符合咱们的正常思惟。固然初始化前确定要进行前面几步,可是何时加载是没有限制的。
1.加载:这个加载是整个流程的第一步,与标题的类加载不是同一个意思。这一步主要作三件事:1.1获取此类的二进制字节流。1.2将字节流中表明静态存储结构转化为方法区的运行时数据结构。1.3生成一个表明这个类的Class对象,做为方法区这个类各个数据的访问入口。Java虚拟机规范并无对这三件事作很严格的要求,好比获取二进制字节流,并无要求必定要从Class文件中获取,因此有了如今的jar包、war包等从压缩文件中读取。也能够从其余文件里读取,好比jsp文件。
2.验证:这个阶段很是重要,工做量也在整个流程当中占至关大一部分。这一阶段要确保字节流中的信息符合规范要求,不存在危害虚拟机的代码。若是仅在Java代码层面,是很难作出不合规范的操做,好比访问数组边界外的数据等等,编译器都会抛出异常,拒绝编译。整个阶段主要有如下四个检验动做:文件格式验证、元数据验证、字节码验证、符号引用验证。文件格式验证:验证字节流是否符合Class文件格式的规范,好比是否以魔数0xCAFEBABE开头、代码版本号是否在当前虚拟机接受范围内等。元数据验证:主要是对类的元数据信息进行语义验证,保证要符合Java语言规范。好比这个类是否有父类,除了Object,每一个类都应该有父类等。字节码验证:对方法体进行校验,保证方法在运行时不会危害虚拟机,如保证在任什么时候候都不会跳到方法体之外的字节码指令上等。符号引用验证:这个验证主要是确保解析行为能正常执行,这个验证会发生在解析阶段,即符号引用转为直接引用,如检查符号引用中经过全限定名是否能找到对应的类等。
3.准备:这个阶段是为类变量分配内存(方法区)和设置初始值。注意这里说的是类变量,即被static修饰的,而不包括其余的变量,其余的变量会在这个类实例化时随着这个类的对象一块儿分配。还有一点就是,初始值是零值,也就是一些基本数据类型的默认值,好比int是0,即便语句以下:static int value=123;初始化是0,而不是123.除非是常量,如static final int value=123,那么它的初始值就是123.
4.解析:这个阶段是将常量池内的符号引用替换为直接引用。符号引用:用一组符号来描述所引用的目标,能够是任何形式的字面量,只要能定位到目标便可。直接引用:能够是直接指向目标的指针,或者一个句柄,总之就是能直接定位到目标,并且只要有了直接引用,那么虚拟机内存中必定就有该引用目标。
5.初始化:是类加载的最后一步,在准备阶段时已经为变量设置了初始零值,这个阶段会根据程序代码初始化变量和其余资源。这个阶段才从咱们编码角度进行真正的初始化。初始化阶段其实就是执行类构造器<clinit>()方法的过程。这个方法并非由程序员去编写的,而是Javac编译器自动生成的。是由编译器自动收集类中全部类变量的赋值动做和静态语句块中的语句合并生成的,收集顺序就是代码的编写顺序。所以<clinit>()方法也不是类和接口必须的,若是一个类中没有类变量的赋值语句和静态语句块,也就不会有这个方法了。若是一个类生成了这个方法,即便是多个线程同时调用了,执行<clinit>()也只能有一个线程,其余线程会被阻塞。
Java虚拟机的设计团队有意把类加载阶段中获取一个类的二进制字节流这个动做放到Java虚拟机外部去实现。而实现这个动做的代码被称为“类加载器”。在Java虚拟机的角度看,只有两种两种不一样的类加载器:1.启动类加载器,使用C++实现,是Java虚拟机的一部分。2.其余全部类加载器,这个类加载器由Java语言实现,独立存在Java虚拟机以外,而且要所有继承启动类加载器。可是在Java开发人员的角度看,类加载器应该被分的更细一点,自JDK1.2以来Java一直保持这三层类加载器、双亲委派的类加载架构。下面说到的是JDK8及以前版本的三层类加载器和双亲委派模型。
启动类加载器:这个类加载器负责加载存放在JAVA_HOME\lib目录、或者被-Xbootclasspath参数指定的路径的类库。而且是Java虚拟机可以识别类库,即按名称识别,若是名称不符合要求,即便在这个目录中也不会被加载。
扩展类加载器:这个类加载器是Java代码实现的。负责加载<JAVA_HOME>\lib\ext目录中、或被java.ext.dirs系统变量指定的路径中全部的类库。就如其名,用户能够将一些通用的类库放到ext目录中,以拓展JAVA SE的功能。
应用程序类加载器:负责加载ClassPath即用户路径下全部的类库,若是没有自定义的类加载器,通常状况下,这将是默认的类加载器。
加上用户自定义的类加载器,各个类加载的协做关系一般如P283的图所示,即从自定义类加载器——>应用程序类加载器——>扩展类加载——>启动类加载。层层递进。而这种关系,被称为类的双亲委派模型。
双亲委派模型的工做过程以下:一个类加载器收到加载一个类的请求后,本身不会去加载,而是请求委派给父类加载器,如上箭头同样,层层递进,直到父类没法完成这个加载请求时(它的执行目录下没找到该类)本身才会去加载。
这么作的好处就是,若是用户本身编写一个与Java类库中重名的Java类,好比Object类,而各加载器都各自加载,那系统中就会出现多个不一样的Object类,后果确定是混乱的。有了双亲委派模型后,Object类会一直被委派到启动类加载器中去执行,若是这个时候用户再写一个Object类,最后也到达启动类加载器时,会先根据类名判断这个类是否被加载过,若是被加载过就再也不加载。能够看P284双亲委派模型的实现源码,第一句就是根据name判断是否加载过,很好的杜绝了上面的那个问题。
首先要明白一点,这里所说的Java内存模型与前面说到的Java堆、Java栈等不是一个层次的对内存的划分。Java堆等区域是Java虚拟机所管理的内存中运行时的数据区域。其实这全部的划分在物理机角度上是不存在的,只是逻辑上的划分,是Java虚拟机为了方便管理内存而设计的,就像Java堆里还分为老年代和新生代。看完前面的章节知道了运行时的几个数据区域的做用很是多也很是重要,而Java内存模型的主要目的是为了定义程序中各类变量的访问规则。不一样的硬件和操做系统它们对内存的访问规则均可能有所不一样,为了屏蔽这种差别,就有了Java内存模型。
上面说到的对变量的访问规则,这里的变量并非指代码里面的全部变量,而是包括成员变量、类变量和构成数组对象的元素。不包括局部变量、方法参数。由于后者是线程私有的,不存在竞争问题。Java内存模型主要分为主内存和工做内存,上面规定的变量都存在主内存中,每条线程有本身私有的工做内存。工做内存中保存着该线程当前操做的变量在主内存中的副本。线程操做变量时,会从主内存复制一份到本身的工做内存,修改完后再把新值赋值到主内存中的变量。
内存间的交互:Java内存模型定义了8种操做来完成,这8种操做都是原子性的。
1.lock:做用在主内存,标识一个变量为线程独占。2.unlock:做用于主内存,把线程独占的变量释放出来。
3.read:做用于主内存,把一个变量的值传输到线程的工做内存。4.load:做用于工做内存,把read到的值放入到副本中。
5.use:做用于工做内存,把工做内存中变量的值传给执行引擎。咱们在获取一个变量的值时就是这个操做。
6.assign:把执行引擎传来的值赋值给工做内存中的变量。咱们给一个变量赋值时就是这个操做。
7.store:把工做内存中变量的值传给主内存。8.write:把store传来的值放入到主内存对应的变量中。
除了这8种操做,还有一些对这8种操做的规定。如lock标识的变量其余线程不能使用。上面的原子操做每每是须要两个一块儿配合才能完成一个完整的步骤的,因此还有些规则规定这些原子操做间的配合不能不合逻辑,有冲突。如read后不load,assign后就无论了等。详情看P443。
针对volatile修饰的变量的特殊规则:
volatile有两个做用:1.volatile变量对全部线程是当即可见的,即volatile变量的全部操做都能当即反映到其余线程之中。这是普通变量不具有的,普通变量被一个线程修改后,必需要被该线程传回主内存,而其余线程必须读取主内存中这个变量后才知道这个变量改变了。2.禁止指令重排序优化,指令重排序即处理器会把多条指令分发给不一样的电路单元进行处理,有时候这种处理顺序不必定是程序上的顺序,但不会打乱有先后关联的两个指令。好比一个变量A,第一条指令是A+10,第二条指令是A*2,第三条指令是B-3,显然第一条指令和第二条指令不能打乱顺序,而第三条指令跟它们没有任何关联,因此是放在它们前面仍是后面都没有影响。
因此这些特殊规则也都是为了知足上述的volatile的两个做用。好比线程use一个变量前必须load,这就是为何volatile变量是当即可见的;线程执行了assign,才能执行store,这是为了保证每次修改都能同步到主内存中,才能保证其余线程能当即看到改变。详情见P449.
最后一点内容与线程有关,但提到的并非不少。关于线程的笔记会在《Java并发编程的艺术》中再记。该篇笔记总计一万两千字左右,在看完整本书后,做为理论部分的复习笔记也是不错的。若是之后对Java虚拟机有更深入或者其余的理解,也会随时更新到这个笔记中。