1. 说一下字节码的编译过程

大部分代码从编译到转化为虚拟机能执行的指令集之前,都会按照下图的步骤进行

image.png
其中绿色的模块是可以选择性实现的

  • 上图中间的那条分支是解释执行的过程(一条字节码一条字节码的解释执行)
  • 下面的那条分支就是传统编译原理中从源代码到机器代码的生成过程。

编译器可以分为:前端编译器、JIT 编译器和AOT编译器
image.png

前端编译器

javac 编译器的处理过程可以分为下面四个阶段:

  1. 第一个阶段:词法、语法分析。在这个阶段,JVM 会对源代码的字符进行一次扫描,最终生成一个抽象的语法树。简单地说,在这个阶段 JVM 会搞懂我们的代码到底想要干嘛。就像我们分析一个句子一样,我们会对句子划分主谓宾,弄清楚这个句子要表达的意思一样。
  2. 第二个阶段:填充符号表。我们知道类之间是会互相引用的,但在编译阶段,我们无法确定其具体的地址,所以我们会使用一个符号来替代。在这个阶段做的就是类似的事情,即对抽象的类或接口进行符号填充。等到类加载阶段,JVM 会将符号替换成具体的内存地址。
  3. 第三个阶段:注解处理。我们知道 Java 是支持注解的,因此在这个阶段会对注解进行分析,根据注解的作用将其还原成具体的指令集。
  4. 第四个阶段:分析与字节码生成。到了这个阶段,JVM 便会根据上面几个阶段分析出来的结果,进行字节码的生成,最终输出为 class 文件。

我们一般称 javac 编译器为前端编译器,因为其发生在整个编译的前期。常见的前端编译器有 Sun 的 javac,Eclipse JDT 的增量式编译器(ECJ)。

JIT 编译器:从字节码到机器码

当源代码转化为字节码之后,其实要运行程序,有两种选择。一种是使用 Java 解释器解释执行字节码,另一种则是使用 JIT 编译器将字节码转化为本地机器代码。
这两种方式的区别在于,前者启动速度快但运行速度慢,而后者启动速度慢但运行速度快。至于为什么会这样,其原因很简单。因为解释器不需要像 JIT 编译器一样,将所有字节码都转化为机器码,自然就少去了优化的时间。而当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。所以在实际情况中,为了运行速度以及效率,我们通常采用两者相结合的方式进行 Java 代码的编译执行。
image.png
在 HotSpot 虚拟机内置了两个即时编译器,分别称为 Client Compiler 和Server Compiler。这两种不同的编译器衍生出两种不同的编译模式,我们分别称之为:C1 编译模式,C2 编译模式。

AOT 编译器:源代码到机器码

AOT 编译器的基本思想是:在程序执行前生成 Java 方法的本地代码,以便在程序运行时直接使用本地代码
但是 Java 语言本身的动态特性带来了额外的复杂性,影响了 Java 程序静态编译代码的质量。例如 Java 语言中的动态类加载,因为 AOT 是在程序运行前编译的,所以无法获知这一信息,所以会导致一些问题的产生。

2. 类如何加载的

类的加载过程如下图所示:
image.png
工作机制
类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件。在Java中,类装载器把一个类装入JVM中,要经过以下步骤:

(1) 装载:查找和导入Class文件; (2) 连接:把类的二进制数据合并到JRE中; (a)校验:检查载入Class文件数据的正确性; (b)准备:给类的静态变量分配存储空间; (c)解析:将符号引用转成直接引用; (3) 初始化:对类的静态变量,静态代码块执行初始化操作

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这个顺序来按部就班地开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段后再开始。 类的生命周期的每一个阶段通常都是互相交叉混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。
类的加载
类加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
jvm进行类加载阶段都做了什么。虚拟机需要完成以下三件事情:
1).通过一个类的全限定名称来获取定义此类的二进制字节流。
2).将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3).在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
相对于类加载过程的其他阶段,加载阶段是开发期相对来说可控性比较强,该阶段既可以使用系统提供的类加载器完成,也可以由用户自定义的类加载器来完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式
验证
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
1)文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
2)元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
3)字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
4)符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
1)这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2)这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。
直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
1)、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
2)、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。
3)、类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。
4)、接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此,只递归向上搜索父接口就行了。
初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了加载(Loading)阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。 初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量时指定初始值
②使用静态代码块为类变量指定初始值
JVM初始化步骤

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

