类的加载
Java虚拟机规范中,没有强制约束什么时候要开始加载,但是,却严格规定了几种情况
必须进行初始化(加载,验证,准备则需要在初始化之前开始):
- 遇到 new、getstatic、putstatic、或者invokestatic 这4条字节码指令,如果没有类没有进行过初始化,则触发初始化。
- 使用java.lang.reflect包的方法,对垒进行反射调用的时候,如果没有初始化,则先触发初始化
- 初始化一个类时候,如果发现父类没有初始化,则先触发父类的初始化。
类从被加载到虚拟机内存开始,直到卸载出内存为止,它的整个生命周期包括:
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中,验证、准备和解析统称为连接(Linking)。过程如下图所示:

加载
加载阶段会做3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
验证
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
准备
为类的静态变量分配内存,并初始化默认值,这些内存是在方法区中分配,需要注意以下几点:
- 此处内存分配的变量仅包含类变量(static),而不包括实例变量,实例变量会随着对象实例化被分配在java堆中。
- 这里默认值是数据类型的默认值(如0、0L、null、false),而不是代码中被显示的赋予的值。
- 如果类字段的字段属性表中存在ConstatntValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化
初始化算是类加载过程的最后一个阶段,在这个阶段在是真正的开始有java代码主导。将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
类加载器
类加载器除了能用来加载类,还能用来作为类的层次划分。Java自身提供了3种类加载器
- 启动类加载器(Bootstrap ClassLoader),它是属于虚拟机自身的一部分,用C++实现的,主要负责加载<JAVA_HOME>\lib目录中或被-Xbootclasspath指定的路径中的并且文件名是被虚拟机识别的文件。它等于是所有类加载器的爸爸。
- 扩展类加载器(Extension ClassLoader),它是Java实现的,独立于虚拟机,主要负责加载<JAVA_HOME>\lib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。
- 应用程序类加载器(Application ClassLoader),它是Java实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器那这玩意就是我们程序中的默认加载器。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
双亲委派模型

上面图片所展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证Java 核心库的类型安全,比如,加载位于rt.jar包中的 java.lang.Object类,不管是哪个加载器加载这个类,最终都是委托给顶层的BootstrapClassLoader来加载的,这样就可以保证任何的类加载器最终得到的都是同样一个Object对象。
破坏双亲委派模型:
你先得知道SPI(Service Provider Interface),它和API不一样,它是面向拓展的,也就是我定义了这个SPI,具体如何实现由扩展者实现。我就是定了个规矩。
Java弄了个线程上下文类加载器,通过setContextClassLoader()默认情况就是应用程序类加载器然后Thread.current.currentThread().getContextClassLoader()获得类加载器来加载。
Java中所有涉及SPI(Service Provider Interface)的加载动作基本上都采用这种方式(线程上下文类加载器,可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动 作,),例如JNDI、JDBC、JCE、JAXB和JBI等。
对象的创建
创建对象(克隆、反序列化)一般是一个newkeyword而已,而在虚拟机中,对象的创建步骤例如以下:
①当虚拟机遇到new指令时。首先将去检查这个指令參数能否在常量池中定位到一个类的引用符号,而且检查这个符号引用代表的类是否被载入、解析和初始化过。假设没有。那必须先执行相应的类载入过程。②在类载入检查通过以后。接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类载入后便确定。为对象分配空间的任务等同于把一块确定大小的内存从Java堆划分出来。
②.①创建对象的过程其实也是一个非线程安全的过程,所以也需要考虑线程安全的问题。可能出现正在给对象A分配内存,指针还没来得及改动,对象B又同一时候使用了原来的指针来分配内存的情况。解决这一问题的方案是:
- 方案一、对分配内存空间的动作进行同步处理--实际上虚拟机採用CAS配上失败重试的方式,保证更新操作原子性 。
- 方案二、把内存分配的动作依照线程划分在不同空间之中进行。即每一个线程在Java堆中预先分配一小块内存。称为本地线程分配缓存(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,仅仅有TLAB用完并分配新的TLAB时,才须要同步锁定。虚拟机是否使用TLAB,能够通过-XX:+/-UseTLAB參数来设定。
③内存分配完毕以后。虚拟机会将分配到的内存空间都初始化为零值(不包括对象头),假设使用TLAB,这一工作过程也能够提前至TLAB分配时进行,这一步操作保证了对象实例字段在Java代码中能够不赋初始值就能直接使用,程序能訪问到这些字段的数据类型所相应的零值。
④接下来虚拟机要对对象进行必要的设置,比如:这个对象是哪个类的实例、怎样才干找到类的元数据信息、对象的哈希码、对象GC分代年龄信息等。这些信息存放在对象的信息头之中。依据虚拟机执行状态的不同。如是否使用偏向锁等,对象头会有不同的设置方式。
上述工作完毕以后,从虚拟机角度来看,一个新的对象已经产生了,可是从Java程序来看,对象才刚刚开始——(init)方法还没有执行。全部的字段都还为零,所以,一般来说。执行new命令后。会接着执行init方法。把对象依照程序猿的意愿进行初始化,这样一个真正可用的对象才算全然产生出来。
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
HotSpot虚拟机的对象头包括两部分信息:
第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等,这部分数据称为Mark Word。
对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类 型的字段内容。
对齐填充:对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
对象的访问定位
使用对象时Java程序需要通过栈上的reference数据来操作堆上的具体对象。
目前主流的访问方式有使用句柄和直接指针两种。
- 如果使用句柄访问的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 如图所示:

- 如果使用直接指针访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,如图所示:

各自优势:
使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。就本书讨论的主要虚拟机Sun HotSpot而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
对象是否“已死”
引用计数算法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。(一般面试问和教科书上的解释的都是这个。)
可达性分析算法:
在主流程序语言(Java、C#)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么它们就会被行刑(清除)。