Java 宇宙大冒险

111 阅读19分钟

在一个遥远的数字宇宙中,有一个神奇的世界叫做 Java 星球。这里的一切都充满了奇幻色彩和神秘规则。

一、Java 对象的诞生传奇

有一天,勇敢的程序员探险家小码在 Java 星球上发出了一个神秘指令,想要创造一个新的 “Java 对象”。这可不得了,整个 Java 星球瞬间忙碌起来。

首先,星球的智慧核心 ——Java 虚拟机(JVM)感受到了这个请求。如果这个对象对应的类还没被加载到内存中,那就得先来一场 “加载大冒险”。就像一个超级快递员,JVM 读取类的字节码文件,把这些神秘的代码解析成二进制数据,然后小心翼翼地放在方法区这个神秘宝库中。

接着是链接阶段,这就像是给新到的宝贝做检查和准备。验证环节,JVM 要确保这个类文件不会有啥坏心思,不会危害星球安全,简直就是个严格的安检员。准备阶段呢,给类的静态变量分配好小房间,先设置个默认初始值,等着后面的精彩变化。解析阶段嘛,就像给神秘符号找对应的宝贝,把符号引用变成直接引用,不过这个步骤有时候也会晚点再做。

然后是初始化类,这时候会执行类构造器() 方法,这个方法可是个神秘的魔法师,由编译器自动生成,把所有静态变量赋值动作和静态代码块里的语句都融合在一起。

当一切准备就绪,就该给新对象分配内存啦。JVM 在堆里努力寻找一块合适的地方,还得保证线程安全,不能让多个线程抢同一个地址,不然可就乱套了。

内存分配好后,初始化对象的大工程开始了。先执行父类构造函数,就像拜访长辈一样,按照继承层次从上到下依次打招呼。接着给实例变量设置初始值,最后执行对象自身的构造函数,把这个新对象打扮得漂漂亮亮。

最后,JVM 把指向这个新对象的引用交给小码,小码就可以通过这个引用和新对象一起玩耍啦。

二、类的生命周期大冒险

在 Java 星球上,每个类都有自己的奇妙生命周期。

加载阶段,就像是把一个神秘宝盒里的东西拿出来放到 Java 星球的方法区。类加载器这个大力士把类的.class 文件中的二进制数据读进内存,然后转换成方法区里的运行时数据结构,还会生成一个代表这个类的 java.lang.Class 对象,就像是给这个类发了一张专属身份证。

验证阶段,一群聪明的小卫士出来检查这个新到的类是不是符合要求,确保不会伤害星球安全。文件格式验证、元数据验证、字节码验证和符号引用验证,一个都不能少。

准备阶段,给类的静态变量分配小房间,先设置个默认初始值,这时候可不管实例变量哦。

解析阶段,把常量池里的符号引用变成直接引用,让大家能更快找到想要的东西。

初始化阶段,执行类构造器() 方法,把静态变量设置成程序员指定的初始值,静态初始化器也开始工作啦。

使用阶段,类就可以被大家尽情使用啦,创建实例、调用静态方法、访问静态变量,热闹非凡。

卸载阶段呢,当一个类不再被任何人需要的时候,就有可能被垃圾回收机制回收,从方法区里消失不见。不过这一般要等应用程序退出或者方法区内存不足的时候才会发生。

三、探索 Java 对象的结构

小码对 Java 星球上的对象结构充满了好奇。

每个对象都有一个神秘的对象头,就像对象的小帽子。里面有 Mark Word 和 Klass Pointer 两部分。Mark Word 就像个万能小助手,占用 8 个字节,存储着对象的哈希码、锁状态标志等各种重要信息。在 32 位的 JVM 上,它可能只有 4 个字节哦。Klass Pointer 也是个重要指针,指向对象所属类的元数据,也就是那个 java.lang.Class 对象,同样占用 8 个字节,32 位 JVM 上就变成 4 个字节啦。

接着是实例数据部分,这里存储着对象的实际属性值,就像对象的小背包。这些数据按照声明顺序摆放,不过 JVM 这个聪明的整理师可能会对它们进行优化,提高缓存命中率。各种类型的字段占用的空间也不一样,boolean 和 byte 最小,只占 1 个字节,long 和 double 最大,要占 8 个字节呢。引用类型的就看是 32 位还是 64 位 JVM 啦。

最后还有对齐填充部分,这就像是给对象的小尾巴。因为 JVM 对对象的内存分配有要求,对象的总大小必须是某个固定大小的倍数,所以如果不够,就加点填充字节,让对象变得整整齐齐。

比如有个简单的 Person 类,在 64 位 JVM 上,Person 对象的内存布局可有意思了。先是 16 字节的对象头,然后是实例数据,最后可能还有几个填充字节,让总大小达到最接近 16 字节倍数的大小。

