JVM模拟面

来源:互联网 发布:oracle 执行sql脚本 编辑:程序博客网 时间:2024/06/05 15:11

JVM

说说java虚拟机运行时数据区的划分

java虚拟机在执行Java程序时,将所管理的内存划分为若干个不同的数据区域,这些区域各有各的用途。
总的来说,运行时数据区共有五部分,分别是程序计数器、Java虚拟机栈、Java堆、方法区以及本地方法栈。其中程序计数器、虚拟机栈和本地方法栈都是线程私有的,它们当中的数据随着线程的创建而存在,随着线程的停止而销毁。
这里写图片描述

程序计数器(PC计数器)

程序计数器是一块较小的内存,是当前线程所执行的字节码的行号指示器,使得线程切换后能恢复到正确的执行位置。
每个线程都有一个独立的程序计数器,互不干扰,独立存储,因此是线程私有的。
如果线程执行的是Java方法,则程序计数器记录的是正在执行的字节码指令地址;如果是native方法,则计数器值为空。不存在OutOfMemoryError异常。

Java虚拟机栈

Java虚拟机栈也是线程私有的,生命周期和线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每一个方法执行时都会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口灯信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的一个过程。
其中,**局部变量表存放了编译时期可知的各种基本数据类型、对象引用和returnAddress类型(指向字节码地址)。**double 和long都占用两个局部变量空间(slot),其他的都只占用1个。
局部变量表所需的内存空间在编译期间就完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小
JVM Stack 异常情况:
StackOverflowError:当线程请求分配的栈深度超过JVM允许的深度时抛出
OutOfMemoryError:如果JVM Stack可以动态扩展,但在扩展时无法申请到足够的内存时抛出异常。

本地方法栈

本地方法栈与Java虚拟机栈的作用类似,只不过本地方法栈执行的是native方法。它也会抛出StackOverflowError和StackOverflowError异常。

方法区

方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、函数的字节码内容等数据。在虚拟机启动时创建,在实际内存空间可以不连续,逻辑上连续即可。

运行时常量池

运行时常量池本来是方法区中的一部分,jdk1.7将其从方法区中移出,放入了堆空间内。
运行时常量池用于存放编译期生成的各种字面量和符号引用,这部分内容
将在类加载后进入常量池存放。
运行时常量池具备动态性,可以在运行期间将新的常量放入池中。如String.intern(),先在常量池中创建引用,再找值,如果存在,则引用指向它并返回;如果不存在,将值放入常量池再返回。
由于这一部分的内容
方法区异常主要是OutOfMemoryError异常。

Java堆

Java堆是Java虚拟机所管理的内存中最大的一部分。它是被所有线程所共享的,在虚拟机启动时创建。
所有的对象实例以及数组都要在堆上分配内存。
Java堆上的对象实例等被垃圾收集器所管理,无需、也无法显示的被销毁。
Java堆和方法区一样,不需要保证物理连续,只需要逻辑上连续即可。
Java堆的异常,也主要是OutOfMemoryError异常。

什么是指针碰撞,什么是空闲列表

指针碰撞和空闲列表都是Java堆对象创建过程中为对象分配空间的方法。

指针碰撞

假设Java堆上的内存是绝对规整的,用过的放在一边,没用过的放在另一边。它们的分界线放着一个指针作为指示器。为对象分配内存时就是将指示器往没用的一边挪动对象内存大小的距离,这种方式就成为指针碰撞。

空闲列表

假设Java堆上的内存并不是规整的,已存的和空闲的交错分布。这个时候指针碰撞的分配方式就无法实现了。虚拟机就必须维护一张表,用来记录哪些内存时可用的。在分配内存时就在表中查找一块足够大的空间分配给该对象实例,并更新表上的记录。这种方式就是空闲列表。
选择哪种方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。如采用复制算法的收集器就可以使用指针碰撞,采用标记整理的算法可以使用空闲列表。

说说你理解的GC机制

对于GC机制,主要是从三个方面来谈,首先是要明确哪些内存需要回收;其次是确定什么时候进行回收;最后就是如何回收的问题了。

哪些内存需要回收

我们知道,Java运行时内存大致分为了五个数据区,程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。
由于程序计数器、Java虚拟机栈和本地方法栈都是线程私有的,随着线程的创建而产生,随着线程的结束而销毁,因此这部分不需要我们自己来进行垃圾收集。
而对于方法区和Java堆,由于一个接口的多个实现类需要的内存可能不一样,一个方法的多个分支需要的内存也可能不一样,我们只有在运行期间才能知道可能会创建哪些对象,这部分的内存分配和回收都是动态的,所以我们所关注的垃圾收集就是关于这部分的内容。