初始化阶段时执行类构造器方法的过程。
1)类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。
2)类构造器方法与类的构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的类构造器方法执行之前,父类的类构造器方法已经执行完毕,因此在虚拟机中第一个执行的类构造器方法的类一定是java.lang.Object。
3)由于父类的类构造器方法方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
4)类构造器方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成类构造器方法。
5)接口中可能会有变量赋值操作,因此接口也会生成类构造器方法。但是接口与类不同,执行接口的类构造器方法不需要先执行父接口的类构造器方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也不会执行接口的类构造器方法。
6)虚拟机会保证一个类的类构造器方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的类构造器方法,其它线程都需要阻塞等待,直到活动线程执行类构造器方法完毕。如果在一个类的类构造器方法中有耗时很长的操作,那么就可能造成多个进程阻塞。
结束生命周期
在以下情况的时候,Java虚拟机会结束生命周期
1). 执行了System.exit()方法
2). 程序正常执行结束
3). 程序在执行过程中遇到了异常或错误而异常终止
4). 由于操作系统出现错误而导致Java虚拟机进程终止

3. 类的初始化几种情况

对于初始化阶段虚拟机规范是严格规定了如下几种情况,如果类未初始化会对类进行初始化。

  1. 创建类的实例
  2. 访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。
  3. 访问类的静态方法
  4. 反射如(Class.forName(“my.xyz.Test”))
  5. 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
  6. 虚拟机启动时,定义了main()方法的那个类先初始化

以上情况称为称对一个类进行“主动引用”,除此种情况之外,均不会触发类的初始化,称为“被动引用” 接口的加载过程与类的加载过程稍有不同。接口中不能使用static{}块。当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。
被动引用例子

  1. 子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。。对于静态字段,只有直接定义这个字段的类才会被初始化.
  2. 通过数组定义来引用类,不会触发类的初始化
  3. 访问类的常量,不会初始化类
  1. class SuperClass {
  2. static {
  3. System.out.println("superclass init");
  4. }
  5. public static int value = 123;
  6. }
  7. class SubClass extends SuperClass {
  8. static {
  9. System.out.println("subclass init");
  10. }
  11. }
  12. public class Test {
  13. public static void main(String[] args) {
  14. System.out.println(SubClass.value);// 被动应用1
  15. SubClass[] sca = new SubClass[10];// 被动引用2
  16. }
  17. }

4. 类初始化顺序

下面是类初始化的一些规则:

1.类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化 2.超类早于子类和衍生类的初始化 3.如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的4.初始化即使静态域被子类或子接口或者它的实现类所引用。 5.接口初始化不会导致父接口的初始化。 6.静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前。 7.非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了非静态或实例变量(父类)初始化早于子类

一般顺序:静态块(静态变量)——>成员变量——>构造方法——>静态方法
总结:
如果类还没有被加载:
1、先执行父类的静态代码块和静态变量初始化,并且静态代码块和静态变量的执行顺序只跟代码中出现的顺序有关。
2、执行子类的静态代码块和静态变量初始化。
3、执行父类的实例变量初始化
4、执行父类的构造函数
5、执行子类的实例变量初始化
6、执行子类的构造函数
如果类已经被加载:
则静态代码块和静态变量就不用重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。

5. Java为什么要设计双亲委派模型?

从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。
大部分Java程序一般会使用到以下三种系统提供的类加载器:
1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。该类加载器无法被Java程序直接引用
2)扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。该加载器可以被开发者直接使用。
3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

使用双亲委托机制这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。 在rt.jar包中的java.lang.ClassLoader类中,我们可以查看类加载实现过程的代码,具体源码如下:

  1. protected synchronized Class loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException {
  3. // 首先检查该name指定的class是否有被加载
  4. Class c = findLoadedClass(name);
  5. if (c == null) {
  6. try {
  7. if (parent != null) {
  8. // 如果parent不为null,则调用parent的loadClass进行加载
  9. c = parent.loadClass(name, false);
  10. } else {
  11. // parent为null,则调用BootstrapClassLoader进行加载
  12. c = findBootstrapClass0(name);
  13. }
  14. } catch (ClassNotFoundException e) {
  15. // 如果仍然无法加载成功,则调用自身的findClass进行加载
  16. c = findClass(name);
  17. }
  18. }
  19. if (resolve) {
  20. resolveClass(c);
  21. }
  22. return c;
  23. }