四、判断对象的 “退休” 时刻

在 Java 星球上,垃圾回收器就像是个勤劳的清洁工,时刻判断哪些对象可以 “退休” 了。

有一种叫引用计数法的方法,不过现代的 Java 虚拟机可不用它。这个方法就是给每个对象弄个引用计数器,有多少引用指向它,计数器就显示多少。当计数器为 0 的时候,就表示这个对象没人要了,可以被回收。但这种方法太简单,容易出错。

现代 Java 虚拟机用根搜索算法。从一组叫做 “GC Roots” 的重要对象开始,就像从一群老大出发,通过引用链去追踪所有可达的对象。那些不可达的对象,就等着被清洁工回收吧。常见的 GC Roots 有虚拟机栈里的局部变量、方法区中的类静态属性引用、常量引用和本地方法栈中 JNI 的引用。

判断对象是否可以被回收有几个步骤呢。先是标记阶段,从 GC Roots 开始,把可达的对象都标记为存活,就像给好对象贴上小红花。然后是清除阶段,把没被标记的对象都清理掉,这些就是垃圾啦。还有个整理阶段是可选的,把存活对象挪到一起,消除内存碎片,让内存更整洁。

还有软引用、弱引用和虚引用这些特殊情况呢。软引用对象在内存不足的时候会被回收,弱引用对象下次垃圾回收的时候肯定被回收,虚引用对象主要是在对象被回收的时候通知一下,自己可没法直接访问对象。如果对象覆盖了 finalize () 方法,在被回收之前,JVM 会调用这个方法,不过从 Java 9 开始,这个方法就被嫌弃啦,还是 try-with-resources 和 AutoCloseable 接口更好用。

就像有个小码写的程序,一开始有两个对象被引用,后来都被赋值为 null,这两个对象就变得不可达了,等着被垃圾回收器回收。

五、永久代的变迁故事

在 Java 星球的早期,有个神秘的地方叫永久代。这里专门存储类的元数据信息,像类的结构、方法数据、常量池啥的。虽然主要是放静态数据,但有时候也会发生垃圾回收呢。

类卸载的时候,如果一个类不再被任何类加载器引用,JVM 就可以把这个类从永久代里卸载掉,释放空间。还有常量池清理,那些不再被引用的常量也会被回收。方法区的清理也会发生,当类的结构信息不再需要的时候,也会腾出空间。

不过永久代也有问题,它大小有限还固定分配,容易出问题。比如内存溢出,JVM 就会抛出 “OutOfMemoryError: PermGen space” 的错误。在一些动态加载类的应用场景,像 Web 应用服务器,很容易就把永久代空间耗尽了。

从 Java 8 开始,永久代被元空间取代啦。元空间在本地内存里,不在 JVM 的堆内存中。这下可好了,元空间可以动态扩展,减少了内存溢出的风险,管理也更灵活,能更好地适应不同应用程序的需求。在元空间里,垃圾回收还是会发生,类卸载、常量池清理和内存管理都更加高效灵活。

小码在 Java 星球的冒险还在继续,他不断探索着这个神奇世界的奥秘。

详细篇:

1.说说Java对象创建过程

  • 加载类:当程序首次遇到某个类的新实例请求时,如果该类还没有被加载到内存中,那么Java虚拟机(JVM)会先加载该类。加载类的过程包括读取类的字节码文件、解析这些字节码为二进制数据,并将其放入方法区中。

  • 链接类:一旦类被加载,接下来就是链接阶段。链接包括验证、准备和解析(可选)三个子阶段。

    • 验证:确保加载的类文件信息符合当前版本Java虚拟机的要求,不会危害虚拟机安全。
    • 准备:为类的静态变量分配内存空间,并设置默认初始值,但此时不执行任何初始化代码。
    • 解析:将类、接口、字段和方法的符号引用转换为直接引用。这一阶段是可选的,因为解析也可以在初始化之后进行。
  • 初始化类:这是类加载的最后一个阶段,在这个阶段,执行类构造器()方法,这个方法是由编译器自动收集类中的所有静态变量赋值动作和静态代码块中的语句合并产生的。这个方法不需要程序员直接编写,它是编译期自动生成的。

  • 分配内存:当程序执行到new关键字时,JVM会在堆中为新对象分配足够的内存空间。分配内存的过程中还需要保证线程安全,防止多个线程同时分配相同的地址。

  • 初始化对象:在内存分配完成后,JVM开始初始化对象。这一步骤包括:

    • 执行父类构造函数(如果有父类的话),按照继承层次从上到下依次调用。
    • 初始化实例变量为其初始值。
    • 执行对象自身的构造函数,执行构造函数中的代码,包括对成员变量的赋值和其他初始化操作。
  • 对象引用赋值:当对象初始化完成后,JVM会将指向该对象的引用返回给程序,通常通过一个变量来保存这个引用,以便后续可以通过这个引用来访问和操作对象。

