JVM
2021-09-17 11:11:57 12 举报
AI智能生成
JVM脑图(内存模型和垃圾回收部分,待完善)
作者其他创作
大纲/内容
Java内存区域
JVM会在执行Java程序的过程中把它所管理的内存划分成若干个不同的数据区域
程序计数器
程序计数器占据较小的内存区域,它可以看作当前线程所执行的字节码的行号指示器。为了保证CPU执行权从A线程跳转到B线程后,返回A时,A线程能够继续执行之前的代码,所以A,B两个线程都具有自己的程序计数器,所以程序计数器具有线程隔离性
如果线程执行一个Java方法,程序计数器中存放的就是正在执行的虚拟机字节码指令的地址
如果线程执行一个本地方法,程序计数器中为空
线程私有
Java虚拟机栈
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧再虚拟机栈中从入栈到出栈的过程。
线程私有
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。
一个线程中方法的调用链可能会很长,很多方法都同时处于执行状态。对于JVM执行引擎来说,在在活动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,与这个栈帧相关连的方法称为当前方法,定义这个方法的类叫做当前类。
本地方法栈
与虚拟机栈作用相似,区别在于虚拟机栈执行Java方法服务,而本地方法栈则是虚拟机使用到的本地方法服务
堆
虚拟机管理的最大一块内存,是线程共享的一块内存区域,再虚拟机启动时创建,唯一的目的就是存放对象实例。
Java堆可以处于物理上不连续的内存空间中,但再逻辑上它应该被视为连续的。
堆无法扩展时,Java虚拟机会抛出OutOfMemoryError
可固定大小,也可扩展,通过-Xmx和-Xms设定
方法区
存储已被虚拟机加载的类型信息,常量,静态变量,即使编译器编译后的代码缓存等数据。是线程共享的。方法区里的class文件信息包括:魔数,版本号,常量池,类,父类和接口数组,字段,方法等信息,其实类里面又包括字段和方法的信息。
运行时常量池:运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容及那个在类加载后存放到方法区的运行时常量池中
运行时常量池具备动态性,就是运行期间也可以将新的常量放入池中,例如String的intern()。
当常量池无法申请到内存时会抛出OutOfMemoryError
运行时常量池具备动态性,就是运行期间也可以将新的常量放入池中,例如String的intern()。
当常量池无法申请到内存时会抛出OutOfMemoryError
HotSpot在JDK不同版本中对方法区的设计
JDK7以前
JDK7
JDK8
Java虚拟机运行时数据区
访问对象
Java程序通过栈上的reference数组来操作堆上的具体对象。对象的访问方式由虚拟机实现,主流的访问方式主要有使用句柄和直接指针两种
使用句柄访问
Java堆中划分出一块内存来存放句柄池,reference中存储大的就是对象的句柄地址,而聚丙种包含了对象实例数据和类型数据各自的地址信息
reference存储的时稳定的句柄地址,对象被移动时只需要改变句柄池中的实例数据指针
直接指针访问
reference中存储的就是对象地址
速度更快,节省了一次指针定位的时间开销
JDK1.8
永久代退出历史舞台,元空间作为其替代者登场
垃圾收集
对象已死?
引用计数算法
在对象中添加一个引用计数器,每当一个地方引用它时,计数器加一;每当引用失效的时候,计数器值减一;任何时刻计数器为零的对象就是不可能再被使用的
缺点:对象相互循环引用
可达性分析算法
通过一系列"GC Roots"的根对象作为其实节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过过的路径成为"引用链",如果某个对象到GC Roots间没有任何引用链相连,则证明此对象不可能再被使用
可以作为GC Roots的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中的类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)中引用的对象
Java虚拟机内部的引用
所有被同步锁(synchronized关键字持)持有的对象
反应Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码换粗等
即使在可达性分析算法中不可达的对象,也并非“非死不可”,这时候这些对象将暂时处在“缓刑”阶段。
要宣告一个对象的真正死亡,至少要经历俩次标记过程:
如果对象在进行可达性分析之后发现没有与GC Roots相连的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象石油有必要执行finalize()方法
当对象没有覆盖finalize()方法,或者finalize()方法已经被JVM调用过,虚拟机会认为这俩种情况都“没有必要执行”,此时的对象才是真正“死”的对象。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(执行指的是虚拟机会触发finalize()方法)。
finalize()方法是对象逃脱“死亡”的最后一次机会,稍候GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功自救(只需要重新与引用链上的任意一个对象建立起关联关系即可),那在第二次标记时,它将会被移除“即将回收”的集合;如果对象这个时候还是没有自救成功,那么就会被真正的回收了。
要宣告一个对象的真正死亡,至少要经历俩次标记过程:
如果对象在进行可达性分析之后发现没有与GC Roots相连的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象石油有必要执行finalize()方法
当对象没有覆盖finalize()方法,或者finalize()方法已经被JVM调用过,虚拟机会认为这俩种情况都“没有必要执行”,此时的对象才是真正“死”的对象。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(执行指的是虚拟机会触发finalize()方法)。
finalize()方法是对象逃脱“死亡”的最后一次机会,稍候GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功自救(只需要重新与引用链上的任意一个对象建立起关联关系即可),那在第二次标记时,它将会被移除“即将回收”的集合;如果对象这个时候还是没有自救成功,那么就会被真正的回收了。
回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型
垃圾收集算法
分代垃圾收集理论
建立在两个分代假说上
弱分代假说:绝大多数对象都是朝生夕灭
强分代假说:熬过 越多次垃圾收集过程的对象就越难以消亡
跨代引用假说
存在相互引用关系的两个对象,一个存在于新生代,一个存在于老年代,那么这两个对象应该同时生存或者同时消亡呢
解决方法:在新生代上建立一个全局的数据结构(该结构被称为记忆集),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描
设计原则
收集器应该将Java堆划分出不用的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾手机过程的次数)分配到不用的区域之中存储
定义收集
部分收集(Partial GC)
新生代收集(Minor GC/Young GC)
老年代收集(Major GC/Old GC)
混合收集(Mixed GC)
整堆收集(Full GC)
标记-清除算法(Mark-Sweep)
分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象
两个主要缺点
执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须要进行大量标记和清除动作,导致标记和清除两个过程的执行效率都会随对象数量增长而降低
内存空间碎片化问题,标记,清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后再程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作
标记-复制算法(Semispace Copying)
被称为“半区复制”,将内存划分成大小相等的两块内存区域,每次只使用其中的一半,当其中一块使用完时,就将还存活的对象复制到另一块内存上,然后再把一使用过的内存空间一次清理掉
缺点:可用内存缩小为一半,浪费空间
Apple式回收
将新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配只内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍存货的对象一次性复制到另外一块Survivor空间上,然后清理掉这两块已经使用过的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1
逃生门设计:当survivor空间不足以容纳一次Minor GC之后存货的对象时,就需要依赖其他内存区域进行分配担保
标记整理算法(Mark-Compact)
标记所有需要回收的对象,然后将所有存活的对象都想内存一端移动,最后直接清理掉边界以外的内存
HtoSpot垃圾回收算法细节
根节点枚举
枚举根节点时需要暂停用户现成的,当用户线程暂停时,并需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机应当有办法得到哪些地方存放着对象引用。
HotSpot使用OopMap的数据结构来记录什么地方存储对象引用,对一旦类加载动作完成时,Hotspot就会把对象内什么偏移量上是什么类型的数据计算出来。
安全点
不是所有指令都会生成对应的OopMap,只是在特定的的位置记录了这些信息,这些位置被称为安全点。
安全点的选择基本上以“是否具有让程序长时间执行的特征”为标准进行选定的,因为每条指令执行时间都非常短暂,程序不太可能因为指令流太长这样的原因而长时间执行,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用,循环跳转,异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点
垃圾收集时,线程如何跑到安全点?
抢先式中断
垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上
主动式中断
当垃圾收集需要中断线程时,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己再最近的安全点上主动挂起
安全区域
线程没有分配时间片时,例如线程处于Sleep状态或者Blocked状态,线程无法响应虚拟机中断的请求,不可能走到安全点去挂起自己。引入了安全区域
安全区域是指能够确保在某一代码片段中,引用关系不会发生变化,因此,在此区域中任意地方开始垃圾收集都是安全的
记忆集和卡表
记忆集
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
最简单的实现:非收集区域中所有含跨代引用的对象数组来实现这个数据结构
实现记忆集时选择不同记录粒度来节省记忆集的存储和维护成本
字长精度
对象精度
卡精度
使用一种称为“卡表”的方法去实现记忆集
卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度,与堆内存的映射关系等
卡表最简单的形式就是一个字节数组,HotSpot虚拟机就是这么做的。CARD_TABLE[this address >> 9] = 0;
卡页
字节数组的每一个元素都对应着其标识的内存区域中的一块特定大小的内存区域,一个内存被称为“卡页”。
卡页大小都是2的N次幂的字节数,HotSpot的卡页是2的9次幂,即512字节
计算对象引用所在卡页的卡表索引号。将地址右移9位,相当于用地址除以512(2的9次方)。可以这么理解,假设卡表卡页的起始地址为0,那么卡表项0、1、2对应的卡页起始地址分别为0、512、1024(卡表项索引号乘以卡页512字节)
只要卡页中内有一个及以上的对象字段存在着跨代指针,那就将对应卡表数组的值标识为1,称这个元素变脏,没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把他们加入GC Roots中一并扫描
并发的可达性分析
根节点的枚举阶段是不太耗时的,也不会随着java堆里面存储的对象增加而增加耗时。而"标记"过程的耗时是会随着java堆里面存储的对象增加而增加的。 "标记"阶段是所有使用可达性分析算法的垃圾回收器都有的阶段。因此我们可以知道,如果能够削减"标记"过程这部分的停顿时间,那么收益将是可观的。
所以并发标记要解决什么问题呢?
就是要消减这一部分的停顿时间。那就是让垃圾回收器和用户线程同时运行,并发工作。也就是我们说的并发标记的阶段。
所以并发标记要解决什么问题呢?
就是要消减这一部分的停顿时间。那就是让垃圾回收器和用户线程同时运行,并发工作。也就是我们说的并发标记的阶段。
为什么遍历对象图的时候必须在一个能保障一致性的快照中?
三色标记
在遍历对象图的过程中,把访问都的对象按照"是否访问过"这个条件标记成以下三种颜色:
白色:表示对象尚未被垃圾回收器访问过。显然,在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
黑色:表示对象已经被垃圾回收器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其它的对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
灰色:表示对象已经被垃圾回收器访问过,但这个对象至少存在一个引用还没有被扫描过。
白色:表示对象尚未被垃圾回收器访问过。显然,在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
黑色:表示对象已经被垃圾回收器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其它的对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
灰色:表示对象已经被垃圾回收器访问过,但这个对象至少存在一个引用还没有被扫描过。
在可达性分析的扫描过程中,如果只有垃圾回收线程在工作,那肯定不会有任何问题。
但是垃圾回收器和用户线程同时运行呢?这个时候就有点意思了。
垃圾回收器在对象图上面标记颜色,而同时用户线程在修改引用关系,引用关系修改了,那么对象图就变化了,这样就有可能出现两种后果:
但是垃圾回收器和用户线程同时运行呢?这个时候就有点意思了。
垃圾回收器在对象图上面标记颜色,而同时用户线程在修改引用关系,引用关系修改了,那么对象图就变化了,这样就有可能出现两种后果:
一种是把原本消亡的对象错误的标记为存活,这不是好事,但是其实是可以容忍的,只不过产生了一点逃过本次回收的浮动垃圾而已,下次清理就可以。
一种是把原本存活的对象错误的标记为已消亡,这就是非常严重的后果了,一个程序还需要使用的对象被回收了,那程序肯定会因此发生错误。
产生“对象消失”的问题,即原本应该时黑色的对象被误标为白色的原因,当且仅当两个条件同时满足时会产生该问题
赋值器插入了一条或者多条从黑色对象到白色对象的新引用
赋值器删除了全部从灰色对象到白色对象的直接或者间接引用
解决方案
增量更新
破坏条件一,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。可以简化的理解为:黑色对象一旦插入了指向白色对象的引用之后,它就变回了灰色对象。
CMS基于增量更新来做并发标记
原始快照
破坏条件二,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
G1,Shenandoah基于原始快照做并发标记
经典垃圾收集器
HotSpot虚拟机的垃圾收集器,连线表示收集器可以搭配使用,Serial + CMS和Par New + Serial Old着两组收集器组合在JDK9中被取消了
新生代收集器
Serial
单线程收集器
进行垃圾收集时,必须暂停其他所有工作线程,直到它手机借宿
Serial/Serial Old收集器运行示意图
依然时HotSpot虚拟机运行在客户端模式下的默认新生代收集器
简单高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的
基于标记-复制算法
Par New
Serial收集器的多线程并行版本
Par New/Serial Old收集器示意图
Par New是激活CMS(使用-XX:+UseConcMarkSweepGC选项)的默认收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它
-XX:+UseParNewGC参数在JDK9中被取消了,从此ParNew + CMS只能互相搭配使用了
Par New收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果
基于标记-复制算法
Parallel Scavenge
基于标记-复制算法
关注点和其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程暂停的时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,
吞吐量优先收集器
吞吐量
处理器用于运行用户代码的时间与处理器总消耗时间的比例
吞吐量=运行用户代码时间/(运行用户代码时间 + 运行垃圾收集时间)
停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务
提供了两个参数用于精确控制吞吐量
-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,允许值是一个大于0的毫秒数,收集器尽力保证内存回收花费时间不超过用户设定值
-XX:GCTimeRatio:设置吞吐量大小,是一个大于0小于100的整数
-XX:+UseAdaptiveSizePolicy
这个参数被激活后,就不需要人工指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio),晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了
老年代收集器
Serial Old
Serial的老年代版本
单线程收集器
标记-整理算法
Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集
标记-整理算法
CMS(Concurrent Mark Sweep)
收集器是一种以获取最短回收停顿时间为目标的收集器
标记-清除算法
收集过程
初始标记
标记GC Roots能直接关联到的对象,速度很快
需要Stop The World
并发标记
从GC Roots的直接关联对象开始便利整个对象图的过程,耗时较长,但不需要停顿用户线程
重新标记
需要Stop The World
重新标记是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。会比初始标记阶段稍长,但比并发标记阶段稍短
并发清理
清理掉标记阶段判断已经死亡的对象
Concurrent Mark Sweep收集器运行示意图
优缺点
优点
并发收集
低停顿
对处理器资源非常敏感,在并发阶段,会占用一部分线程而导致应用程序变慢,降低总吞吐量
必须预留一部分空间供并发收集时的程序运作使用
产生空间碎片
Garbage First收集器(G1)
面向堆内存任何部分组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式
G1把Java堆划分为多个大小相等的独立区域(Region),每个Region都可以根据需要,扮演新生代的Eden空间,Survivor空间或者老年代空间
Region中还有一类特殊的HUmongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一般的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB-32MB,且应为2的N次幂。对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中。
如何回收一个Region通过判断里里面垃圾堆积的价值大小,价值即回收所获得的空间大小以及回收所需时间的经验值。然后再后台维护一个优先级列表
存在问题
跨Region引用对象,每个Rgion都维护一个记忆集,则G1至少需要耗费大约相当于Java堆容量10%到20%的额外内存来维持收集器工作
G1采用原始Kauai找解决“对象消失的问题”
对于回收过程这种新创建的对象,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,新分配的对象地址都必须再这两个指针位置以上。默认这些对象存货
G1收集器的停顿预测模型是以衰减均值为理论基础来实现的,再垃圾收集过程中,G1收集器会记录每个Region的回收耗时,每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值,标准偏差,置信度等统计信息
垃圾回收过程
初始标记
标记GC Roots能直接关联到的对象,并且修改TAMS指针的值
并发标记
从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象
最终标记
对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
筛选回收
负责更新Region的统计数据,对各个Region的回收价值和成本进行排序。根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存货对象复制到空的Region中,再清理整个旧Region的全部空间
目前再小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间
低延迟垃圾收集器
衡量垃圾收集器的三项最重要的指标是:内存占用,吞吐量和延迟,三者共同构成了一个“不可能三角”。一款优秀的收集器通常最多可以同时达成
虚拟机类加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据及进行校验,转换解析和初始化。最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制
类的声明周期,加载,验证,准备,初始化和卸载的顺序是确定的,解析不一定,因为为了支持Java语言的运行时绑定特性
对类"初始化"有且仅有6中情况(加载,验证,准备需要在此之前开始)
遇到 new、getstatic、putstatic或invokestatic这 4 条字节码指令时,如果类未进行过初始化,那么需首先触发类的初始化。分别对应的场景为:1、使用 new 关键字实例化对象时;2、读取类的静态变量时(被 final修饰,已在编译期把结果放入常量池的静态字段除外);3、设置类的静态变量时;4、调用一个类的静态方法时。
使用java.lang.reflect包的方法堆类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化
初始化类时,父类还没初始化
虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
当一个接口中定义了JDK 8新加入的默认方法(被default关键字修的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
这六种场景中的行为称为对一个类型进行主动引用,除此之外,所有引用类型的方法都不会触发初始化,称为被动引用。例如,对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,指挥触发父类的初始化而不会触发子类的初始化
类加载过程
加载
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束条件
验证阶段大致上会完成下面四个阶段的检验动作
文件格式验证
是否以魔数0xCAFEBABE开头
主、次版本号是否在当前Java虚拟机接受范围之内
Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
.......
元数据验证
这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
这个类的父类是否继承了不允许被继承的类(被final修饰的类)
......
字节码验证
通过数据流分析和控制流分析,确定程序语义是否是合法的,符合逻辑的
符号引用验证
符号引用验证可以看作是对类自身意外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类,方法,字段等资源
准备
正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:public static int value = 123; 那么变量value准备阶段过后的初始值为0而不是123。特殊情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量就会被初始化为ConstantValue属性所指定的初始值,例如:public static final int value = 123;
解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定是已经加载到虚拟机内存当中的内容。
直接引用:直接引用是可以直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。
类活接口解析
字段解析
方法解析
初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。
换句话说,只对static修饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行
换句话说,只对static修饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行
类加载器
类加载器(class loader)用来加载 Java 类到 Java 虚拟机中
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。也就是说两个类即使来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 这个“相等”,包括代表类的Class对象的equals()方法,isAssignableFrom()方法,isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况
双亲委派模型
启动类加载器
属于虚拟机的一部分
负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可
其他类加载器
独立于虚拟机外部,继承自抽象类java.lang.ClassLoader
扩展类加载器
这个加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
应用程序加载器
这个类加载器是由sun.misc.Launcher$AppClassLoader实现的。由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以也叫系统类加载器。它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
定义
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器
工作过程
双亲委派模型的工作过程如下:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围内没找到这个类)时,自加载器才会尝试自己加载
Java技术体系
JDK
Java虚拟机(JVM)
Java程序设计语言
Java类库
JDK是 用于支持Java程序开发的最小环境
定义
线程私有
每条线程中独占的内存区域
JVM参数设置
-Xms
初始堆大小
默认值:物理内存的1/64(<1GB)
默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.
将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展
-Xmx
最大堆大小
默认值:物理内存的1/4(<1GB)
默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-XX:+HeapDumpOnOutOfMemoryError
让虚拟机再出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行时候分析
-XX:MaxMetaspaceSize
设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小
-XX:MetaspaceSize
指定元空间的初始空间大小,以字节为单位
-XX:MinMetaspaceFreeRatio
指定元空间的最小空间
异常
内存溢出
内存泄漏
引用
强引用
在程序代码之中普遍存在的引用赋值,即类似Object obj = new Object()这种引用关系,无论在任何情况下,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象
软引用
描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列入回收范围之内进行第二次回收,如果这次回收还没有足够的内存,才会抛出异常
弱引用
描述那些非必须对象,强度比软引用更弱以些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾回收器开始工作,无论当前内存是否足够,都会回收掉被弱引用关联的对象
虚引用
最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,为对象设置虚引用的唯一目的只是为了能在这个对象被收集器回收时受到一个系统通知
0 条评论
下一页