通过上面代码可以看出,双亲委派模型是通过loadClass()方法来实现的,根据代码以及代码中的注释可以很清楚地了解整个过程其实非常简单:先检查是否已经被加载过,如果没有则调用父加载器的loadClass()方法,如果父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,则先抛出ClassNotFoundException,然后再调用自己的findClass()方法进行加载。

6. 什么时候需要自定义类加载器?如何自定义 ClassLodader ,实现过程说一下

自定义类加载器的场景

  • 对于自定义路径中的class类文件的加载,我们需要自己的ClassLoader。
  • 有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,例如字节码是放在数据库、甚至是在云端,从指定的来源加载类。
  • 可以定义类的实现机制,实现类的热部署,如OSGi中的bundle模块就是通过实现自己的ClassLoader实现的。
  • 安全性考虑:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。

自定义类加载器
流程概括为:继承ClassLoader对象,覆盖findClass方法,这个方法的作用就是找到.class文件,转换成字节数组,调用defineClass对象转换成Class对象返回。
若要实现自定义类加载器,只需要继承java.lang.ClassLoader 类,并且重写其findClass()方法即可。java.lang.ClassLoader 类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class 类的一个实例。除此之外,ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等
注意:在JDK1.2之前,类加载尚未引入双亲委派模式,因此实现自定义类加载器时常常重写loadClass方法,提供双亲委派逻辑,从JDK1.2之后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass方法,而推荐重写findClass方法。

  1. /**
  2. * 一、ClassLoader加载类的顺序
  3. * 1.调用 findLoadedClass(String) 来检查是否已经加载类。
  4. * 2.在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。
  5. * 3.调用 findClass(String) 方法查找类。
  6. * 二、实现自己的类加载器
  7. * 1.获取类的class文件的字节数组
  8. * 2.将字节数组转换为Class类的实例
  9. */
  10. public class ClassLoaderTest {
  11. public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
  12. //新建一个类加载器
  13. MyClassLoader cl = new MyClassLoader("myClassLoader");
  14. //加载类,得到Class对象
  15. Class<?> clazz = cl.loadClass("classloader.Animal");
  16. //得到类的实例
  17. Animal animal=(Animal) clazz.newInstance();
  18. animal.say();
  19. }
  20. }
  21. class Animal{
  22. public void say(){
  23. System.out.println("hello world!");
  24. }
  25. }
  26. class MyClassLoader extends ClassLoader {
  27. //类加载器的名称
  28. private String name;
  29. //类存放的路径
  30. private String path = "E:\\workspace\\Algorithm\\src";
  31. MyClassLoader(String name) {
  32. this.name = name;
  33. }
  34. MyClassLoader(ClassLoader parent, String name) {
  35. super(parent);
  36. this.name = name;
  37. }
  38. /**
  39. * 重写findClass方法
  40. */
  41. @Override
  42. public Class<?> findClass(String name) {
  43. byte[] data = loadClassData(name);
  44. return this.defineClass(name, data, 0, data.length);
  45. }
  46. public byte[] loadClassData(String name) {
  47. try {
  48. name = name.replace(".", "//");
  49. FileInputStream is = new FileInputStream(new File(path + name + ".class"));
  50. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  51. int b = 0;
  52. while ((b = is.read()) != -1) {
  53. baos.write(b);
  54. }
  55. return baos.toByteArray();
  56. } catch (Exception e) {
  57. e.printStackTrace();
  58. }
  59. return null;
  60. }
  61. }

7. 自定义类加载器怎么实现,其中哪个方法走双亲委派模型,哪个不走,不走的话怎么加载类。

自定义类加载器:继承ClassLoader对象,覆盖findClass方法,这个方法的作用就是找到.class文件,转换成字节数组,调用defineClass对象转换成Class对象返回。

双亲委派模型的实现依赖于loadClass方法:
1. 如果不想不破坏双亲委派模型,只要去重写findClass方法,不需重写loadClass(String name)方法。一般用defineClass加载外部类。
2. 如果想要去破坏双亲委派模型,需要去重写loadClass方法

