类加载过程类加载的五个过程:加载、验证、准备、解析、初始化。加载在加载阶段,虚拟机主要完成三件事:通过类的全限定名获取定义该类的二进制字节流。将这个字节流表示的静态存储结构转换成方法区的运行时数据结构。在Java堆中生成一个代表该类的java.lang.Class对象作为方法区数据的访问入口。Verification验证阶段的作用是确保Class文件的字节流中包含的信息符合JVM规范,不会对JVM造成危害。如果验证失败,则抛出java.lang.VerifyError异常或其子类。验证过程分为四个阶段:文件格式验证:验证字节流文件是否符合Class文件格式的规范,是否能被当前虚拟机正确处理。元数据校验:就是对字节码所描述的信息进行语义分析,确保其描述的信息符合Java语言的规范要求字节码校验:主要分析数据流和控制流,保证方法无害虚拟机在运行时。符号引用验证:符号引用验证发生在虚拟机将符号引用转换为直接引用时,这种转换将发生在解析阶段。准备准备阶段为变量分配内存并设置类变量的初始化。这个阶段只赋值类变量(静态修饰变量),不包括类实例变量。对于非final变量,JVM会将它们设置为“零值”而不是其赋值语句的值:pirvatestaticintsize=12;。那么在这个阶段,size的值是0,而不是12。但是final修饰的类变量会被赋予真实的值。解析过程是将常量池中的符号引用替换为直接引用。主要包括对四类参考文献的分析。类或接口分析,字段分析,方法分析,接口方法分析。初始化在准备阶段,类变量已经被初始化了一次。在此阶段,类变量和其他资源通过程序制定的计划进行初始化。这些资源包括静态{}块、构造函数、父类的初始化等,至于使用和卸载阶段,这里就不用过多解释了。使用过程按照程序定义的行为执行,卸载由GC完成。双亲委托模型类加载器按照级别分为以下三种,从顶层到底层:引导类加载器(BootstrapClassLoader)该类加载器负责加载%JRE_HOME下的rt.jar、resources.jar、charsets%\lib.jar和class等。可以通过System.getProperty("sun.boot.class.path")查看加载的路径。扩展类加载器(ExtensionClassLoader)负责加载%JRE_HOME%\lib\ext目录下的jar包和类文件。也可以通过System.out.println(System.getProperty("java.ext.dirs"))查看加载的类文件的路径。应用类加载器(ApplicationClassLoader)该加载器是ClassLoader中getSystemClassLoader()方法的返回值,所以一般称为系统类加载器。它负责加载用户类路径(Classpath)上指定的类库。这个loader可以直接使用。如果应用程序没有自定义自己的类加载器,这一般是程序中默认的类加载器。上图只是类加载顺序,与类继承无关。ExtClassLoader,AppClassLoader继承URLClassLoader,URLClassLoader继承ClassLoader。BoopStrapClassLoder是用C/C++写的,它本身就是虚拟机的一部分,不是java类。AppClassLoader的父加载器为ExtClassLoader,ExtClassLoader的父加载器为null,BoopStrapClassLoader为顶层加载器。如果一个类加载器收到一个类加载请求,它不会首先尝试加载该类,而是把这个请求委托给父类加载器完成,每一级类加载器都是如此,所以所有的加载请求都应该最终被传递给顶层的启动类加载器,只有当父类加载器报告它无法完成时,子加载器才会在发出这个请求时尝试加载自己(在它的搜索范围内没有找到需要的类)。对应的实现逻辑:首先检查类是否已经加载,如果没有,则调用父加载器的loadClass方法,如果父加载器为空,则默认使用启动类加载器作为父加载器。如果父加载器加载失败,则抛出异常,然后调用自己的findClass方法加载。具体例子:如果我们自定义Test类文件,当jvm要加载Test.class时:首先会在自定义加载器中搜索,看是否已经加载。如果已经加载,则返回字节码。如果自定义加载器还没有加载,那么就询问之前的加载器(即AppClassLoader)是否加载了Test.class。如果还没有加载,则询问之前的加载器(ExtClassLoader)是否已经加载。如果还没有加载,则继续询问是否加载了上一层加载(BoopStrapClassLoader)。如果BoopStrapClassLoader还是没有加载,则到指定的类加载路径(“sun.boot.class.path”)检查是否有Test.class字节码,有则返回,下一层加载器ExtClassLoader不通知在自己指定的类加载路径(java.ext.dirs)下查看。以此类推,直到自定义类加载器指定的路径还没有找到Test.class字节码,才会抛出异常ClassNotFoundException。双亲委派的好处Java类与其类加载器具有优先级层次关系。例如,类Object放在rt.jar中。无论哪个类加载器要加载这个类,最终都会委托给启动类加载器加载。因此,Object类在程序的各种类加载器环境中。同一个班级。判断两个类是否相同是通过classloader.class来完成的,所以即使两个类加载器加载同一个类文件,也是不同的类。实现自己的loader,只需要继承ClassLoader,重写findClass方法即可。对象创建过程对象的过程1.类加载检查JVM遇到一条新的指令时,首先检查这条指令的参数是否可以在常量池中定位到某个类的符号引用,并检查该类所代表的类是否存在符号引用已被加载、解析和初始化。如果没有,则必须先执行相应的类加载过程。2.对象分配内存对象所需内存的大小在类加载完成后就完全确定了(对象内存布局)。为对象分配空间的任务相当于从Java堆中分出一块一定大小的内存。根据Java堆是否规则分配内存有两种方式:(Java堆是否规则取决于所使用的垃圾收集器是否具有compaction功能)。指针碰撞(Bumpthepointer)Java堆中的内存是有规律的。所有已用内存放在一边,空闲内存放在另一边,中间放一个指针作为分界点的指示。分配内存是将指针移动到空闲空间,距离等于内存大小。例如:Serial和ParNew等收集器。空闲列表(FreeList)Java堆中的内存是不规则的,已用内存和空闲内存是相互交错的,所以没有办法简单的指针碰撞。虚拟机必须维护一个列表,记录有哪些内存块可用,分配时从列表中找到足够大的空间分配给对象实例,并更新列表上的记录。例如:CMS,一个基于Mark-Sweep算法的收集器。3.并发处理是在虚拟机中创建对象时非常频繁的行为。即使只是修改指针指向的位置,在并发情况下也不是线程安全的。有可能正在为对象A分配内存,而指针还没有来得及修改,对象B同时使用原来的指针分配内存。这个问题有两种解决方案:同步虚拟机使用失败重试的CAS来保证更新操作的原子性。ThreadLocalAllocationBuffer(TLAB)根据线程将内存分配动作划分到不同的线程中。空间,即每个线程在Java堆中预先分配一小块内存(TLAB)。哪个线程要分配内存,就在那个线程的TLAB上分配。仅当TLAB用完并分配新的TLAB时才需要同步锁。虚拟机是否使用TLAB可以通过-XX:+/-UseTLAB参数设置。4.内存空间初始化虚拟机将分配的内存空间初始化为零值(不包括对象头)。如果使用TLAB,这个工作过程也可以在分配TLAB时提前进行。内存空间初始化保证了对象的实例字段可以在Java代码中直接使用而无需赋初值,程序可以访问到这些字段的数据类型对应的零值。注意:类的成员变量可以不显示初始化(Java虚拟机会先自动初始化为默认值)。如果方法中的局部变量只负责接收表达式的值,则不需要初始化,但涉及计算、直接输出等其他情况的局部变量则需要初始化。5.对象设置虚拟机对对象进行必要的设置,比如对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC世代年龄,和其他信息。此信息存储在对象的对象头中。6.执行init()以上工作完成后,从虚拟机的角度来看,已经生成了一个新的对象。但是从Java程序的角度来看,对象的创建才刚刚开始。init()方法尚未执行,所有字段仍为零。因此,一般来说(由字节码是否遵循invokespecial指令决定),在执行完new指令后会执行init()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象就生成出来了.对象的内存布局在HotSpot虚拟机中。对象在内存中存储的布局分为:对象头实例数据对齐填充HotSpot虚拟机的对象头包括两部分信息:运行时数据和类型指针。运行时数据:用于存储对象本身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。类型指针:对象指向到它的类元数据指针,虚拟机使用这个指针来确定对象是哪个类实例。如果对象是Java数组,那么对象头中肯定也有一段数据记录数组的长度,因为虚拟机可以通过普通Java对象的元数据信息来确定Java对象的大小,但是无法从数组的元数据信息中判断Java对象的大小。确定数组的大小。(并不是所有的虚拟机实现都必须保留对象数据上的类型指针,换句话说,查找对象的元数据并不一定要通过对象本身,可以参考对象的访问位置。)HotSpot层通过markOop实现MarkWord,具体实现位于markOop.hpp文件中。markOop提供了大量查看当前对象头状态和更新对象头数据的方法,为同步锁的实现提供了基础。[比如我们知道synchronized锁是对象而不是代码,锁的状态是保存在对象头中,然后对象就被加锁了]。关于synchronized的进一步介绍可以点击查看:Java多线程锁synchronized实例数据详解实例数据部分是对象实际存储的有效信息,也是定义的各种类型字段的内容在程序代码中。无论是从父类继承还是在子类中定义,都需要记录下来。HotSpot虚拟机的默认分配策略是longs/doubles、ints、shorts/chars、bytes/booleans和oop。从分配策略可以看出,相同宽度的字段总是分配在一起。对齐填充HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。对象头部分恰好是8字节的倍数(1或2倍)。因此,当对象实例数据部分没有对齐时,需要通过对齐填充来完成。对象访问定位Java程序需要通过引用(ref)数据来操作堆上的对象,那么如何通过引用来定位和访问对象的具体位置。对象的访问方式由虚拟机决定。java虚拟机提供了两种主流方式:1.句柄访问对象2.直接指针访问对象。(SunHotSpot使用这种方法)句柄访问简单的说就是java堆分配一块内存作为句柄池,里面存放的是引用中对象的句柄地址,句柄中包含对象实例数据的地址信息和类型数据。优点:引用存储了一个稳定的句柄地址,当对象被移动【垃圾回收时移动对象是正常的】只需要改变句柄中实例数据的指针,而不需要改变引用[ref]本身。在这种直接指针方式下,JVM栈中栈帧中局部变量表存储的引用地址就是实例数据的地址。通过这个引用,可以直接获取到实例数据的地址。实际上,引用指向的内存中的对象数据由两部分组成,一部分是对象实例本身,另一部分是方法区中对象类型的地址。优点:优点很明显,就是速度快,而且和句柄访问相比,减少了一次指针定位的开销时间。由于Java中对象访问非常频繁,所以这种开销加起来也是非常可观的执行成本。就虚拟机SunHotSpot而言,它使用的是第二种方式进行对象访问,但是从整个软件开发来看,各种语言和框架使用句柄访问也是很常见的。
