- 内存动态分配与内存回收技术已经相当成熟,看起来进入了自动化的时代,为什么还要去了解垃圾收集和内存分配?
- 当需要排查各种内存溢出、内存泄漏问题时
- 当垃圾收集成为系统达到更高并发量的瓶颈时
- 我们就必须对内存动态分配与垃圾收集技术实施必要的监控与调节。
A = B :A引用B,A依赖于B
线程私有,生命周期和线程一致
描述的是java方法执行的内存模型,即栈帧;
方法在执行时会创建一个栈帧,存储的是局部变量表、操作数栈、动态链接、方法出口等信息。
方法从执行到结束对应着一个栈帧从虚拟机栈中入栈到处出栈的过程。
局部变量表:存储的是编译器可知的各种基本数据类型、对象引用类型和返回地址类型
可能会出现两种异常:
与虚拟机栈的作用相似。区别在于虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机执行本地方法服务。本地方法指的是不是由java代码编写的方法。也就是用native修饰的方法,比如底层是c或c++实现的方法。
有的虚拟机(如 HotSpot 虚拟机)把本地方法栈和虚拟机栈合二为一。
和虚拟机栈一样,本地方法栈也会出现 StackOverflowError 和 OutOfMemoryError 两种异常。
堆内存中对象的存储布局可划分为三个部分:
1、对象头 2、实例数据 3、对齐填充
程序中通过栈上的reference数据来操作堆上的具体对象。具体实现是由虚拟机完成的,主流的访问方式主要有句柄和直接指针两种:
在java运行时数据区域中,只有程序计数器不会发生OutOfMemoryError异常。
Java堆溢出异常时实际应用中最常见的内存溢出异常情况。
通过程序测试:无限循环新建对象
查看java堆溢出异常信息:通过参数设置让虚拟机在出线内存溢出异常时Dump出当前的内存对转储快照
通过内存映像分析工具对Dump出来的堆转储快照进行分析,一般是有两种情况:内存泄露与非内存泄漏
虚拟机栈和本地方法栈能抛出两种异常:
线程请求的栈深度大于虚拟机所允许的最大深度,简单来说就是虚拟机栈容量不够了,将抛出StackOverflow异常;
通过程序测试:运行无终止条件的递归函数。在HotSpot虚拟机上会显示栈溢出异常。
如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,简单来说就是栈帧太大了,将抛出OutOfMemoryError异常。
通过程序测试:在方法中定义大量的局部变量+递归调用此方法。因为HotSpot虚拟机不支持栈内存动态扩展,所以在HotSpot虚拟机上依旧会抛出内存溢出OutOfMemoryError异常。而支持栈内存动态扩展的Claasic虚拟机就会抛出内存溢出OutOfMemoryError异常。
但是HotSpot虚拟机也不是不会抛出OutOfMemoryError异常,当创建的线程很多时,则会抛出此异常,但此异常和栈空间大小不存在直接关系,取决于操作系统的内存使用状态。
例如:32位的Windows操作系统单个进程最大内存限制位2GB,除去给方法区、堆分配的内存,程序计数器消耗内存很小可忽略,剩下的内存就由虚拟机栈和本地方法栈来分配了。如果为每个线程分配到的栈内存越大,可以建立的线程数量自然就越少。容易把剩下的内存耗尽。
所以在开发多线程应用时,可以通过减少最大堆和栈容量来换取更多的线程,避免内存溢出;当然,还可以将32位更换成64位操作系统。
运行时常量池是方法区的一部分。String::intern()是一个本地方法,返回字符串常量池中等于此String对象的字符串应用,如果字符串常量池中已经有了该对象,则直接返回引用;如果没有,则先添加到字符串常量池中再返回引用。
通过程序测试:在方法中创建数值,准换成String类型并调用intern()方法
方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、字段描述和方法描述等。对于这部分区域的测试,基本思路是
通过程序测试:运行时产生大量的类,使用CGLIB直接操作字节码运行时产生大量的动态类
通过程序测试:调用Unsafe::allocateMemory()方法
Java内存运行时区域中的程序计数器、虚拟机栈和本地方法不需要过多考虑内存回收的原因:
1、这三个区域随线程而生,随线程而灭。
2、每一个栈帧分配多少内存在类结构确定下来时就已知
因此,这三个区域的内存分配与回收都具备确定性,不需要过多考虑内存回收的问题。
然而,java堆和方法区的内存分配与回收有着很显著的不确定性:
1、一个接口的多个实现类需要的内存可能会不一样 ??
2、一个方法所执行的不同条件分支所需要的内存也可能不一样 ??
因此,只有处于运行期间才能知道程序会创建哪些对象,创建多少个对象,这部分内存分配与回收是动态的,垃圾收集器所关注的也是此部分该如何管理
在垃圾收集器对堆进行回收前,需要判断对象中哪些“存活”,哪些“死去”;“死去”即不可能再被任何途径使用的对象
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
此时,objA和objB都为空,但是引用计数器的值不为零,无法被回收
当前主流的商用程序语言(Java、C#)的内存管理子系统都是通过可达性分析算法来判定对象是否存活。
前面的两种方式判断存活时都与‘引用’有关,一个对象只有“被引用”和“未被引用”两种状态,但是 JDK 1.2 之后,引用概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用,下面四种引用强度一次逐渐减弱,下面具体介绍。
即使在可达性分析算法中不可达的对象,也并非是“facebook”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。
总结:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
finalize() 方法只会被系统自动调用一次。
finalize() 方法并不等同于C、C++中的析构函数,运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐使用,使用try-finally更好。
在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而方法区(HotSpot虚拟机中的元空间或者永久代)的垃圾收集效率远低于此。
永久代垃圾回收主要两部分内容:废弃的常量和无用的类。
对于常量的回收:与java堆中回收对象非常类似,例如:当前系统没有一个字符串对象的值是“java”,即没有任何字符串对象引用常量池中的“java”常量,那么就会被垃圾收集器回收。
对于类的回收:需要满足以下三个条件
1、该类的所有实例都已经被回收,也就是java堆中不存在该类及其任何派生子类的实例
2、加载该类的类加载器已经被回收
3、该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
注:满足上述三个条件仅仅是“被允许”,是否要被回收需要根据虚拟机参数的控制来判断。
关于判定对象消亡,垃圾收集算法可划分为“引用计数式垃圾收集”和“追踪式垃圾收集”两大类,但是在主流的Java虚拟机中用的都是后者,因此本节讨论的是追踪式垃圾收集算法。
当前的垃圾收集器(垃圾收集算法)都遵循了“分代收集”理论,名为理论,实则经验法则,建立在两个分代假说之上:
1、弱分代假说:绝大数对象都是朝生夕灭的;因此将java堆划分出新生代,新生代收集:重点关注如何保留少量存活而不是去标记将要回收的对象
2、强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡;因此有老年代,老年代收集:低频率回收
分代的缺点:对象不是孤立的,对象之前存在跨代引用,例如在新生代中扫描GC roots的路径,还得遍历整个老年代的所有对象是否存在跨代引用。因此添加了第三条假说:
3、跨代引用假说:跨代引用相对于同代引用仅占少数;基于此假说,不必为了少量的跨代引用去扫描整个老年代,可以在新生代中建立一个数据结构把老年代分块,标识出哪一块内存会存在跨代引用。这样就不用对整个老年代进行扫描。
简介:算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象;或者反过来,标记存活的对象,统一回收未被标记的对象。
存在两个主要缺点:
简介:1969年第一次称为“半区复制”的垃圾收集算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
优点:顺序分配内存,移动对顶指针。不会产生内存碎片
主要存在两个缺点:
简介:现在的Java虚拟机大多都采用了这种收集算法去回收新生代,新生代中的对象98%熬不过第一轮收集(IBM公司曾做过一项研究),所以不需要按照1:1来划分新生代的内存空间
1989年提出了一个更优化的半区复制分代策略,HotSpot虚拟机按照8:1:1的比例划分Eden:Survivor:Survivor区,内存可分配空间占90%,剩下10%作为复制区,当复制区的内存不足以存储存活下来的对象,需要依赖其他区域存储(例如老年代)进行分配担保。
对于标记—复制算法,对象存活率较高则会有较多的复制操作,效率会降低。因此老年代不适合选用这种算法。
简介:标记过程与标记—清除算法一样,后续步骤是让所有存活对象都向内存空间一段移动,然后直接清理掉边界以外的内存。
优点:无需浪费空间
缺点:移动存活对象,需要暂停用户应用程序
优缺点:移动内存回收时更复杂,不移动内存(标记—清除算法)则内存分配时会更复杂。从整个程序的吞吐量来看,移动对象会更划算。
吞吐量的本质是:赋值器与收集器的效率总和,不移动对象会使收集器效率增加,但内存分配和访问比垃圾收集频率要高得多,所以这部分耗时增加,总吞吐量仍然是下降的。
多数时间使用标记—清除算法,知道产生的内存碎片化程度大到影响对象分配时,再用标记-整理算法收集一次以获得更连续规整的内存空间。
因此,在 HotSpot 中采取了空间换时间的方法,使用 OopMap 来存储栈上的对象引用的信息。
在 GC Roots 枚举时,只需要遍历每个栈桢的 OopMap,通过 OopMap 存储的信息,快捷地找到 GC Roots。
用OopMap,可能会发生的情况是:引用关系变化导致OopMap存储的内容变化,如果为每一条指令设置OopMap开销则很大
优化:OopMap是只在特定的位置记录信息,将这些位置设成安全点,强制要求程序必须执行到安全点后才能执行暂停(之前暂停是基于垃圾收集时刻,垃圾收集则会暂停,安全点解决的是如何停顿用户线程的问题),安全点的选取是以“能否具有让程序长时间执行的特征”为标准选取的,例如指令序列的复用(方法调用、循环跳转、异常跳转);
那么如何保证垃圾收集发生时让所有线程跑到最近的安全点:
1、抢先式中断:垃圾收集发生时,所有线程中断,如果有用户线程中断的地方不在安全点上,就恢复执行直到跑到安全点上再中断
2、主动式中断:垃圾收集发生时,设置标志位,各线程在执行过程中不断轮训标志,如果发现中断标志为真,那么就在最近的安全点上主动中断挂起
用安全点的设计方案,可能会发生的情况是:处于Sleep或者Blocked状态的线程无法走到安全点去中断挂起自己。
优化:将安全点扩展拉伸成安全区域,确保安全区域中引用关系不会发生变化,从任意地方开始垃圾收集都是安全的
程序进入安全区域,标识自己进入安全区域,离开时如果根节点枚举完成后,如果完成,则没事发生;如果没完成,一直等待,直到收到可以离开安全区域的信号为止。
通过写屏障技术来维护卡表状态,一旦收集器在写屏障中增加了更新卡表操作,虚拟机就会为所有赋值操作生成相应的指令,每次只要对引用进行更新就会更新卡表状态
可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能进行分析,这意味着必须全冻结用户线程的运行;
但如果用户线程和收集器并发工作呢?收集器在对象图上标记颜色,用户线程修改引用关系相当于修改对象图的结构,可能会导致:
1、原本消亡的对象错误标记为存活;可容忍
2、原本存活的对象错误标记为消亡;程序肯定会发生错误
初始阶段,只有GC ROOTS节点是黑色的
如何解决?
1、增量更新:当黑色对象插入新的指向白色对象的引用关系时,将插入的引用记录下来,等并发扫描结束之后,将记录引用关系中的黑色对象为根节点,重新扫描一次。可以理解为:黑色对象插入新指向白色对象的引用之后,就变成了了灰色对象
2、原始快照:当灰色对象删除指向白色对象的引用关系时(相当于给黑色对象提供了白色对象),将删除的引用记录下来,等并发扫描之后,将记录引用关系中的灰色对象为根节点,重新扫描一次(这时灰色对象和白色对象的引用关系还在,因为做了原始快找保存)。
没有最好的垃圾收集器,也不存在万能的收集器,有的只是在具体应用场景下最合适的收集器。衡量收集器的三项最重要指标是:内存占用吞吐量和延迟,三者形成了“不可能三角”,一款收集器不可能在这三个方面都有优秀的表现,通常最多可以同时达成其中的两项。
优点:
缺点:
适用场景:
适用场景:
适用场景:
补充:
在谈论垃圾收集器的上下文语境中,并行和并发可以理解为:
适用场景:
缺点:
适用场景:
特点:
怎么保证时间停顿模型可靠?通过获得“衰减平均值”:统计可测量的数据,得到预测值。多次预测取平均,统计状态越新平均值约新,预测的停顿时间越准确
优点:
缺点:
适用场景:
Shenandoah 收集器和 ZGC 收集器;在任意可管理的堆容量下,实现垃圾收集的停顿不超过10毫秒。但是这两款目前仍处于实验状态。
特点:
特点:
Java技术体系的内存管理,根本上解决的是两个问题:1、自动给对象分配内存; 2、自动回收分配给对象的内存
前面垃圾收集器体系以及运作原理已经介绍了自动内存回收原则,下面通过Serial + Serial Old收集器重点讲解若干最基本的内存分配原则
字节码文件和Java虚拟机既是构成平台无关性的基石,也是语言无关性的基石。
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类会可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
符号引用:符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。符号引用与虚拟机实现的内存布局无关,引用的目标并不必定已经加载到了内存中。
直接引用:直接引用能够是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不一样虚拟机实例上翻译出来的直接引用通常不会相同。若是有了直接引用,那说明引用的目标一定已经存在于内存之中了
Class文件的结构时,会有这样几个概念,字面量、全限定名(Fully Qualified Name)、简单名称(Simple Name)和描述符(Descriptor)和符号引用
public class TestClass {
private int m;
public int inc() {
return m+1;
}
}
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被Java虚拟机直接使用的Java类型,这个过程叫做类加载机制
这六种场景中的行为成为对一个类型进行主动引用,出此之外所有引用类型的方式都不会触发初始化,称为被动引用,例如以下场景:
在加载阶段,Java虚拟机需要完成以下三件事情:
1、通过一个类的全限定名来获取定义此类的二进制字节流
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口
在1中,该规则没有指明从哪里获取、如何获取class文件,因此,开发人员可以在这个空隙上大做文章,许多举足轻重的Java技术都建立在这一基础之上,例如:
1、从ZIP压缩包中读取,最终成为日后JAR、EAR、WAR格式的基础;
2、从网络中获取,这种场景最典型的就是Web Applet;
3、运行时计算生成,这种场景使用得最多的就是动态代理技术;
4、由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件
5、…
相对于类加载过程的其他阶段:
1、 非数组类型的加载阶段(准确地说,是加载阶段中获取二进制字节流的动作)是开发人员可控性最强的阶段。加载阶段既可以使用java虚拟机内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过自定义的类加载器去控制获取字节流的获取方式(重写类加载器的findClass()或loadClass()方法)
2、 数组类的加载阶段,情况有所不同,数组类本身不通过加载器创建,它是由Java虚拟机直接在内存中动态构造出来,但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程遵循以下规则:
(1)如果数组的组件类型是引用类型,那就递归采用类加载加载。
(2)如果数组的组件类型不是引用类型(例如int[] 数组),Java 虚拟机会把数组标记为引导类加载器关联。
(3)数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。
加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区中了。
验证是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息当做代码不会危害虚拟机自身的安全。验证阶段大致上会完成下面四个阶段的校验动作:
文件格式验证
例如:
1、是否以魔数0×CAFEBABE开头;
2、主、次版本号是否在当前Java虚拟机接受范围之内;
3、常量池中的常量中是否有不被支持的常量类型 等等
该阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合一个Java类型信息的要求。通过该阶段之后,字节流才被允许进入Java虚拟机内存的方法区中进行存储,因此这部分也是要在加载阶段之前完成;后面的三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。
元数据验证
例如:
1、这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类);
2、这个类的父类是否继承了不允许被继承的类(被final修饰的类);
3、如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法;
4、类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
5、 …
该阶段的主要目的是对类的元数据信息进行语义校验,保证符合《Java虚拟机规范》
字节码验证
第三阶段是验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。该阶段主要是对类的方法体进行校验(Class文件中的Code属性)分析,确保被校验类的方法在运行时不会做出危害虚拟机的行为,例如:
1、保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作;(例如:不会出现类似于“在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况)
2、保证任何跳转指令都不会挑战转到方法体意外的字节码指令上
3、保证方法中类型转换总是有效的,(例如:可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫不相干的一个数据类型,则是危险和不合法的)
4、 …
符号引用验证
发生的时机是在虚拟机将符号引用转化为直接引用的时候,这个转化动作是发生在连接的第三阶段——解析阶段,符号引用验证可以看作是对类自身以外(常量池中的各类符号引用)的各类信息进行匹配性校验,通俗来说就是该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源,例如:
1、符号引用中通过字符串描述的全限定名是否能找到对应的类
2、在指定类中是否符合方法的字段描述符及简单名称所描述的方法和字段
3、符号引用中的类、字段、方法的可访问性(private、protected、public、)
总结:验证阶段非常重要但不是非必要的,如果程序运行的全部代码都已经被反复使用和执行过,那么就可以设置参数来关闭大部分类的验证措施,以缩短虚拟机类加载的时间
准备阶段是正式为静态变量分配内存并设置变量初始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中分配内存。并且这里所说的初始值“通常情况下”是数据类型的零值
int 0
boolean false
long 0L
short (short)0
float 0.0f
double 0.0d
reference null
byte 0
char '\u0000'
public static int value = 1127;
这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 value 赋值为 1127 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,所以初始化阶段才会对 value 进行赋值。
public static final int value = 1127;
编译时Javac将会为value生成 ConstantValue属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 1127。
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程;简单来说,在解析阶段,Java虚拟机会把符号引用替换为一个指针,该指针指向目标在方法区的内存位置
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型。
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,(相当于嵌套解析,但是解析的目标是类型是D还是C?)那虚拟机完成解析过程包括以下3个步骤:
1、如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出线了任何异常,解析过程就将宣告失败
2、如果C是一个数组类型,并且数组类型为对象,那将会按照第一个点的规则加载元素类型,接着由虚拟机生成一个代表该数组维度和元素的数组类型
3、如果上面两步没有出现任何异常,那么C在虚拟机中实际上已经成为了一个有效的类或接口了,但在解析完成前还要进行符号引用验证,确认D是否具备对C的访问权限,必须满足以下3条规则中的一个:
1)被访问类C是public,并且与访问类D处于同一个模块
2)被访问类C是public,不与访问类D处于同一个模块,但是被访问类C的模块允许访问类D的模块进行访问
3)被访问类C不是public,但是它与访问类D处于同一个包中
Java中字段包括类级变量和实例级变量,不包括局部变量;字段可以包括的修饰符有:访问权限修饰符,类变量还是实例变量(static)、可变性(final)、并发可见性(volatile,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本数据类型、对象、数组)、字段名称
要解析一个未被解析过的字段符号引用,首先会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用进行解析,如果解析完成,那么把这个字段所属的类或接口用C表示,按照以下步骤对C进行后续字段的搜索:
1、如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
2、否则,如果在C中实现了接口,将会按照继承关系向上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
3、否则,如果在C不是java.lang.Object的话,将会按照继承关系向上递归搜索其父类,如果父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
4、否则,查找失败,抛出java.lang.NosuchFieldError异常
最后,对字段进行权限验证,如果不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常
要解析一个未被解析过的方法符号引用,首先会对方法表内class_index项中索引的方法所属的类或接口的符号引用进行解析,如果解析完成,那么把这个方法所属的类或接口用C表示,按照以下步骤对C进行后续方法的搜索:
1、由于Class文件格式中的类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类的方法表中发现class_index中索引的C是个接口的话,那就直接抛出java.lang.IncompatibleClassChangeError异常
2、如果通过了第一步,在类C中查找是否有简单名称和描述符斗鱼目标相匹配的方法,如果有,则返回这个方法的引用,查找结束。
3、否则,如果在C不是java.lang.Object的话,将会按照继承关系向上递归搜索其父类,如果父类中包含了简单名称和字段描述符都与目标相匹配的方法,则返回这个方法的直接引用,查找结束
4、否则,在类C实现的接口列表及它们的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时候查找结束,抛出java.lang.AbstractMethodError异常
5、否则,查找失败,抛出java.lang.NosuchMethodError异常
最后,对方法进行权限验证,如果不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常
要解析一个未被解析过的方法符号引用,首先会对接口方法表class_index项中索引的方法所属的类或接口的符号引用进行解析,如果解析完成,那么把这个方法所属的类或接口用C表示,按照以下步骤对C进行后续方法的搜索:
1、由于Class文件格式中的类的方法和接口的方法符号引用的常量类型定义是分开的,如果在接口方法表中发现class_index中索引的C是个类而不是接口的话,那就直接抛出java.lang.IncompatibleClassChangeError异常;
2、否则,在接口C中查找是否有简单名称和描述符斗鱼目标相匹配的方法,如果有,则返回这个方法的引用,查找结束。
3、否则,在接口C的父接口中递归查找,直到java.lang.Object类(接口方法的查找也会包括Object类中的方法)为止,是否有简单名称和描述符都与目标相匹配的方法,如果有,则返回这个方法的引用,查找结束。
4、对于规则3,由于java的接口允许多重继承,如果C的不同父接口中存有多个简单名称和描述符都与目标相匹配的方法,那将会从这多个方法返回其中一个并查找结束
5、否则,宣告方法查找失败,抛出java.lang.NosuchMethodError异常
在JDK9之前,Java接口中的所有方法都默认是public,也没有模块化约束,所以不存在访问权限的问题;但在JDK9中增加了接口的静态私有方法,也有了模块化访问的约束,所以从JDK9起,接口方法的访问也可能存在因访问权限控制而出现java.lang.IllegalAccessError异常
public class Test {
static {
i = 0; // 给变量复制可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
2、clinit()方法与类的构造函数不同,它不需要显示地调用父类构造器,Java虚拟机保证在子类的()方法执行前,父类的()方法已经执行完毕。因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object
3、由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如下面代码所示,字段B的值将会是2而不是1
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
4、()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成()方法
5、接口中不能使用静态语句块,但任然有变量初始化的复制操作,因此接口与类一样都会生成()方法。但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法,因为只有当父类接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的()方法
6、Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的()方法,其他下线程都需要阻塞等待,直到活动线程执行完毕()方法。
需要注意,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒后则不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会被初始化一次。
在类加载阶段中,类加载器可以通过一个类的全限定名来获取描述该类的二进制字节流,这个动作在虚拟机外部实现,以便让程序自己去决定如何获取所需的类
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
启动类加载器(Bootstrap ClassLoader):前面已经大致介绍过了,这个类加载器负责将存放在
扩展类加载器(Extension ClassLoader):这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将
应用程序类加载器(Application ClassLoader):这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般也称为“系统类加载器”。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由上述这三种类加载器互相配合从而实现类加载,如果有必要,还可以加入自己定义的类加载器:来扩展获取Class文件的方式
简单总结两个优点:
1、因为双亲委派是向上委托加载的,所以它可以确保类只被加载一次,避免重复加载;
2、Java的核心API都是通过引导类加载器进行加载的,如果别人通过定义同样路径的类比如java.lang.Integer,类加载器通过向上委托,两个Integer,那么最终被加载的应该是jdk的Integer类,而并非我们自定义的,这样就避免了我们恶意篡改核心包的风险;
作者:一个程序员的成长
链接:https://www.zhihu.com/question/315563427/answer/1807406721
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
未详述
上文提到过双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。
Java9 模块化系统
未详述
并发处理的广泛应用是Amadahl定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类压榨计算机运算能力的最有力武器。
Amadal定律:通过系统中并行化与串行化的比重来描述多处理器系统能获得的运算加速能力;
摩尔定律:用于描述处理器晶体管数量与运行效率之间的发展关系
这两个定律的更替代表了近年来硬件发展从追求处理器频率到追求多核心并行处理的发展过程
注:此处的变量指的是实例字段、静态字段和构成数组的对象的元素,不包括局部变量和方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
这里的主内存、工作内存与Java内存区域中的Java堆、栈、方法区等并不是同一个层次的对内存的划分,这两者基本没什么关系;
如果一定要对应起来,那么主内存主要对应于Java堆中的实例数据部分,而工作内存则对应与虚拟机占中的部分区域。
从更基础的层次上说,主内存直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问的是工作内存
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8中原子操作来完成: