JVM知识梳理

JVM****是一种规范

1.1 Java程序的执行过程

一个 Java 程序,首先经过 javac 编译成 .class 文件,然后 JVM 将其加载到方法区,执行引擎将会执行这些字节码。执行时,会翻译成操作系统相关的函数。JVM 作为 .class 文件的翻译存在,输入字节码,调用操作系统函数。

过程如下:Java 文件->编译器>字节码->JVM->机器码。

JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。

一、Java程序从源文件创建到程序运行要经过两大步骤:

1、编译期:编译器把 .java 源代码文件编译成 .class 字节码文件

2、运行期:Java类加载器将 .class 字节码文件加载到内存,在 JVM 中进行解释并生成可执行代码。

编译期

创建完源文件之后,程序先要被 JVM中 的 Java 编译器进行编译为 .class 文件。Java 编译一个类时,如果这个类所依赖的类还没有被编译,编译器会自动的先编译这个所依赖的类,然后引用。如果 Java 编译器在指定的目录下找不到该类所依赖的类的 .class 文件或者 .java 源文件,就会报 "Cant found sysbol" 的异常错误。

编译后的字节码文件格式主要分为两部分:常量池和方法字节码。常量池记录的是代码出现过的(常量、类名、成员变量等)以及符号引用(类引用、方法引用,成员变量引用等);方法字节码中放的是各个方法的字节码。

运行期

Java 类运行的过程大概分为:类的加载和类的执行。需要说明的一点的是:JVM 并不是在运行时就会把所有使用到的类都加载到内存中,JVM主要在程序第一次运行时主动使用类的时候,才会立即去加载。

在 Java 中,JVM可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。

1.2 JVM**、JREJDK的关系**

JVM只是一个翻译,把Class翻译成机器识别的代码,但是需要注意,JVM 不会自己生成代码,需要大家编写代码,同时需要很多依赖类库,这个时候就需要用到JRE。

JRE是什么,它除了包含JVM之外,提供了很多的类库(就是我们说的jar包,它可以提供一些即插即用的功能,比如读取或者操作文件,连接网络,使用I/O等等之类的)这些东西就是JRE提供的基础类库。JVM 标准加上实现的一大堆基础类库,就组成了 Java 的运行时环境,也就是我们常说的 JRE(Java Runtime Environment)。

但对于程序员来说,JRE还不够。我写完要编译代码,还需要调试代码,还需要打包代码、有时候还需要反编译代码。所以我们会使用JDK,因为JDK还提供了一些非常好用的小工具,比如 javac(编译代码)、java、jar (打包代码)、javap(反编译<反汇编>)等。这个就是JDK。

JVM的作用是:从软件层面屏蔽不同操作系统在底层硬件和指令的不同。这个就是我们在宏观方面对JVM的一个认识。

同时JVM是一个虚拟化的操作系统,类似于Linux或者Windows的操作系统,只是它架在操作系统上,接收字节码也就是class,把字节码翻译成操作系统上的机器码且进行执行。

JVM的内存区域

2.1 运行时数据区域

运行时数据区的定义:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域

Java 引以为豪的就是它的自动内存管理机制。相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。

所以要深入理解JVM必须理解内存虚拟化的概念。

在JVM中,JVM 内存主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈等。

同时按照与线程的关系也可以这么划分区域:

线程私有区域:一个线程拥有单独的一份内存区域。

线程共享区域:被所有线程共享,且只有一份。

这里还有一个直接内存,这个虽然不是运行时数据区的一部分,但是会被频繁使用。你可以理解成没有被虚拟机化的操作系统上的其他内存(比如操作系统上有8G内存,被JVM虚拟化了3G,那么还剩余5G, JVM是借助一些工具使用这5G内存的,这个内存部分称之为直接内存)

2.2 JAVA方法的运行与虚拟机栈

虚拟机栈是用来存放线程运行java方法所需的数据,指令、返回地址。

其实在我们实际的代码中,一个线程是可以运行多个方法的。

比如:

这段代码很简单,就是起一个main方法,在main方法运行中调用A方法,A方法中调用B方法,B方法中运行C方法。

我们把代码跑起来,线程1来运行这段代码, 线程1跑起来,就会有一个对应 的虚拟机栈,同时在执行每个方法的时候都会打包成一个栈帧。

比如main开始运行,打包一个栈帧送入到虚拟机栈。

C方法运行完了,C方法出栈,接着B方法运行完了,B方法出栈、接着A方法运行完了,A方法出栈,最后main方法运行完了,main方法这个栈帧就出栈了。

这个就是Java方法运行对虚拟机栈的一个影响。虚拟机栈就是用来存储线程运行方法中的数据的。而每一个方法对应一个栈帧。

2.3 虚拟机栈

**栈的数据结构:**先进后出(FILO)的数据结构,

虚拟机栈的作用: 在JVM运行过程中存储当前线程运行方法所需的数据,指令、返回地址****。****

虚拟机栈是基于线程的:哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。

虚拟机栈的大小缺省为1M,可用参数 --Xss调整大小,例如-Xss256k。

栈帧:在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。

栈帧大体都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)

1.局部变量表:

顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型,一般32位就可以存放下,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。

2.操作数据栈

存放java方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。

操作数栈本质上是JVM执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。

3.动态连接:

Java语言特性多态(后续章节细讲,需要结合class与执行引擎一起来讲)。

4.返回地址:

正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)

**同时,**虚拟机栈这个内存也不是无限大,它有大小限制,默认情况下是1M。

2.4 程序计数器

较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。

程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。

由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。

因为JVM是虚拟机,内部有完整的指令与执行的一套流程,所以在运行Java方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如果是遇到本地方法(native方法),这个方法不是JVM来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器,这个会记录本地代码的执行的地址,所以在执行native方法时,JVM中程序计数器的值为空(Undefined)。

另外程序计数器也是JVM中唯一不会OOM(OutOfMemory)的内存区域。

2.5 栈帧执行对内存区域的影响

在JVM中,基于解释执行的这种方式是基于栈的引擎,这个说的栈,就是操作数栈。

虚拟机栈:

每个线程私有的,线程在运行时,在执行每个方法的时候都会打包成一个栈帧,存储了局部变量表,操作数栈,动态链接,方法出口等信息,然后放入栈。每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。

栈的大小缺省为1M,可用参数 --Xss调整大小,例如-Xss256k

在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

局部变量表 : 顾名思义就是局部变量的表,用于存放我们的局部变量的。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型,一般32位就可以存放下,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。(基本数据类型、对象引用、returnAddress类型)

操作数据栈 :存放我们方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的,操作数栈运行方法是会一直运行入栈/出栈的操作

动态连接 : Java语言特性多态(需要类加载、运行时才能确定具体的方法,后续有详细的讲解)++++动态分派++++

完成出口(返回地址) :

正常返回:(调用程序计数器中的地址作为返回)

三步曲:

恢复上层方法的局部变量表和操作数栈、

把返回值(如果有的话)压入调用者栈帧的操作数栈中、

调整程序计数器的值以指向方法调用指令后面的一条指令、

异常的话:(通过异常处理表<非栈帧中的>来确定)

运行时数据区及JVM的整体内存结构

JVM能涉及非常庞大的一块知识体系,比如内存结构、垃圾回收、类加载、性能调优、JVM自身优化技术、执行引擎、类文件结构、监控工具等。

但是在所有的知识体系中,都或多或少跟内存结构有一定的关系:

比如垃圾回收回收的就是内存、类加载加载到的地方也是内存、性能优化也涉及到内存优化、执行引擎与内存密不可分、类文件结构与内存的设计有关系,监控工具也会监控内存。所以内存结构处于JVM中核心位置。也是属于我们入门JVM学习的最好的选择。

同时JVM是一个虚拟化的操作系统,所以除了要虚拟指令之外,最重要的一个事情就是需要虚拟化内存,这个虚拟化内存就是我们马上要讲到的JVM的内存区域。

3.1 本地方法栈

本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的(比如Object.hashcode方法)。

本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot直接把本地方法栈和虚拟机栈合二为一 。

3.2 方法区

方法区(Method Area)是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法

方法区是JVM对内存的"逻辑划分",在JDK1.7及之前很多开发者都习惯将方法区称为"永久代",是因为在HotSpot 虚拟机中,设计人员使用了永久代来实现了 JVM 规范的方法区。在JDK1.8及以后使用了元空间来实现方法区。

3.2.1 元空间

方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。

在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地内存。

3.2.2 运行时常量池

运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。

运行时常量池是方法区的一部分。运行时常量池相对于Class常量池的另外一个重要特征是具备动态性(Class常量池在类加载章节会具体讲)。

3.5

堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的。随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。

Java 的对象可以分为基本数据类型和普通对象。对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

堆大小参数:

-Xms:堆的最小值;

-Xmx:堆的最大值;

-Xmn:新生代的大小;

-XX:NewSize;新生代最小值;

-XX:MaxNewSize:新生代最大值;

例如- Xmx256m

3.6 直接内存(堆外内存)

直接内存有一种更加科学的叫法,堆外内存。

JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的内存也就是堆外内存。

它不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;

这块内存不受java堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常。

深入理解JVM的内存区域

4.1 深入理解JVM的内存区域

代码示例:

java 复制代码
/**
 * @author King老师
 * VM参数
 * -Xms30m -Xmx30m  -XX:MaxMetaspaceSize=30m  -XX:+UseConcMarkSweepGC -XX:-UseCompressedOops
 *
 *
 */
public class JVMObject {
    public final static String MAN_TYPE = "man"; // 常量
    public static String WOMAN_TYPE = "woman";  // 静态变量
    public static void  main(String[] args)throws Exception {
        Teacher T1 = new Teacher();//这个对象在哪里??哪个地址
        T1.setName("Mark");
        T1.setSexType(MAN_TYPE);
        T1.setAge(36);
        for(int i =0 ;i<15 ;i++){
            System.gc();//主动触发GC 垃圾回收 15次--- T1存活  T1要进入老年代
        }
        Teacher T2 = new Teacher();
        T2.setName("King");
        T2.setSexType(MAN_TYPE);
        T2.setAge(18);
        Thread.sleep(Integer.MAX_VALUE);//线程休眠   T2还是在新生代
    }
}