2.知道类的生命周期吗?

  • 加载(Loading)
    • 类的加载指的是将类的.class文件中的二进制数据读入到内存中,并将这些静态数据转换成方法区中的运行时数据结构。在这个过程中,还会生成一个代表这个类的java.lang.Class对象。
    • 加载阶段是类加载的第一个阶段,它是由类加载器(ClassLoader)来完成的。
  • 验证(Verification)
    • 验证阶段是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机的安全性。验证包括文件格式验证、元数据验证、字节码验证和符号引用验证。
  • 准备(Preparation)
    • 准备阶段是为类的静态变量分配内存并设置默认初始值。注意,这时候并不会为实例变量分配内存,也不会执行任何初始化代码。例如,静态变量即使被赋予了具体的值(如static int value = 123;),在准备阶段它的值仍然会被设置为0。
  • 解析(Resolution)
    • 解析阶段是将常量池内的符号引用替换为直接引用的过程。符号引用是以一组符号来描述所引用的对象,而直接引用是可以直接指向目标的指针、偏移地址或处理器共享寄存器内容。
  • 初始化(Initialization)
    • 初始化阶段是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有静态变量的赋值操作和静态代码块中的语句合并产生的。在这个阶段,静态变量将被赋予程序员指定的初始值,静态初始化器也会被执行。
  • 使用(Using)
    • 在初始化之后,类就可以被程序使用了。这意味着可以创建该类的实例,调用其静态方法,访问其静态变量等。
  • 卸载(Unloading)
    • 当一个类不再被任何地方引用时,它可以被垃圾回收机制回收。卸载类意味着从方法区中清除该类的数据结构。但是,在大多数情况下,只有当应用程序退出或者方法区内存不足时才会发生类的卸载。

3.简述Java的对象结构

1.对象头(Object Header)

对象头是对象的一部分,通常包含两个部分:

  • Mark Word(标记字段):通常占用8个字节,用于存储对象的哈希码、锁状态标志、线程持有的锁、偏向锁的线程ID等信息。在32位JVM上,这个字段可能只有4个字节。
  • Klass Pointer(类指针):同样占用8个字节(32位JVM上为4个字节),用于指向对象所属类的元数据,即java.lang.Class对象。这个指针使得JVM能够确定对象的类型信息和方法表。

2. 实例数据(Instance Data)

实例数据部分存储了对象的实际属性值。这部分数据按照声明顺序存储,但JVM可能会对其进行优化,例如通过字段重排序来提高缓存命中率。每个字段占用的空间取决于其类型,例如:

  • boolean 和 byte 占用1个字节
  • char 和 short 占用2个字节
  • int 和 float 占用4个字节
  • long 和 double 占用8个字节
  • 引用类型(如 Object)占用4个字节(32位JVM)或8个字节(64位JVM)

3. 对齐填充(Padding)

由于JVM对对象的内存分配有对齐要求,对象的总大小必须是某个固定大小(通常是8字节)的倍数。因此,如果对象的实际大小不是这个固定大小的倍数,JVM会在对象末尾添加一些填充字节,以满足对齐要求。

示例

假设有一个简单的Java类:

public class Person {
    private int id;
    private String name;
}

在64位JVM上,Person对象的内存布局可能如下所示:

  • 对象头(16字节):

    • Mark Word(8字节)
    • Klass Pointer(8字节)
  • 实例数据

    • id(4字节)
    • name 引用(8字节)
  • 对齐填充

    • 填充字节(4字节),使总大小达到24字节(最接近16字节倍数的大小)

总结

  • 对象头:包含对象的元数据信息,如锁状态、哈希码和类指针。
  • 实例数据:存储对象的实际属性值。
  • 对齐填充:确保对象的总大小是固定大小的倍数,提高内存访问效率。

4.如何判断对象可以被回收?

在Java中,垃圾回收器(Garbage Collector, GC)负责自动管理内存,回收不再使用的对象。判断一个对象是否可以被回收,主要依赖于对象的可达性分析。以下是几种常见的判断对象是否可以被回收的标准:

1. 引用计数法(Reference Counting)

引用计数法是一种简单的垃圾回收算法,但它并不被现代Java虚拟机(如HotSpot JVM)使用。这种方法通过为每个对象维护一个引用计数器来跟踪有多少引用指向该对象。当对象的引用计数器为0时,表示没有引用指向该对象,可以被回收。

2. 根搜索算法(Reachability Analysis)