不破坏双亲委派模型,重写findClass方法

  1. public class ProtectedClassLoader extends ClassLoader {
  2. public ProtectedClassLoader(ClassLoader parent){
  3. super(parent);
  4. }
  5. @Override
  6. protected Class<?> findClass(String name) throws ClassNotFoundException {
  7. byte[] by = null;
  8. try {
  9. by = getByteByClassName(name);
  10. return defineClass(name, by, 0, by.length);
  11. } catch (IOException e) {
  12. e.printStackTrace();
  13. }
  14. return super.findClass(name);
  15. }
  16. @Override
  17. public InputStream getResourceAsStream(String name) {
  18. return super.getResourceAsStream(name);
  19. }
  20. private byte[] getByteByClassName(String name) throws IOException {
  21. File file = new File("D:\\MyTest.class");
  22. InputStream is = new FileInputStream(file);
  23. byte[] by = new byte[is.available()];
  24. is.read(by);
  25. is.close();
  26. return by;
  27. }
  28. public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException, IOException {
  29. ClassLoader ml = new ProtectedClassLoader(ClassLoader.getSystemClassLoader().getParent());
  30. Object obj = Class.forName("com.gold.aip.test.MyTest", true, ml).newInstance();
  31. System.out.println(obj);
  32. System.out.println(obj.getClass().getClassLoader());
  33. }
  34. }
  35. //输出结果
  36. com.gold.aip.test.MyTest@b03be0
  37. com.gold.aip.test.ProtectedClassLoader@14e8cee

破坏双亲加载模型自定义类加载器

  1. package cn.erong.test;
  2. import java.io.InputStream;
  3. public class Test {
  4. public static void main(String[] args) throws Exception {
  5. ClassLoader myloader = new ClassLoader() {
  6. @Override
  7. public Class<?> loadClass(String name)
  8. throws ClassNotFoundException {
  9. String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
  10. InputStream is;
  11. try {
  12. is =getClass().getResourceAsStream(fileName);
  13. if(is==null) {return super.loadClass(fileName);}
  14. byte[] by = new byte[is.available()];
  15. is.read(by);
  16. is.close();
  17. return defineClass(name, by, 0, by.length);
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. return null;
  22. }
  23. };
  24. myloader.loadClass("cn.erong.test.Test").newInstance();
  25. }
  26. }
  27. //运行结果
  28. java.lang.ClassNotFoundException: Object.class
  • 测试类Test,继承Object,因为破坏了双亲加载模型,Object类也会使用这个加载器加载,从Classpath下找这个类,必然会出错。
    * 断点测试,Object类是后加载的,因为父类永远比子类先初始化,所以可以看出父类是后加载,但是先解析,初始化

8. 能不能自己写个类叫java.lang.System?

通常不可以,但可以采取另类方法达到这个需求。
解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

9. JVM的编译优化

10. 新创建一个对象在jvm如何存储的

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
image.png

对象的存储布局
**
二、实例数据(Instance Data)
实例数据部分是对象真正存储的有效信息,也既是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录下来。
这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。
HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果 CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。

三、对齐填充(Padding)
对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。对象头正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

对象的创建过程
Java是一门面向对象的编程语言,Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常(例外:克隆、反序列化)仅仅是一个 new关键字而已,而在虚拟机中,对象(本文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个过程呢?
虚拟机遇到一条new指令时,
1、首先jvm要检查类A是否已经被加载到了内存,即类的符号引用是否已经在常量池中,并且检查这个符号引用代表的类是否已被加载、解析和初始化过的。如果还没有,需要先触发类的加载、解析、初始化。然后在堆上创建对象。
2、为新生对象分配内存。
  对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务具体便等同于一块确定大小 的内存从Java堆中划分出来,怎么划呢?假设Java堆中内存是绝对规整的,所有用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作 为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)。如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因 此在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的 收集器时(说明一下,CMS收集器可以通过UseCMSCompactAtFullCollection或 CMSFullGCsBeforeCompaction来整理内存),就通常采用空闲列表。
除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是 线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存。解决这个问题有两个方案,一种是对分配内存空 间的动作进行同步——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行, 即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区,(TLAB ,Thread Local Allocation Buffer),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完,分配新的TLAB时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
3. 完成实例数据部分的初始化工作(初始化为0值)
  内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB的话,这一个工作也可以提前至TLAB分配时进行。这 步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4、 完成对象头的填充:如对象自身的运行时数据、类型指针等。
  接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
在上面工作都完成之后,在虚拟机的视角来看,一个新的对象已经产生了。但是在Java程序的视角看来,初始化才正式开始,开始调用方法完成初始复制和构造函数,所有的字段都为零值。因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着就是执 行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。

https://www.cnblogs.com/duanxz/p/4967042.html