class Teacher{
    String name;
    String sexType;
    int age;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public String getSexType() {
        return sexType;
    }
    public void setSexType(String sexType) {
        this.sexType = sexType;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

1.JVM 向操作系统申请内存:

JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给 JVM,接下来 JVM 就进行内部分配。

2.JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小

-Xms30m -Xmx30m -Xss1m -XX:MaxMetaspaceSize=30m

3.类加载(类加载的细节后续章节会讲):

这里主要是把class放入方法区、还有class中的静态变量和常量也要放入方法区

4.执行方法及创建对象:

启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 student 对象,对象引用 student 就存放在栈中。

后续代码中遇到new关键字,会再创建一个 student 对象,对象引用 student 就存放在栈中。

总结一下JVM运行内存的整体流程

JVM在操作系统上启动,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区,最后执行方法。

方法的执行和退出过程在内存上的体现上就是虚拟机栈中栈帧的入栈和出栈。

同时在方法的执行过程中创建的对象一般情况下都是放在堆中,最后堆中的对象也是需要进行垃圾回收清理的。

五 java变量再JVM内存位置

5.1 局部变量

在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个栈帧,其所在方法中声明的变量就放在栈帧中,当方法结束系统会释放该栈帧,其对应在该方法中声明的变量随着栈帧的销毁而结束,这就局部变量只能在方法中有效的原因.

1.1、在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。

(1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在JAVA虚拟机栈中(栈帧中)
(2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在JAVA虚拟机的栈中,该变量所指向的对象是放在堆内存中的。

5.2 成员变量

在类中声明的变量是成员变量,也叫全局变量.

那成员变量又是存在于哪里呢,网上有的说是在堆里,有的说在方法区里.

其实严格来说,两者都有存在,方法区 里存有类信息,常量,静态变量 ,编译后的class文件等,既然是类信息,那么一个class类中的方法名,类名,成员变量名称等都属于该类的信息,也是存在于方法区中,也就是一个类中的成员变量名称是存在于方法区中的,但是在我们new一个对象后,这个对象上包含的成员变量的值是放入堆中的,也就是new的这个对象包含着这个对象的成员变量的信息(也就是值),但是这个对象上不会包含该类的方法,因为方法是共享的.

这里讨论存放在堆中的成员变量, 存放在方法区中的对象是一些类的基本信息, 这些东西是所有线程共享的.

2.1、同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量

(1)当声明的是基本类型的变量其变量名及其值放在堆内存中的
(2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中

深堆和浅堆的认识

java中的堆内存算是整个内存区域中最重要的一块,几乎所有的对象都分配在堆内存。在堆内存中有两个主要的概念需要我们理解,这对分析java堆内存的故障有着重要的作用,分别是深堆和浅堆。

6.1 概念

我们先给出概念,然后再分析。

(1)浅堆表示一个java对象结构所占用内存的大小,这个java对象内部包含了三部分数据:对象头、实例数据和对齐填充。

(2)深堆表示一个java对象被 GC 回收后,可以真实释放的内存大小。

如果你有点疑惑,没关系。对上面出现的那些名词我们一个一个来解释。

1、对象头

一个对象分为三部分:对象头、实例数据、对齐填充。对象头又包含了两部分:标记部分和原始对象引用。

(1)标记部分:包括 hashcode、gc 分代年龄、锁状态标志、线程持有锁、偏向线程锁id,偏向时间戳,这一部分在32位机器上为 4 byte,64 位机器上为 8byte,一般来说我们的机器都是64位,所以现在默认指的是8个字节。

(2)原始对象引用是对象的指针,通过这个指针找到对象的实例,该数据可以压缩,32 位机器上为 4 byte,64位机器上为 8byte,jdk8默认开启压缩,大小为 4byte。

所以我们可以看到:一个java对象的对象头大小,没压缩的时候是16字节,压缩时候是12个字节。

2、实例数据

指的就是我们的java对象实例。这个很容易理解。

3、对齐填充

对齐填充要追根其根源,其实是到了计算机系统结构的学科。我在大学的时候学到了对齐填充,和这里是一样的道理。java中的对象都是以8个字节为单位对齐,所以每一个java对象的大小都是8的整数倍。

4、保留集

对象A能直接或者是间接访问其他的对象,这些对象集合起来就是A的保留集。当进行垃圾回收的时候,不仅回收A,也会回收保留集中所有的对象。

现在我们理解了这些基本的概念,再回过头来好好地看看,深堆和浅堆到底是什么?

**浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。**在《实战java虚拟机》中,使用了一个案例,在这里引用一下:

A引用了对象C,D,对象B引用了对象C,E。

对象A引用了C和D,对象B引用了C和E。

那么对象A的浅堆大小只是A本身,不含C和D,而A的实际大小为A、C、D三者之和。

而A的深堆大小为A与D之和,由于对象C还可以通过对象B访问到,因此不在对象A的深堆范围内。

垃圾回收机制

7.1 可达性分析判断对象的存活

来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的

作为GC Roots的对象包括下面几种(重点是前面4种):

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 方法区中类静态属性引用的对象;java类的引用类型静态变量。
  3. 方法区中常量引用的对象;比如:字符串常量池里的引用。
  4. 本地方法栈中 JNI (即一般说的 Native 方法)引用的对象。
  5. JVM的内部引用(class对象、异常对象NullPointException、OutofMemoryError,系统类加载器)。(非重点)
  6. 所有被同步锁(synchronized关键)持有的对象。(非重点)
  7. JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等(非重点)
  8. JVM实现中的"临时性"对象,跨代引用的对象(在使用分代模型回收只回收部分代的对象,这个后续会细讲,先大致了解概念)(非重点)

以上的回收都是对象,类的回收条件:

注意Class要被回收,条件比较苛刻,必须同时满足以下的条件(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):

  1. 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  4. 参数控制:

废弃的常量和静态变量的回收其实就和Class回收的条件差不多。

7.2 F inalize方法

即使通过可达性分析判断不可达的对象,也不是"非死不可",它还会处于"缓刑"阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与GCRoots的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize中去拯救。

java 复制代码
public class FinalizeGC {
    public static FinalizeGC instance = null;
    public void isAlive(){
        System.out.println("I am still alive!");
    }
    @Override
    protected void finalize() throws Throwable{//不推荐使用
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeGC.instance = this;//把引用接上
    }
    public static void main(String[] args) throws Throwable {
        instance = new FinalizeGC();
        //对象进行第1次GC
        instance =null;
        System.gc();
        Thread.sleep(1000);//Finalizer方法优先级很低,需要等待
        if(instance !=null){
            instance.isAlive();
        }else{
            System.out.println("I am dead!");
        }
        //对象进行第2次GC
        instance =null;
        System.gc();
        Thread.sleep(1000);
        if(instance !=null){
            instance.isAlive();
        }else{
            System.out.println("I am dead!");
        }
    }
}

运行结果:

可以看到,对象可以被拯救一次(finalize执行第一次,但是不会执行第二次)

代码改一下,再来一次。

java 复制代码
public class FinalizeGC {
    public static FinalizeGC instance = null;
    public void isAlive(){
        System.out.println("I am still alive!");
    }
    @Override
    protected void finalize() throws Throwable{//不推荐使用
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeGC.instance = this;//把引用接上
    }
    public static void main(String[] args) throws Throwable {
        instance = new FinalizeGC();
        //对象进行第1次GC
        instance =null;
        System.gc();
//        Thread.sleep(1000);//Finalizer方法优先级很低,需要等待
        if(instance !=null){
            instance.isAlive();
        }else{
            System.out.println("I am dead!");
        }
        //对象进行第2次GC
        instance =null;
        System.gc();
//        Thread.sleep(1000);
        if(instance !=null){
            instance.isAlive();
        }else{
            System.out.println("I am dead!");
        }
    }
}

运行结果:

对象没有被拯救,这个就是finalize方法执行缓慢,还没有完成拯救,垃圾回收器就已经回收掉了。所以建议大家尽量不要使用finalize,因为这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序,建议大家忘了finalize方法!因为在finalize方法能做的工作,java中有更好的,比如try-finally或者其他方式可以做得更好。

7.3Java对象不使用时,为什么要赋值null?

在Java中,将一个对象引用赋值为null的主要目的是释放对象所占用的内存空间,并允许垃圾收集器回收该对象。当一个对象不再被引用时,如果没有其他引用指向它,它将变为不可访问的状态,并且可以被垃圾收集器标记为可回收的。

赋值为null有助于提醒垃圾收集器该对象可以被回收,因为它不再被使用。如果没有赋值为null,垃圾收集器可能无法立即识别出该对象可以被回收,从而延迟垃圾回收的发生时间。

另外,将不再使用的对象引用赋值为null还可以防止意外的对象持有。如果一个对象引用保持不变,即使你不再需要它,它也会继续占用内存资源。通过赋值为null,可以明确地表达出不再需要该对象,并允许它在垃圾回收时被释放。

需要注意的是,只有当对象不再被任何引用指向时,才能被垃圾收集器回收。如果对象仍然被其他引用引用着,将其赋值为null并不会立即回收对象。因此,在赋值为null之前,确保没有其他代码正在使用该对象是很重要的。

相关推荐
东阳马生架构5 小时前
JVM实战—3.JVM垃圾回收的算法和全流程
jvm
xiaolingting13 小时前
Java 引用是4个字节还是8个字节?
java·jvm·引用·指针压缩
HUNAG-DA-PAO19 小时前
Spring AOP是什么
java·jvm·spring
No regret.20 小时前
JVM内存模型、垃圾回收机制及简单调优方式
java·开发语言·jvm
东阳马生架构1 天前
JVM实战—2.JVM内存设置与对象分配流转
jvm
撸码到无法自拔1 天前
深入理解.NET内存回收机制
jvm·.net
吴冰_hogan2 天前
JVM(Java虚拟机)的组成部分详解
java·开发语言·jvm
东阳马生架构2 天前
JVM实战—1.Java代码的运行原理
jvm
ThisIsClark2 天前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉2 天前
【jvm】内存泄漏与内存溢出的区别
jvm