jvm 摘自深入理解java虚拟机

Java虚拟机
一 java内存区域和内存溢出异常
运行时数据区域
在这里插入图片描述

栈帧是方法运行期的基础数据结构。
程序计数器
是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理线程恢复等基础功能都需要依赖这个计数器来完成。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复,每条线程都需要有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,我们称这类内存区域为”线程私有”的内存。
如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是natvie(本地)方法,这个计数器则为空(Undefined)。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈
Java虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态衔接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈内存(存放基本类型变量和对象引用的)是虚拟机栈中的局部变量表部分。局部变量表存放了编译器可知的各种基本数据类型、对象引用和returnAddress类型。64位的数据会占用2个局部变量(slot),其余只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法是,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在java虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
本地方法栈
为虚拟机使用到的native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定。本地方法栈抛出异常和虚拟机栈一致。
Java堆
是java虚拟机多管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。所以的对象实例以及数组都要在堆上分配。
是垃圾收集器管理的主要区域,因此很多时候也被称作”GC堆”(Garbage Collected Heap)。现在收集器采用分代收集算法:新生代和老年代;在细致一点有Eden空间、From Survivor空间、To Survivor空间等。如果从内存分配区域的角度看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区。不过不管怎样划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分是为了更好的回收内存,或者更快的分配内存。
Java堆可以处于不连续的内存空间中,只要逻辑上是连续的即可。当堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区
是各个线程共享的内存区域,用于存储已被虚拟机记载的类信息、常量、静态变量、即时翻译器编译后的代码等数据。与java堆区分。也被称为”永久代”。
不需要连续的内存和可以选择固定大小、可扩展,可以选择不实现垃圾回收。相对而言,垃圾回收在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样”永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收”成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。
当方法去无法满足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池
是方法区的一部分。用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java虚拟机对Class文件的每一部分(自然包括常量池)的格式都要严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。除了保存在class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行池常量池相对于Class文件常量池的另外一个重要特征是具备动态性,java语言并不要求常量一定只能在编译期产生,也就是并非预置入class文件常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
当常量池无法申请到内存时会抛出OutOfMemoryError异常。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常。
本机直接内存的分配不会受到java对大小的限制,但是,既然是内存,则肯定还是会受到本机总内存的大小及处理器寻址空间的限制。
对象访问
Object obj=new Object();Object obj 这部分的语义将会反映到java栈的本地变量表中,作为一个reference(引用)类型数据出现。而new Object() 这部分的语义将会反映到java堆中,形成一块存储了Object类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在java堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息。这些类型数据则存储在方法区中。
由于reference类型在虚拟机规范里面只规定了一个指向对象的引用。并没有定义这个引用应该通过哪种方式去定位,以及访问到java堆中的对象的具体位置。因此不同虚拟机实现的对象访问的方式会有所不同,主流的访问方式有:使用句柄和直接指针。
1、 使用句柄方式,java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址。而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
2、 如果使用直接指针访问方式,java对对象的布局中就必须考虑如何放置访问类型学数据的相关信息。Reference中直接存储的就是对象地址。
使用句柄访问方式的最大好处就是reference中存储到的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在java中非常频繁,一次这类开销积少成多后也是一项非常可观的指向成本。
OutOfMemoryError异常
在java虚拟机的描述中,除了程序计数器外,虚拟机内存的其他的几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。
Java堆溢出
Java堆用于储存对象实例
垃圾收集器于内存分配策略
引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值+1;当引用失效时,计数器-1;任何时刻计数器都为0的对象就是不可能再被使用的。但很难解决对象之间的相互循环引用的问题。
根搜索算法:通过一系列名为”GC Roots”的对象作为起始点,从从这些节点开始往下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
Java可作为GC Roots得到对象:
1、 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
2、 方法区中的类静态引用的对象。
3、 方法区中的常量引用的对象。
4、 本地方法栈中JNI(即一般说的Native方法)的应用引用对象。
引用类型
强引用(Strong Reference):指在程序代码之中普遍存在的,类似”Object obj=new Object()”这类的引用,只要强引用还存在,垃圾回收期永远不会回收掉被引用的对象。
软引用(Soft Reference):用来描述一些还有用,但并非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出情况之前,将会把这些对象列进回收范围之中,并进行第二次回收。如果这次回收之后还是没有足够的内存,才会抛出内存溢出异常。在jdk1.2之后,提供了Soft Reference类来实现软引用。
弱引用(Weak Refrence):描述非必需对象的,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在jdk1.2之后,提供了Weak Reference类来实现弱引用。
虚引用(Phantom Reference):也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在jdk1.2之后,提供了Phantom Reference类来实现虚引用。
Finalize()
要真正宣告一个对象的死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机执行过,虚拟机将这两种情况都视为”没有必要执行”。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的finalizer线程去执行。这里所谓的”执行”是指虚拟机会触发这个方法,但并不承诺会得到它运行结束。这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能导致F-Queue队列中的其他对象永久代处于等待状态,甚至导致整个内存回收系统崩溃。Finalize()方法是对象逃脱死亡命运的最后一次机会。稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize中成功拯救自己,只要重新与引用链上的任何一个对象简建立联系即可,那么在第二次标记时他将会被移出”即将回收”的集合:如果这时候还没有逃脱,就死。
任何对象的finalize()方法只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
方法区回收
在方法区进行垃圾收集的”性价比”一般比较低:在堆中,尤其是在新生代(java堆)中,常规应用进行一次垃圾收集可以回收70%~90%的空间,而永久代(方法区)的垃圾收集效率远低于此。
永久代的垃圾收集机制只要回收两部分内容:废弃常量和无用的类。
判断废弃常量:没有任何对象引用常量池中常量。
判断无用类:
1、 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
2、 加载该类的classloader已经被回收
3、 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机对满足上述3个条件的无用类进行回收,仅仅是”可以”,而不是像其他对象一样,不使用了就必然会回收。
在搭理使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成jsp和OSGI这类频繁自定义classloader的场景都需要虚拟机具备类卸载的功能,以保证永久代不溢出。

