JVM基础知识总结

一、内存模型及分区,详细讲到每个分区放什么
JVM是按照运行时数据的存储结构来划分内存结构的,JVM在运行java程序时,将它们划分成几种不同格式的数据,分别存储在不同的区域,这些数据统一称为运行时数据。运行时数据包括Java程序本身的数据信息和JVM运行Java需要的额外数据信息。
在java虚拟机规范中,将java运行时数据划分为五种,分别为
  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈
  • Java堆
  • 方法区
JVM基础知识总结

(1)程序计数器
程序计数器是一块线程私有的区域,它可以看做当前线程所执行的字节码的行号指示器。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都要依赖这个计数器来完成。
由于Java虚拟机的多线程时通过线程切换并分配处理器执行时间来实现的,对于单核处理器在某一个时间都只会有一个线程在运行,为了线程切换后能恢复到正确的执行位置,**每个线程都需要维护一个独立的程序计数器**,各个线程之间的计数器互不影响,独立存储。
如果当前线程正在执行一个Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的指令。
如果当前线程正在执行一个Native方法,程序计数器记录值则为空。
此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMeoryError情况的区域
(2)java虚拟机栈
java虚拟机栈是线程私有的内存区域,它总是和某个线程关联在一起,它的生命周期和线程相同
虚拟机栈描述的是java方法执行的内存模型:每个方法在执行时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
Java虚拟机栈中会包含多个栈帧,每个栈帧又会于某个方法关联起来,每运行一个方法就会创建一个栈帧(用于存储局部变量表、操作数栈、方法返回值等信息,局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间时完全确定的,在方法运行期间不会改变局部变量表的大小)。
每当一个方法执行时,创建一个栈帧置于栈顶。每当一个方法执行完成时,这个栈帧就会弹出栈帧的元素作为这个方法的返回值,并清除这个栈帧。
Java虚拟机栈栈顶的栈帧就是当前正在执行的活动栈帧,也就是当前Java虚拟机正在执行的方法,程序计数器也会指向这个地址。只有活动的栈帧的局部变量可以被操作栈使用,当这个栈帧调用另一个方法时,与新方法对应的新栈帧也随之被创建,并放到Java虚拟栈的顶部,变为新的活动栈帧,当这个栈帧执行完成时这个栈帧移出Java栈,刚才的那个栈帧又变为活动栈帧,前面栈帧的返回值又变为这个栈帧的操作栈中的一个操作数。如果前面的栈帧没有返回值,那么当前的栈帧的操作栈的操作数就不会有任何变化。
(3)本地方法栈
JVM运行Native方法使用的空间,也是线程私有的内存区域,它的作用与java虚拟机栈的作用是类似的,除了代码中包含的常规的Native方法会使用这个存储空间,在JVM利用JIT技术时会将一些java方法重新编译为Native Code 代码,这些编译后的本地方法代码也是利用这个栈来跟踪方法的执行状态。

区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机栈使用到的Native方法服务。

(4)java堆
jvm所管理的内存中最大的一块,是jvm管理java对象的核心存储区域,是被所有线程共享的一块内存区域。
jvm启动时,java堆唯一目的就是存放对象实例,是java应用程序与内存关系最密切的存储区域。每一个存储在堆中的java对象都时这个对象的类的一个副本,它会复制包括继承自它父类的所有非静态属性。
根据JVM规范,JVM堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,可通过-Xmx设置最大Java堆的大小,-Xms设置初始化java堆大小。
(5)方法区
是线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。 方法区也就是我们通常所说的永久区,它的大小可通过参数—XX:PermSize、—XX:MaxPermSize进行设置,方法区存储区域的大小一般在程序启动后一段时间内就固定了,JVM运行一段时间后,需要加载的类通常都已经加载到JVM中了,但项目中如果存在类的动态编译,就需要观察方法区的大小是否能够满足类存储。
垃圾回收较少发生在该内存区域,它存储的信息相对比较稳定,回收的主要目标是常量池和对类型的卸载。对类型的卸载相当苛刻,要求满足以下三个条件才能算是“无用”的类
  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