确定什么时候进行回收

什么时候回收?!肯定是在对象无法存活的时候进行回收。因此我们就得判断对象是否还存活着,共有两种方法。

引用计数算法

这种方法就是给对象添加一个引用计数器,每当引用一次就+1,失效就-1,任意时刻引用计数器值为0的对象就不可能再被使用,可以进行垃圾收集。
优点:实现简单,判定效率很高,大部分情况下都适合。
缺点:很难解决对象之间相互循环引用的问题,这会导致两个对象明明已经不可能再被访问了,但它们的计数器都不为0,无法通知GC收集器对其进行回收。

可达性分析算法

这种方式是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所经历过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用,就可以进行回收。
GC Roots:虚拟机栈中引用的对象,方法区中静态属性引用的对象,方法区中常量引用的对象,本地方法栈中native方法引用的对象。

———-补充一下引用的知识———-

所谓的引用,就是指它的值代表的是另一块内存的起始地址,就成为这块内存代表着一个引用。
引用分为强引用,软引用,弱引用以及虚引用。
其中,强引用类似Object obj=new Object()。只要强引用存在,将永不会被垃圾收集器回收。
软引用,描述有用但非必需的对象。这种是在内存溢出异常发生之前,才会对其进行第二次回收,如果回收之后还没有足够的内存,才会抛出内存溢出异常。
弱引用,也是描述非必需的对象。只能生存到下一次垃圾收集发生之前。无论当前内存是否足够,都会回收掉弱引用对象。
虚引用:虚引用的唯一目的就是在这个对象被收集器回收时会收到一个系统通知。

对象不可达就一定死亡么?

不是。对象不可达之后,还要经历两次标记过程。
如果对象不可达,将会被标记一次并对其进行筛选:判断此对象是否有必要执行finalize方法。如果没有覆盖finalize方法或者系统已经调用过finalize方法,虚拟机则将其视为不执行
如果要执行finalize,对象将被放在一个F-Queue的队列之中,并随之由虚拟机创建的低优先级的Finalizer线程去触发执行,如果对象在这个过程中和引用链进行了链接(如把自己的this赋值给了某个类变量),那么对象将在第二次标记时将其移出要回收的集合,否则就会被二次标记,从而被真正回收掉。
FinalizerThread的runFinalizer方法对Throwable的异常进行了捕获,因此finalize复写时里面的异常不会被抛出,且方法结束。进入主线程。
任何一个对象的finalize方法只能被系统自动调用一次!!!