垃圾回收算法

标记-清除算法
算法分为两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。缺点:效率,标记和清除效率不高;空间,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

在这里插入图片描述

复制算法
为了解决效率问题,复制算法出现,它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。这样使得每次都是对其中一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针。按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将原来的内存缩小为原来的一半,太高了一点。
在这里插入图片描述

回收新生代:将内存分为一块较大的eden空间和;两块较小的survivor空间,每次使用eden和其中的一块survivor。当回收时,将eden和survivor中还存活着的对象一次性的拷贝到另外一块survivor空间上,最后清理掉eden和刚才用过的survivor的空间。

标记整理算法
复制算法在对象存活率较高时就要执行较多的复制操作,效率就会变低。老年代不使用这种方法。
首先标记出所有需要回收的对象,将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
在这里插入图片描述

分代收集算法
根据对象的存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集都会有大批对象死去,只有少量存活,那就选复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或标记整理算法来进行回收。

垃圾收集器

在这里插入图片描述

Serial收集器
最基本、历史最悠久的收集器,曾经是虚拟机新生代收集的唯一选择。这个收集器是一个单线程的收集器,它的单线程意义并不仅仅是说明他只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop The World),直到它收集结束。
优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。Serial收集器对于运行在client模式下的虚拟机来说是一个很好的选择。
在这里插入图片描述

ParNew收集器
Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshild、-XX:HandlePromotionFailure等)、收集算法、stop the world、对象分配规则、回收策略等都与serial收集器完全一样,实现上两种收集器也共用了相当多的代码。
在这里插入图片描述

PreNew除了多线程收集之外,其他与serial收集器相比并没有太多创新之外,但它却是许多运行在server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了serial收集器外,目前只有它能与CMS收集器配合工作。
它作为老年代的收集器,无法与Parallel Scavenge配合工作。使用-XX:UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。
ParNew在单CPU的环境中绝对不会有比serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超先技术实现的两个CPU的环境中都不能百分之百的超越serial收集器。可以使用-XX:ParallelGCThreads参数限制垃圾收集的线程数。
追求降低用户停顿时间,适合交互式应用,强交互弱计算。