(5.1)运行时常量池是方法区中的一部分,用于存放编译期间生成的各种字面量和符号引用。运行时常量池相对于Class文件常量池的一个重要特征在于其具备动态性,Java语言并不要求常量一定只有编译期才能产生,在运行期间也能产生新的常量放入常量池中,如String.intern()方法。
二、JVM的栈内存分配和堆内存分配
在操作系统中内存分配策略有三种:1.静态内存分配 2.栈内存分配 3.堆内存分配 
JVM内存分配主要基于两种:栈内存分配,堆内存分配
(1)栈内存分配
Java栈空间的分配是和线程绑定在一起的,当一个线程创建时,JVM就会就会为这个线程创建一个新的Java栈,一个线程的方法的调用和返回对应于这个Java栈的压栈和出栈。当线程**一个Java方法时,JVM就会在线程的Java栈里新压入一个栈帧,用来保存参数、局部变量、中间计算过程和其他数据。退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁。
栈的优点:存取速度比堆快,仅次于寄存器,栈数据可以共享。
栈的缺点:存在栈中的数据大小、生存期是在编译时就确定的,导致其缺乏灵活性。

(2)堆内存分配
每一个Java应用都唯一对应一个JVM实例,每一个实例又对应一个堆。Java应用程序在运行中所创建的几乎所有的类实例或数组都会放在这个堆中(随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换让一些对象直接在栈上进行分配,从而提升程序性能),并由该应用程序的所有线程所共享。Java中分配堆空间时在启动时自动初始化的,对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码显示地释放,由垃圾回收机制负责回收这些内容区域。
堆的优点:动态地分配内存大小,生存期不必事先告诉编译器,它是在运行期动态分配的,垃圾回收器会自动收走不再使用的空间区域。

堆的缺点:运行时动态分配内存,在分配和销毁时都要占用时间,因此堆的效率较低。

三、内存分配与回收
对于java而言,内存分配包括静态内存分配,动态内存分配
  • 静态内存分配是指在Java被编译时就已经确定了所需的内存空间,当程序被加载时系统把内存一次性分配给它,在程序执行时不发生变化,程序执行结束时内存被回收。例如:程序计数器、虚拟机栈、本地方法栈3个区域随着线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出不停的进栈和出栈。这几个区域的内存分配和回收都具有确定性,无需过多考虑。
  • 动态内存分配是指Java中的对象内存空间是在运行期间动态分配的,只有程序处于执行期间才会知道哪些对象被创建。这部分内存空间什么时候不被使用,如何回收它们,正是JVM垃圾回收需要解决的问题。
1、GC的两种判定方式:
垃圾回收必须要完成两件事情:
  • 正确检测出垃圾对象
  • 释放垃圾对象占用的空间
当前常见的检测垃圾的方法包括两种:1. 引用计数法;2. 可达性分析算法。

(1)引用计数法:
给对象添加一个引用计数器,每当该对象被引用,它的计数器值就+1;当引用失效时,计数器就-1;在任何情况下,当计数器值为0时,就表示该对象不再被使用。
缺点:它很难解决对象之间相互引用,引起的循环引用问题,会产生无法被释放的内存区域。因此,主流的JVM都没有选用引用计数法来管理内存。

(2)可达性分析算法
      主流的JVM基本都使用可达性分析算法来判断对象是否存活,通过一系列“GC Roots”的对象作为起始点向下搜索,搜索所走过的路径为引用链,当一个对象没有任何引用链与GC Roots相连,代表该对象不再被使用,将其判定为可回收的对象。
JVM基础知识总结
java语言中,可以作为GC Roots 的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(Native方法)引用的对象

也可以这么理解:

  • 方法中局部变量区中的对象引用
  • Java操作栈中对象引用
  • 常量池中的对象引用
  • 本地方法栈中的对象引用
  • 类的Class对象:当每个类被JVM加载时都会创建一个代表这个类的唯一数据类型的Class对象,而这个对象也同样存放在堆中,当这个类不再被使用时,在方法区中类数据和这个Class对象同样被回收。
java中的引用分类
强引用:只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。Object = new Object()
软引用:在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围内进行第二次回收。SoftReference
弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前。WeakReference
虚引用:为一个对象设置徐引用关联的唯一目的就是在这个对象被收集器回收时受到一个系统通知。PhantomReference

四、垃圾收集算法
1、标记-清除算法
该垃圾收集算法主要分成”标记“和”清除“两个阶段:首先标记出所有需要回收的对象,而后在标记完成后统一回收所有被标记的对象。
JVM基础知识总结
缺点:1. 效率问题,标记和清除两个过程的效率都不高;2. 空间碎片问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一个垃圾回收动作。