现代Java虚拟机使用根搜索算法来判断对象是否可以被回收。这个算法的基本思想是从一组称为“GC Roots”的对象开始,通过引用链追踪所有可达的对象。任何不可达的对象都会被认为是垃圾,可以被回收。

常见的GC Roots包括:

  • 虚拟机栈(栈帧中的局部变量):每个线程的栈中保存的局部变量。
  • 方法区中的类静态属性引用:类的静态变量。
  • 方法区中的常量引用:常量池中的引用。
  • 本地方法栈中JNI(Native方法)的引用:本地方法中保存的引用。

判断对象是否可以被回收的具体步骤

  • 标记阶段(Marking Phase)

    • 从GC Roots开始,遍历所有可达的对象,并将这些对象标记为存活。
    • 使用递归或迭代的方法遍历对象图,确保所有可达对象都被标记。
  • 清除阶段(Sweeping Phase)

    • 清除所有未被标记的对象,这些对象被认为是垃圾,可以被回收。
  • 整理阶段(Compaction Phase)(可选):

    • 将存活对象移动到一起,消除内存碎片,提高内存利用率。

特殊情况

  • 软引用(SoftReference):
    • 软引用对象在内存不足的情况下会被回收。
  • 弱引用(WeakReference):
    • 弱引用对象在下一次垃圾回收时一定会被回收。
  • 虚引用(PhantomReference):
    • 虚引用对象无法通过引用访问,主要用于在对象被回收时收到通知。
  • Finalization:
    • 如果对象覆盖了finalize()方法,那么在对象被回收之前,JVM会调用这个方法。但是从Java 9开始,finalize()方法已被弃用,推荐使用try-with-resources和AutoCloseable接口来管理资源。

示例

假设有一个简单的Java程序:

public class Test {
    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();

        obj1 = null; // obj1 不再引用任何对象
        obj2 = null; // obj2 不再引用任何对象

        System.gc(); // 建议JVM进行垃圾回收
    }
}

在这个例子中,obj1和obj2最初分别引用了两个对象。当它们被赋值为null时,这两个对象变得不可达,因此可以被垃圾回收器回收。

总结

判断一个对象是否可以被回收的关键在于它是否可以通过GC Roots可达。如果一个对象无法通过任何GC Roots到达,那么它就会被视为垃圾,可以被回收。现代Java虚拟机使用根搜索算法来进行这种可达性分析,确保内存的有效管理和利用。

5.JVM的永久代中会发生垃圾回收么?

在早期的Java虚拟机(如Java 7及之前的版本)中,永久代(Permanent Generation,简称PermGen)是一个专门用于存储类的元数据信息的区域。这些元数据信息包括类的结构、方法数据、常量池等。虽然永久代主要用于存储这些静态数据,但在某些情况下,永久代中仍然会发生垃圾回收。

永久代中的垃圾回收

  • 类卸载(Class Unloading)

    • 当一个类不再被任何类加载器引用时,JVM可以卸载这个类。类的卸载会释放永久代中占用的空间。
    • 类卸载通常发生在以下情况下:
      • 应用服务器的Web应用重新部署。
      • 动态代理类的卸载。
      • 使用OSGi框架时,模块的卸载。
  • 常量池清理

    • 永久代中存储了大量的字符串常量和类的常量池。当这些常量不再被引用时,垃圾回收器可以回收这些常量占用的空间。
  • 方法区的清理

    • 方法区(Method Area)是永久代的一部分,存储了类的结构信息。当这些结构信息不再需要时,垃圾回收器可以回收这些空间。

永久代的问题 尽管永久代中可以发生垃圾回收,但由于其有限的大小和固定的分配方式,容易出现以下问题:

  • 内存溢出(OutOfMemoryError: PermGen space):当永久代空间耗尽时,JVM会抛出OutOfMemoryError: PermGen space错误。
  • 动态加载类的应用:在一些应用场景中,如Web应用服务器,经常会有大量的类被动态加载,这会导致永久代空间迅速耗尽。

Java 8 及以后的变化

从Java 8开始,永久代被元空间(Metaspace)取代。元空间位于本地内存中,而不是在JVM的堆内存中。这一变化带来了以下好处:

  • 动态扩展:元空间可以根据需要动态扩展,减少了内存溢出的风险。
  • 更好的内存管理:元空间的管理更加灵活,可以更好地适应不同应用程序的需求。

元空间中的垃圾回收

在元空间中,垃圾回收仍然会发生,但与永久代相比,元空间的管理更加高效和灵活:

  • 类卸载:类卸载仍然会发生,当类不再被引用时,其占用的元空间会被回收。
  • 常量池清理:字符串常量和其他常量池条目的清理仍然会进行。
  • 内存管理:元空间的内存管理更加灵活,可以根据需要动态调整大小。