Parallel Scavenge收集器
吞吐量优先收集器。
也是一个新生代收集器,使用复制算法,并行的多线程收集器。
它的关注点不同:CMS等收集器的关注点尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐了就是99%。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快的完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数及直接设置吞吐量大小的-XX:GCTimeRatio参数。
追求CPU吞吐量,能够在较短时间内完成指定任务,适合没有交互的后台计算,弱交互强计算。
在这里插入图片描述

-XX:+UseAdaptiveSizePolicy,开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式被称为调解策略。
Serial Old 收集器
Seial的老年代版本,单线程收集器,使用标记整理算法。主要意义是被client模式下的虚拟机使用。在sever模式下使用,两个用途:jdk1.5及之前版本中与Parallel Scavenge收集器搭配使用,另一个就是作为CMS收集器的后备预案,在并发收集发生concurent Mode Failure的时候使用。
在这里插入图片描述

Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。配合新生代parallel scanvenge使用。吞吐量优先组合,注重吞吐量及CPU资源敏感的场合,优先考虑parallel scavenge加parallel old收集器。
在这里插入图片描述

CMS(Concurrent Mark Sweep)收集器
一种以获取最短回收停顿时间为目标的收集器,重视服务的响应速度,希望系统停顿时间最短。基于标记清除算法,运作过程分为4个步骤:初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS ramark)、并发清除(CMS concurrent sweep)。、
其中初始标记、重新标记仍然需要stop the world。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记节点就是进行GC Roots Tracing的过程,而重新标记阶段则是为了并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是用户线程一起并发地执行的。
在这里插入图片描述

优缺点:优点:并发收集、低停顿。缺点:对CPU资源敏感、无法处理浮动垃圾、收集结束产生大量空间碎片。

G1收集器
基于标记整理算法,不会产生空间碎片。可以非常精确的控制停顿,及能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎是已经是实时java(RTSJ)的垃圾收集器的特征了。
可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力的避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的由来)。
内存分配和回收策略
Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。
对象的内存分配,就是在堆上分配,对象主要分配在新生代的Eden区,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况也可能直接分配在老年代中,分配的规则并不是百分之百固定,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的配置。
虚拟机性能监控与故障处理工具
在这里插入图片描述

类文化结构
Class文件是一组以8位字节为基础单位的二进制流。
常量池主要存放两大类变量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于java语言层面的常量概念,如果文本字符串、被声明为final的常量值。而符号引用则属于编译原理方面的概念,包括了下面三类常量:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符.。
类加载机制
类的生命周期:加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备和解析统称为连接。
在这里插入图片描述

四种必须立即对类进行初始化的情况:

1、 遇到new、getstatic、pubstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
2、 使用java.lang.reflect包的方法对类进行反射调用时。
3、 当初始化一个类的时候,如果发现其父类还没有初始化,则需要先触发其父类的初始化。
4、 当虚拟机启动时,用户需要指定一个要执行的主类。
类加载的过程
加载、验证、准备、解析和初始化。
加载
加载是类加载过程的第一个阶段。在加载阶段,虚拟机需要完成:
1、 通过一个类的全限定名来获取此类的二进制字节流。
2、 将这个字节流所代表的的静态出存储结构转化为方法区的运行时数据结构。
3、 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在夹在阶段之中的先后顺序,扔然属于连接阶段的内容,这两个阶段开始时间仍然保持着固定的先后顺序。
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段的工作量在虚拟机的类加载子系统中占了很大一部分,虚拟机规范对这个阶段的限制和指导显得非常笼统,如果验证到输入的字节流不符合当前Class文件的存储格式,将抛出一个java.lang.VerifyError异常或其子类异常。虚拟机大致完成四个阶段的检验过程:
1、 文件格式验证:第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个java类型信息的要求。这阶段的验证是基于字节流进行的,经过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储,所有后面的三个验证阶段全部是基于方法区的存储结构进行的。
2、 元数据验证:第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。主要目的是对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。
3、 字节码验证:第三阶段是整个验证过程中最复杂的一个阶段,主要工作是进行数据流和控制流分析。在第二阶段对元数据中的数据类型做完校验后,这阶段对类的方法体进行校验分析。这阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
4、 符合引用验证:最后一个阶段发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段—解析阶段中发生。符合引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性的校验。目的是确定解析动作能正常执行。
验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但不一定是必要的阶段。可以通过-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。
解析
解析阶段是虚拟机就跟你常量池内的符号引用替换为直接引用的过程。
符号引用:以一组符号来描述所引用的目标。符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用于虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用:可以是直接指向目标的指针、相对偏移量或是一个螚间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
解析动作主要针对类或接口、字段、类方法、皆可四类符号引用进行,分别对应常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_ref及CONSTANT_InterfaceMethodref_info四种常量类型。
初始化
类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中自定义的java程序代码(或者说字节码)。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,初始化阶段是执行类构造器clinit方法的过程。
类加载器
实现”通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为称为类加载器。
类加载器实现类的加载动作,比较两个类是否相等在只有在两个类是由同一个类加载器加载的前提之下才有意义。
双亲委派机制
在这里插入图片描述