public class Test2 {    public static void main(String[] args) throws InterruptedException {        Test2 t=new Test2();        t=null;        System.gc();        Thread.sleep(1000);        System.out.println("...1");        System.gc();        Thread.sleep(1000);        System.out.println("...2");    }    @Override    protected void finalize(){        //System.out.println(10/0);        System.out.println("dashdkaj");    }}

结果如下:

dashdkaj     ...1...2

抛异常时结果:

...1...2

方法区的回收

HotSpot虚拟机中方法区被定义为永久代,Java虚拟机规范中明确可以不要求虚拟机在方法区实现垃圾收集,因为方法区垃圾收集的性价比特别低:Java堆中常规一次回收大致有70%-90%的空间,而永久代回收远低于此。
永久代回收主要包括两部分:废弃的常量和无用的类。常量池中已存在常量,而当前系统没有任何一个对应的对象,这就是废弃的常量。而一个无用的类则需要满足该类的所有实例被回收,加载类的类加载器也得被回收,以及该类对应的类对象没有任何地方被引用,无法通过反射机制来获得该类的方法。

如何回收

对于Java堆的回收主要有以下三种方式:

标记-清除算法

这种算法是将Java堆中需要被回收的对象进行标记,然后统一回收,即标记,清除两个阶段。
不足:效率问题:标记和清除过程效率都不是很高;其次是空间碎片化,对于大对象来说可能无法获得足够的内存而将再一次引发垃圾收集。

复制算法

这种算法是将可用内存划分为两块,每次只使用其中的一块,当这一块用完就将还存活的对象复制到另一块,而将已用过的内存空间一次性清理掉。
不会出现碎片化空间,实现简单,运行高效。但是它牺牲了一半的内存,代价太大。同时对于存活较多的情况,并不适用,复制也会有一定的消耗

标记-整理算法

这种算法是先对对象进行标记清除,然后再进行整理,让存活对象向着一段移动,连贯起来。

分代收集算法

当前商业虚拟机大多采用这种算法:根据对象的存活周期的不同将内存分为新生代和老年代。由于新生代98%的对象都是朝生夕死,因此就选用复制算法,而老年代对象存活率较高、没有额外的空间对其进行分配担保,就必须使用标记清除或标记整理算法来进行回收。
新生代分为Eden和两个Survivor区。默认比例8:1(新生代80%以上都是朝生夕死的,用完即可放弃),而不是1:1(复制算法的特点)。即新生代可用空间90%。如果回收剩下存活的对象如果大于一个Survivor空间(10%)就需要依赖老年代的内存进行分配担保了。

说说垃圾收集器的种类及特点

Serial收集器

这是最基本、发展历史最久的收集器,JDK1.3之前的唯一选择。
特点:单线程收集器,垃圾收集时必须暂停其他所有线程(Stop the World)直至收集结束。简单高效,单个CPU环境中无线程交互,自然高效。Client模式下的虚拟机的很好选择。
用于新生代中,使用复制算法。

ParNew收集器

Serial收集器的多线程版本,除了使用多线程进行垃圾收集以外,其余和serial完全一样。
用于新生代,复制算法。运行在Server模式下的虚拟机首选,因为除了Serial以外只有它能和CMS收集器配合工作。默认开启的收集线程数与CPU数量相同。

Parallel Scavenge收集器

**新生代收集器,复制算法。并行的多线程收集器。**JDK1.4
Parallel Scavenge收集器主要是为了达到一个可控制的吞吐量。
所谓的吞吐量,即运行用户代码的时间与CPU总消耗时间的比值(运行用户代码时间/(运行用户代码时间+垃圾收集时间))
高吞吐量可以高效的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
因此,Parallel Scavenge收集器也是一个“吞吐量优先”收集器。它具有GC自适应调节策略,可以动态调整参数以提供最合适的停顿时间或者最大的吞吐量停顿时间(GC停顿时间不能过短,缩短是牺牲吞吐量和新生代空间来换取的)。

Serial Old收集器

Serial对应的老年代收集器,单线程,client模式下,采用标记整理算法。Stop the world!
在Server模式下,它主要有两大用途:JDK1.5之前可以和Parallel Scavenge配合使用(老年代无法充分利用服务器多CPU的处理能力,不一定获得吞吐量最大化);还可以作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器

Parallel Scavenge对应的老年代收集器。多线程,标记整理算法。Server模式下。JDK1.6才开始提供。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

CMS收集器

Concurrent Mark Sweep:以获取最短回收停顿时间为目标的并发收集器。基于标记清楚算法。
JDK1.5。老年代,其新生代只能选择Serial和ParNew。
尽可能地缩短垃圾收集时用户线程的停顿时间。
分为四个步骤:初始标记,并发标记,重新标记,并发清楚。
初始标记和重新标记都得Stop the world。
初始标记是标记GCroots能直接关联的对象,速度很快;
并发标记是GC Roots Tracing的过程;
重新标记是为了修正并发标记期间因用户现场继续运作而导致标记产生变动的那部分对象的标记记录,停顿时间比初始标记长,但比并发时间短。
总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点:
1。对CPU资源非常敏感,默认启动回收线程为(CPU数量+3)/4。提出 增量式并发收集器i-CMS来处理:在并发标记清理时让GC线程、用户线程交替运行,尽量减少GC线程独占资源时间,这样虽然过程更长,但对用户线程影响较小。
2。CMS收集器无法处理浮动垃圾,可能出现“Concurrent Model Failure”失败而导致另一次Full GC 的发生。即并发清理阶段用户线程还在运行着,会产生新垃圾但出现在标记过程之后,无法再当次被回收,只能等待下一次,这就是浮动垃圾。失败后就会启动后备预案:启动SerialOld收集器来重新进行老年代垃圾回收。停顿时间就会延长。
3。标记清除算法,导致碎片化空间。可能提前触发一次Full GC。
内存整理是无法并发的,可以解决碎片化,但停顿时间又延长了。

G1收集器(!!!)

JDK1.7u4,是面向服务端应用的垃圾收集器。以期未来可以替换CMS,新生代和老年代都可以进行。
特点:
并行与并发:充分利用多CPU、多核环境下的硬件优势,多CPU来缩短停顿时间。
分代收集:采用不同方式去处理新建对象以及熬过多次GC的旧对象以获得更好的收集效果;
空间整合:整体上是标记整理,局部是复制;有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。将Java堆分为多个大小相等的独立区域,新生代和老年代不再是物理隔离的。
可预测的停顿:除了追求低停顿,还能建立可预测的停顿时间模型。
运作步骤:初始标记、并发标记、最终标记、筛选回收。
最终标记需要停顿线程,但是可以并行执行。

说说并发与并行

并发,指的是能在一个或多个处理器上同时处理多个任务。所谓的“同时”只是一种假象,在同一时刻只有一条指令执行,但多个进程指令被快速的轮换执行,使得宏观上具有多个进程同时执行的效果
并发的目的是充分利用处理器以达到最高的处理性能。
并行,指的是多个处理器同时处理多个任务。同一时刻多个事件在不同的处理器上发生。

内存如何进行分配的

对象的分配,主要就是在堆上进行分配。

首先,对象优先在Eden区中进行分配

若启动了本地线程分配缓冲,则按线程优先在TLAB上分配。若Eden区没有足够内存时,将发起一次Minor GC.
三个参数:-Xms20M -Xmx20M -Xmn10M
前两个表示Java堆的最大最小值,相等则不可扩展。Xmn表示新生代大小。剩余的就是老年代大小了。一般情况下,Xms和Xmx的比例为1:1。

分配担保机制

对象分配时,发现Eden区不足以放下对象,因此发生了Minor GC。GC期间又发现已有的Survivor区也无法放下对象,所以只能通过分配担保机制提前转移到老年代。
在发生MinorGC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立,那么MinorGC是安全的。如果不成立,会去检测HandlePromotionFailure的设置值是否允许担保失败。如果允许,就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试一次MinorGC(有风险,因为出现大量对象存活,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代),如果小于,或者HandlePromotionFailure设置不允许冒险,这时要进行一次FullGC.

其次,大对象直接进入老年代

需要大量连续内存空间的Java对象,即大对象,如很长的字符串或数组。
设置-XX:PretenureSizeThreshold,大于这个值的对象将直接进入老年代。这样是为了避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用的是复制算法)
仅对Serial和ParNew收集器有效,其他的无这个参数。

长期存活的对象将进入老年代

每个对象有一个对象计数器,经过一次MinorGC后能存活并且能被Survivor容纳,就被移动到Survivor区,计数器值为1,在Survivor区每经过一次MinorGC还存活的话,计数器+1。超过一定程度(默认为15)对象就将进入老年代中。
用-MaxTenuringThreshold来设置。

动态对象年龄判断

为了能更好适应不同程序的内存状况,对象并不是一定要等到MaxTenuringThreshold才能晋升到老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold。

说说虚拟机的类加载机制

所谓的虚拟机类加载机制,指的就是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行检验、转换解析和初始化,最终形成被虚拟机直接使用的Java类型。
类从被加载到内存开始到卸载处内存为止,整个生命周期包括加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备和解析统称为连接。
其中加载、验证准备、初始化和卸载这5个阶段的顺序是确定的,而解析不一定,它在某些情况下可以在初始化之后在开始(运行时绑定)。

加载

1、通过一个类的全限定名来获取其定义的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

注意,这里第1条中的二进制字节流并不只是单纯地从Class文件中获取,比如它还可以从Jar包中获取、从网络中获取(最典型的应用便是Applet)、由其他文件生成(JSP应用)等。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

类加载器

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。
站在Java虚拟机的角度来讲,只存在两种不同的类加载器:
启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分
所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
启动类加载器:Bootstrap ClassLoader,跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器
应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。
这里写图片描述

双亲委派模式

这种层次关系称为类加载器的双亲委派模型。我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这便保证了Object类在程序中的各种类加载器中都是同一个类。

双亲委派模式的破坏

由于用户对程序动态性的追求而导致的。代码热替换,模块热部署等。。。

验证

验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
* 2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。*
假设一个类变量的定义为:
public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
这里还需要注意如下几点:
对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过
对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
假设上面的类变量value被定义为:
public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。

解析

解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。前面说解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。
对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。
1、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
2、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束,
3、类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。
4、接口方法解析:与类方法解析步骤类似,知识接口不会有父类,因此,只递归向上搜索父接口就行了。

初始化

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器()方法的过程。
这里简单说明下()方法的执行规则:
1、()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
2、()方法与实例构造器()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。因此,在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object。
3、()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成()方法。
4、接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成()方法。但是接口鱼类不同的是:执行接口的()方法不需要先执行父接口的()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。
5、虚拟机会保证一个类的()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

静态代码块等的加载顺序

–>先初始化父类的静态代码
—>初始化子类的静态代码
–>初始化父类的非静态代码
—>初始化父类构造函数
—>初始化子类非静态代码
—>初始化子类构造函数
静态代码指静态变量,静态代码块。
静态方法是调用时才加载。