2.复制算法
为了解决标记-清除存在的效率问题,复制算法将内存划分为相等的两块,每次只使用其中一块。当这一块内存用完时,就将还存活的对象复制到另一块上面,然后将已经使用过的内存空间一次清理掉。
JVM基础知识总结
缺点:将内存缩小为了原来的一半,对内存空间耗费较大。在对象存活率较高时,需要进行多次复制操作,效率会变低。
3、标记-整理算法
将原有标记-清除算法进行改造,不是直接对可回收对象进行清理,而是让所有存活对象都向另一端移动,然后直接清理掉端边界以外的内存。
JVM基础知识总结
4、分代收集算法
把对象按照寿命长短进行分组,分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法,在新生代采用复制算法,在老年代采用“标记-清除”或者“标记-整理”算法。只需要少量存活对象的复制成本就可以完成收集,老年代因为对象存活率高,没有额外的空间对它进行分配担保,新创建的对象被分配在新生代,如果对象经过几次回收后仍然存活,那么就把这个对象划分到老年代。老年代的收集频度不象年轻代那么频繁,这样就减少了每次垃圾回收所需要扫描的对象,从而提高了垃圾回收效率。
JVM将整个堆划分为Young区、Old区和Perm区,分别存放不同年龄的对象,这三个区存放的对象有如下区别:
  • Young区分为Eden区和两个相同大小的Survivor区,其中所有新创建的对象都分配在Eden区域中,当Eden区域满后会触发**minor GC 将Eden区仍然存活的对象复制到其中一个Survivor区域中,另外一个Survivor区中的存活对象也复制到这个Survivor区域中,并始终保持一个Survivor区是空的。一般建议Young区地大小为整个堆的1/4**。
  • Old区存放Young区Survivor满后触发minor GC后仍然存活的对象,当Eden区满后会将存活的对象放入Survivor区域,如果Survivor区存不下这些对象,GC收集器就会将这些对象直接存放到Old区中,如果Survivor区中的对象足够老,也直接存放到Old区中。如果Old区满了,将会触发**Full GC回收整个堆内存**。
  • Perm区主要存放类的Class对象和常量,如果类不停地动态加载,也会导致Perm区满。Perm区地垃圾回收也是有Full GC触发地

五、垃圾收集器
Stop The World”是什么?因为JVM内存堆是分代的,不同的分代采用的垃圾收集器是不一样的;
例如在JDK1.3.1之前Serial收集器是年轻代垃圾收集的唯一选择;
Serial收集器,是单线程收集器,在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。这个就是Serial收集器的工作特性,我们也把这个特性称为“Stop The World”;

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体体现。下图列出了HotSpot虚拟机的垃圾收集器,两个垃圾器之间存在连线,就说明它们可以搭配使用。**新生代的垃圾回收器包括Serial、ParNew、Parallel Scavenge,老年代的垃圾回收器包括CMS、Serial Old、Parallel Old。其中新生代的三种垃圾回收器都采用了复制算法**。
JVM基础知识总结

(1)Serial 收集器


Serial收集器是一个单线程收集器,这个“单线程”不只是说它只会使用一个CPU或者一条线程去完成垃圾收集工作,更重要的是在它进行**垃圾收集时,必须暂停其他所有的工作线程**,直到它垃圾收集结束。它对于运行在client模式下的虚拟机来说是一个不错的选择
JVM基础知识总结
(2)ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,由于除了Serial收集器外,只有它能够与CMS收集器配合工作,因此,**在运行在Server模式下的虚拟机中,ParNew收集器是首选的新生代收集器**。
ParNew收集器是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC强制指定。使用-XX:ParallelGCThreads可以限制垃圾收集的线程数。
JVM基础知识总结
(3) Parallel Scavenge 收集器
这也是一个并行的新生代垃圾收集器,不同于其他收集器(以尽可能缩短垃圾收集时用户线程的停顿时间为目的),它是**唯一一个以达到一个可控制的吞吐量为目标的垃圾收集器**。
throughput = 运行用户代码的时间 / 总时间(垃圾收集时间+运行用户代码的时间)。
在后台运算的任务中,不需要太多的交互,保证运行的高吞吐量可以高效地利用CPU时间,尽快完成程序的运算任务
Parallel Scavenge 收集器可以使用**自适应调节策略**,使用-XX:+UserAdaptiveSizePolicy选项之后,就不需要指定-Xmn、-XX:SurvivorRatio等参数,虚拟机可以根据当前系统的运行情况动态收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
JVM基础知识总结