工作过程:如果一个类收到了类加载的请求,它首先不会去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试去加载。
虚拟机字节码执行引擎
执行引擎:输入的是字节码文件,处理过程师字节码解析的等效过程,输出的是执行结果。
栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
在这里插入图片描述

局部变量表
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。
局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明Slot应占用的内存空间大小,只是很有导向性地说明每个Slot都应该存放一个boolean、byte、short、int、float、reference(引用)或returnAddress(指向一条字节码指令的地址)类型的数据。
对于64位的数据,虚拟机会以高位在前的方式为其分配两个连续的slot空间。由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的slot是否是原子操作,都不会引起数据安全问题。
虚拟机通过索引定位的方式使用局部变量表,索引值得范围从0开始到局部变量表最大的slot数量。如果是32位数据类型的变量,索引n就代表了使用第n个slot,如果是64位数据类型的变量,则说明要使用第n和第n+1两个slot。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static的方法),那么局部变量表中第0位索引的slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字this来访问这个隐含的参数。其余参数则按照参数表的顺序来排列,占用从1开始的局部变量slot,参数表分配完毕后吗,再根据方法体内部定义的变量顺序和作用域分配其余的slot。
局部变量表的slot是可重用的,方法体中定义的变量,其作用域不一定会覆盖整个方法体,如果当前字节码PC计数器的值以及超出了某个变量的作用域,那么这个变量对应的slot就可以交给其他变量使用。
操作数栈
在这里插入图片描述

常被称为操作栈,是一个后入先出的栈。操作数栈的最大深度在编译的时候被写入Code属性的max_stacks数据项之中。操作数栈的每一个元素可以是任意的java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。操作数栈的深度不会超过在max_stacks数据项中设定的最大值。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。

动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
方法返回地址
当一个方法执行后,有了两个方式退出这个方法,第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口。
另一种退出方式,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种方法退出方式称为异常完成出口。一个方法使用异常完成出口退出,不会有返回值给上层调用者。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
方法调用
方法调用不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本。暂时还不涉及方法内部的具体运作流程。在程序运行时,进行方法调用时最普遍、最频繁的操作,class文件的编译过程不包含传统编译中的连接步骤,一切方法调用在class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用)。这个特性给java带来了更强大的动态扩展能力,单页使得java方法的调用过程变得相对复杂起来,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。
Java内存模型和线程
内存间交互操作
Lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
Unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
Read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存,以便随后的load操作。
Load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
Use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,没得虚拟机遇到一个需要使用到变量的值得字节码指令时会将会执行这个操作。
Assign(赋值):作用于工作内存的变量,它把一个从执行引擎接受到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就执行这个操作。
Store(存储):作用于工作内存的变量,它把工作内存字中的一个变量传送到主内存,以便随后的write操作使用。
Write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
先行发生行为(happens-before)
程序次序关系:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生在书写在后面的操作。准确来说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
管程锁定规则:一个unlock操作先行发生于后面的同一个锁的lock操作。
Volatile变量规则:对一个volatile变量的写操作先行发生于后面合格变量的读操作。
线程启动规则:thread的start()方法先行发生于此线程的每一个动作。
线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
线程中的规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发送,可以通过Thread.interrupted()方法检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
传递性:
线程的实现
使用内核线程实现
使用用户线程实现
使用用户线程加轻量级进程混合实现
Java线程调度
协同式先调度和抢占式线程调度。
、 协同式调度:线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另一个线程上去。最大好处是实现简单。坏处是:线程执行时间不可控制,甚至如果一个线程编写有问题,一致不告知系统,那么程序会一直阻塞。
抢占式调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(thread.yield)。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致进程阻塞的问题,java使用的就是抢占式。
线程安全与锁优化
线程安全
不可变:不可变的对象一定时线程安全的。
绝对线程安全:
相对线程安全:需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保证措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
线程兼容:对象本身并不是线程安全的,但是可以通过调用端正确地使用同步手段来保证对象在并发环境中安全的使用。
线程对立:不管在调用端是否采取了措施,都无法在多线程环境中并发使用的代码。
锁优化
自旋锁和自适应自旋:
锁消除:对一些被检测到不可能存在共享数据竞争的锁进行消除。
锁粗化
轻量级锁:在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
偏向锁:消除数据在无竞争的情况下的同步原语,以进一步提高程序的运行性能。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

JVM参数
java -XX:+PrintCommandLineFlags -version 查看当前虚拟机使用的垃圾回收器。
在这里插入图片描述

java -XX:+PrintFlagsFinal -version 查看jvm命令
java -XX:+PrintFlagsFinal -version | wc -l 查看jvm命令数量 需在linux服务器上。
Jps 查看正在运行的java线程
Jinfo 进程id 打印进程id相关信息
Jstat -gc 进程pid 查看堆内存情况
Jstat -gc 进程pid 毫秒数 没过多少毫秒刷新一次堆内存情况
Jstack 进程pid 列出进程所有线程 一般用来发现有没有死锁

宋红康讲解

1、 
Class files(字节码文件)通过Class Loader(类加载器子系统)加载后生成,对应到内存当中生成大的Class对象,同时一些必要的静态属性初始化。真正执行字节码指令时ExecutionEngine(执行引擎)发挥作用。

java命令行

jps

可以通过配置jvm参数 -XX:-UserPerfData设置进程能否被jps显示。
-q:只显示进程id
-l:显示全类名
-m:显示传递给main的参数
-v:显示jvm参数
例:jps -q     jps -l     jps -m   jps -v  jps -lm

jstat

查看jvm统计信息。显示类装载、内存、垃圾收集、JIT编译等运行数据。常用于检测垃圾回收和内存泄漏问题。
-class::类装载使用,后面跟进程id
例: jstat -class PID  
信息详解:装载类数  装载类字节数  卸载类数  卸载类字节数 消耗时间
后面第一个数字 为间隔多少毫秒打印一次 第二个为共打印多少次
-t:显示程序运行时长
-h:间隔多少次打印一次表头
例:jstat -class  -t -h 3 PID 1000 10   每一秒打印一行共打印10次,间隔3次就打印一次表头

-compiler:查看编译情况
-printcompilation:查看编译过的方法

jinfo

查看和修改jvm参数。
-sysprops:查看系统参数。
-flags:查看jvm参数,会有历史记录。
-flag:后跟个jvm参数名,可查看该参数具体值。后面需跟pid
修改jvm参数,只有标记为manageable的fial才能修改。jinfo -flag +/-jvm参数名 pid。jinfo -flag +/-jvm参数名=具体参数值 pid。
例:jinfo -sysprops 1592 ;  jinfo -flags 1988  ; jinfo -flag HeadDumpPath=d:\a.hprof  ; jinfo -flag +PrintGC 1988
扩展:
java -XX:+PrintFlagsFinal:查看所有JVM参数最终值
java -XX:+PrintFlagsInitial:查看所有JVM启动初始值,第二列前面打了冒号的是被修改过的。
java -XX:PrintCommandLineFlags:查看被用户或者jvm设置过的xx参数和详细值