(4)Serial Old 收集器
该收集器**使用标记-整理算法对老年代垃圾进行回收**,它主要的两大用途:1. 配合Parallel Scavenge收集器;2. 作为CMS收集器在并发收集出现Concurrent Mode Failure时使用的后备预案。
(5)Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,**使用多线程和标记整理算法**。在**注重吞吐量和CPU资源敏感的场合,优先考虑使用Parallel Scavenge + Parallel Old收集器的组合**,切记Parallel Scavenge 是无法与CMS收集器组合使用的
(6)CMS(Concurrent Mark Sweep 收集器
首先说明下并发与并行的区别:
并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
并发:指用户线程与垃圾收集线程同时执行。
CMS收集器是一款并发收集器,是一种以获取最短回收停顿时间为目标的收集器,它是基于**标记-清除**算法实现的,它整个过程包含四个有效的步骤:
  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

JVM基础知识总结
其中,初始标记、重新标记仍然需要"Stop the World",但是它们的速度都很快。**初始标记只是标记一下GC Roots能直接关联到的对象重新标记是为了修正并发标记期间因为用户线程继续运作而导致标记产生变动的那一部分对象的标记记录并发标记是进行GC Roots Tracing的过程**。
缺点:
  • CMS收集器对CPU资源非常敏感,在并发阶段,它虽然不会导致用户线程停顿,但是它会占用一部分CPU资源进行垃圾收集从而导致应用程序变慢,总吞吐量会降低。
  • 由于CMS并发清除阶段用户线程还在运行,伴随程序的运行必然还有的新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再进行清理。也是由于垃圾收集阶段用户线程还需要运行需要预留足够内存给用户线程使用,如果CMS运行期间预留内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,虚拟机只得临时启动Serial Old进行老年代垃圾收集,这样会导致长时间停顿
  • 由于CMS是一款采用标记-清除算法实现的垃圾收集器,收集结束时会有大量的空间碎产生,空间碎片过多时,如果分配大对象找不到足够大的连续空间分配当前对象,就不得不提前触发一次Full GC。

(7)G1收集器
是一款面向服务器端应用的垃圾收集器。具有以下特点:
1、并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要挺短java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集:G1可以不需要其他收集器配合就能独立管理整个GC堆,但是它能够采用不同的方式去处理新的对象和已经存货了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
3、空间整合:从整体上看是“标记-整理”算法,从局部看(两个Rgeion之间)上来看是基于“复制”算法实现。总之,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。
4、可预测的停顿:这是相比于CMS的一大优势,CMS和G1都是为了降低停顿时间,但是G1除了追求低停顿外,还能建立可预测的停顿时间模型。

G1之前的收集器的范围都是整个新生代或者老年代,而使用G1时,java堆的内存分布就与其他收集器不同,它将java对分为多个大小相等的独立区域(Region)

G1简历可预测的停顿时间模型的原因:是因为它可以有计划的避免在整个java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

G1收集器的动作大致分为以下几个步骤:
初始标记:标记GC Roots能直接关联到的对象,并修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建对象,需要停顿线程,但耗时很短。
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,耗时较长,但可与用户程序并发执行。
最终标记:是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,并行执行程序。
筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,可以并发执行,但是选择停顿用户线程。

五、内存分配和回收策略
新生代GC(Minor GC)指发生在新生代的垃圾收集动作,因为java对象大多都具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也快。
老年代GC(Major GC/Full GC)指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC,Major GC的速度一般会比Minor GC慢10倍以上。

1.对象优先在Eden分配
大多数情况下,对象在新生代Eden区分配,当Eden区没有足够空间进行分配是,虚拟机将发起一次Minor GC。
2.大对象直接进入老年代
大对象是指:需要大量连续分配内存空间的java对象,例如很长的字符串以及数组
虚拟机提供-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做可以避免在Eden和两个Survivor区域之间发生大量的内存复制操作
3.长期存活的对象进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。在Survivor每次熬过一次Minor GC就会加1
4.动态对象年龄判定
为了更好地适应不同程序的内存状况,虚拟机并不是永远的要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代;如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
5.空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行一次Minor GC。如果小于或者没有设置HandlePromotionFailure,则要进行一次Full GC。
取平均值进行比较其实仍然是一种动态概率的手段,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败。如果出现了HandlePromotionFailure失败,则会重新发起一次Full GC,大部分情况i 啊都会将HandlePromotionFailure打开,避免过于频繁的Full GC。

大多数情况下降HandlePromotionFailure开关打开,避免Full GC过于频繁

老年代内存增长的原因:
  • 大对象直接在老年代空间分配
  • 生存年龄超限移动到老年代
  • Young GC时交换空间不足
  • Full GC导致年龄未超限的实例移动到老年代

六、对象创建方法
当虚拟机遇到一条new指令时
对象的内存分配:
(1)检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、初始化。如果没有首先进行类加载
(2)为新生对象分配内存。假如java堆中内存绝对规整,使用指针碰撞方法,否则使用空闲列表法
(3)对象生成过程中的安全性问题:一种是给分配内存空间的动作进行同步处理——虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲,哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAN用完并分配新的TLAB是,才需要同步锁定。
(4)分配完内存后,虚拟机需要将分配到内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作可以提前至TLAB分配时进行,这一步操作保证了对象实例字段在java代码中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
(5)虚拟机要对对象进行必要的设置,对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC分代年龄等,将这些信息存放在对象的对象头中。
(6)从虚拟机角度,对象已经创建完成,但从java程序的视图来看,还需要进行init

对象的访问定位:
目前使用指针和句柄的方式
使用句柄:java堆中会划分一块内存来作为句柄池,reference存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
优点:reference中存储的是稳定的句柄地址,在对象被移动时只改变句柄中的事例数据指针,不需要更改reference

使用直接指针访问:java堆对象的布局中就必须考虑如何防止访问类型数据的相关类型,reference中存储的就是对象地址。
优点:速度更快,它节省了一次指针定位的时间开销。


七、类加载的过程
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
类加载的时机:遇到new,getstatic,putstatic,invokestatic这四个字节码指令。
                       使用java.lang.reflect包的方法对类进行反射调用的时候。
                       当初始化一个类的时候。
                       当虚拟机启动时,用户需要指定一个要执行的主类。
                       当使用jdk1.7的动态语言支持时。
(1)加载:通过一个类的全限定名来获取定义此类的二进制字节流
                   将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
                    在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
(2)连接(验证过程) 验证是连接阶段的第一步,它主要是用于保证加载的字节码符合java语言的规范,并且不会给虚拟机带来危害。比如验证这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。按照验证的内容不同又可以细分为4个阶段:文件格式验证(这一步会与装载阶段交叉进行),元数据验证,字节码验证,符号引用验证(这个阶段的验证往往会与解析阶段交叉进行)。
(3)连接(准备阶段)
准备阶段主要是为类的静态变量分配内存,并设置jvm默认的初始值。对于非静态的变量,则不会为它们分配内存。
在jvm中各类型的初始值如下:
int,byte,char,long,float,double 默认初始值为0
boolean 为false(在jvm内部用int表示boolean,因此初始值为0)
reference类型为null
对于final static基本类型或者String类型,则直接采用常量值(这实际上是在编译阶段就已经处理好了)。
(4)连接(解析阶段)解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析过程就是查找类的常量池中的类,字段,方法,接口的符号引用,将他们替换成直接引用的过程。
a.解析过程主要针对于常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四种常量。
b. jvm规范并没有规定解析阶段发生的时间,只是规定了在执行anewarray,checkcast,getfield,getstatic,instanceof,invokeinterface,invokespecial,invokespecial,invokestatic,invokevirtual,multinewaary,new,putfield,putstatic这13个指令应用于符号指令时,先对它们进行解析,获取它们的直接引用.
c. jvm对于每个加载的类都会有在内部创建一个运行时常量池(参考上面图示),在解析之前是以字符串的方式将符号引用保存在运行时常量池中,在程序运行过程中当需要使用某个符号引用时,就会促发解析的过程,解析过程就是通过符号引用查找对应的类实体,然后用直接引用替换符号引用。由于符号引用已经被替换成直接引用,因此后面再次访问时,无需再次解析,直接返回直接引用。
(5)初始化
初始化阶段是根据用户程序中的初始化语句为类的静态变赋予正确的初始值。这里初始化执行逻辑最终会体现在类构造器方法<clinit>()方中。该方法由编译器在编译阶段生成,它封装了两部分内容:静态变量的初始化语句和静态语句块。
按照顺序执行,前面的只能给后面的赋值但是不能访问
初始化执行时机
jvm规范明确规定了初始化执行条件,只要满足以下四个条件之一,就会执行初始化工作
(1) 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法(对应new,getstatic,putstatic,invokespecial这四条字节码指令)。
(2) 通过反射方式执行以上行为时。
(3) 初始化子类的时候,会触发父类的初始化。
(4) 作为程序入口直接运行时的主类。
 初始化过程
初始化过程包括两步:
(1) 如果类存在直接父类,并且父类没有被初始化则对直接父类进行初始化。
(2) 如果类当前存在<clinit>()方法,则执行<clinit>()方法。
需要注意的是接口(interface)的初始化并不要求先初始化它的父接口。(接口不能有static块)
<clinit>()方法存在的条件
并不是每个类都有<clinit>()方法,如下情况下不会有<clinit>()方法:
a. 类没有静态变量也没有静态语句块
b.类中虽然定义了静态变量,但是没有给出明确的初始化语句。
c.如果类中仅包含了final static 的静态变量的初始化语句,而且初始化语句采用编译时常量表达时,也不会有<clinit>()方法。
并发性
在同一个类加载器域下,每个类只会被初始化一次,当多个线程都需要初始化同一个类,这时只允许一个线程执行初始化工作,其他线程则等待。当初始化执行完后,该线程会通知其他等待的线程。
八、双亲委派模型 ClassLoader
类加载器 默认的三种类加载器
BootStrap ClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等
Extension ClassLoader:称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。
App ClassLoader:称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。

双亲委派模型

 1、原理介绍

       ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
 

2、为什么要使用双亲委托这种模型呢?

       因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

3、 但是JVM在搜索类的时候,又是如何判定两个class是相同的呢?

     JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。现在通过实例来验证上述所描述的是否正确:
JVM基础知识总结
九、 分派:静态分派和动态分派
静态分派
所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派,静态分派的最典型应用就是多态性中的方法重载。静态分派发生在编译阶段,因此确定静态分配的动作实际上不是由虚拟机来执行的。
java中重载是静态分派的应用。

动态分派

动态分派与多态性的另一个重要体现——方法覆写有着很紧密的关系。向上转型后调用子类覆写的方法便是一个很好地说明动态分派的例子。这种情况很常见,因此这里不再用示例程序进行分析。很显然,在判断执行父类中的方法还是子类中覆盖的方法时,如果用静态类型来判断,那么无论怎么进行向上转型,都只会调用父类中的方法,但实际情况是,根据对父类实例化的子类的不同,调用的是不同子类中覆写的方法,很明显,这里是要根据变量的实际类型来分派方法的执行版本的。而实际类型的确定需要在程序运行时才能确定下来,这种在运行期根据实际 类型确定方法执行版本的分派过程称为动态分派。

单分派和多分派

前面给出:方法的接受者(亦即方法的调用者)与方法的参数统称为方法的宗量。但分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。
为了方便理解,下面给出一段示例代码:
class Eat{
}
class Drink{
}

class Father{
public void doSomething(Eat arg){
System.out.println("爸爸在吃饭");
}
public void doSomething(Drink arg){
System.out.println("爸爸在喝水");
}
}

class Child extends Father{
public void doSomething(Eat arg){
System.out.println("儿子在吃饭");
}
public void doSomething(Drink arg){
System.out.println("儿子在喝水");
}
}

public class SingleDoublePai{
public static void main(String[] args){
Father father = new Father();
Father child = new Child();
father.doSomething(new Eat());
child.doSomething(new Drink());
}
}
运行结果应该很容易预测到,如下:
爸爸在吃饭
儿子在喝水
我们首先来看编译阶段编译器的选择过程,即静态分派过程。这时候选择目标方法的依据有两点:一是方法的接受者(即调用者)的静态类型是 Father 还是 Child,二是方法参数类型是 Eat 还是 Drink。因为是根据两个宗量进行选择,所以 Java 语言的静态分派属于多分派类型。
再来看运行阶段虚拟机的选择,即动态分派过程。由于编译期已经了确定了目标方法的参数类型(编译期根据参数的静态类型进行静态分派),因此唯一可以影响到虚拟机选择的因素只有此方法的接受者的实际类型是 Father 还是 Child。因为只有一个宗量作为选择依据,所以 Java 语言的动态分派属于单分派类型。


自己理解:首选静态多分派,找到调用者是Father,因为两个对象都向上转型为了父类型,所以这时候找到的调用者就是father类
然后找到重载方法中的具体的参数类型

动态单分派:现在就剩下向上转型后的调用者到底是father还是child,所在运行期使用了动态单分派。
十、几种内存调试工具