jmap

导出内存映射文件和内存使用情况。
jmap -dump:format=b,file="d:\a.hprof" pid
jmap -dump:live,format=b,file="d:\b.hprof" pid 只生成存活对象
jmap -heap pid:显示堆内存相关信息 
jmap -histo pid:同上
例:jmap -dump:format=b,file="d:\a.hprof" 1988 ; jmap -dump:live,format=b,file="d:\b.hprof" 1988  ; jmap -heap 1988  ;  jma -histo 1988
TLAB:Thread Local Allocation Buffer

jhat

jhap xxx.hprof
开启一个web服务,端口默认 7000,浏览器访问即可。
Execute Object Query Language (OQL) query:可往此菜单写查询语句。例:select s from java.lang.String s where s.value.length > 100
Show heap histogram:查看堆内存信息,按对象占用从大到小排序。

jstack

用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照是当前虚拟机内指定进程的每一条线程正在执行方法的堆栈集合。生成线程快照的作用:可用于定位线程长时间出问题的原因。如:死锁、死循环、请求外部资源导致长时间等待。
-l:除了堆栈信息外,显示关于锁的附加信息。
-F:当正常输出的请求不被响应时,强制输出。
-m:如果调用本地方法,可以显示C/C++堆栈。
jstack pid

jcmd

多功能工具,实现除了jstat外所有命令的功能。
jcmd 和 jcmd -l 相当于jps
jcmd pid help:列出当前进程可执行的指令。
jcmd pid 具体指令:指令为上方命令执行结果中的一条,可查看具体数据。

jstatd

远程主机信息收集。

jconsole

对JVM中内存、线程和类等监控是一个基于JMX(java management extensions)的GUI性能监控工具。

在这里插入图片描述

深堆和浅堆

浅堆(Shallw Heap):指一个对象所消耗的内存。除数据引用之外所占的内存。只关心对象头和属性值占用字节。
深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和。
保留集:对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或间接访问到的所有对象的集合。通俗的说,就是指仅被对象A所持有的对象的集合。

浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。
对象实际大小:定义位一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。

类字节数计算

一个属性占4字节,对象头占8个字节,类字节数为 属性个数*3+对象头数。字节数必须是8的倍数,不足需要加。例:一个类有3个属性,3*4+8=20,不是8的倍数,故占24字节。

内存泄漏和内存溢出

内存泄漏:申请了内存用完后不释放。
内存溢出:申请内存时,没有足够的内存可用。

GC类型

年轻代分为一个伊甸园空间(Eden),两个幸存者空间(Survivor),当年轻代中的Eden区分配满的时候,会触发年轻代的GC(Minor GC)。
混合回收(Mined GC,G1收集器特有的回收方式):能并发清理老年代中的整个整个的小堆区是一种最优情形,混合收集过程,不只清理年轻代,还会将一部分老年代区域也加入到CSet中。

在这里插入图片描述
年轻代提升至老年代的条件:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

G1收集器

G1的思路:不要求每次都把垃圾清理的干干净净,它只是努力做它认为对的事情,我们要求G1,在任意的1秒的时间内,停顿不得超过10ms。G1会尽量达成这个目标,它能够推算出本次要收集的大体区域,以增量的方式完成收集。这也是使用G1垃圾回收器不得不设置的一个参数:-XX:MaxGCPauseMillis=10。
G1把内存分为若干个区域,一小份取余的大小是固定的,名字叫做小堆区(Region)。小堆区可以是Eden区,也可以是Survivor区,还可以是Old区。所以G1的年轻代和老年代的概念都是逻辑上的。每一块Region,大小都是一致的,它的数值是在1M到32M字节之间的一个2的幂值数。

在这里插入图片描述
在这里插入图片描述
G1具体回收过程
1、并发标记:当整个堆内存使用达到一定比例(默认是45%),并发标记阶段就会被启动,这个比例也是可以调整的,通过参数 -XX:InitialtingHeapOccupancyPercent 进行配置。

在这里插入图片描述
在这里插入图片描述

ZGC

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值