💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

【Jvm基础篇1】内存管理_java

  • 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老
  • 导航
  • 檀越剑指大厂系列:全面总结 java 核心技术点,如集合,jvm,并发编程 redis,kafka,Spring,微服务,Netty 等
  • 常用开发工具系列:罗列常用的开发工具,如 IDEA,Mac,Alfred,electerm,Git,typora,apifox 等
  • 数据库系列:详细总结了常用数据库 mysql 技术点,以及工作中遇到的 mysql 问题等
  • 懒人运维系列:总结好用的命令,解放双手不香吗?能用一个命令完成绝不用两个操作
  • 数据结构与算法系列:总结数据结构和算法,不同类型针对性训练,提升编程思维,剑指大厂

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。💝💝💝 ✨✨ 欢迎订阅本专栏 ✨✨


博客目录

  • 一.内存管理
  • 1.并发与并行?
  • 2.创建对象的过程?
  • 3.指针碰撞和空闲列表
  • 4.什么是 TLAB
  • 5.对象的内存布局?
  • 6.对象的访问定位的方式?
  • 7.方法调用的 2 种形式?
  • 8.说说对 invoke 包的理解?
  • 9.新创建对象占多少内存?
  • 10.内存申请的种类?
  • 12.java 异常分类?
  • 13.StringBuffer 为什么是可变类?
  • 14.jvm 中几种常见的 JIT 优化?
  • 15.逃逸分析
  • 16.JVM 表示浮点数
  • 17.匿名内部类只能访问 final 变量?
  • 18.Java 参数值传递
  • 19.finally 返回时机


一.内存管理

1.并发与并行?

并发:同一时间同时发生,内部可能存在串行或者并行.又称共行性,是指处理多个同时性活动的能力。

并行:同一时间点同时执行,不存在阻塞.指同时发生两个并发事件,具有并发的含义。并发不一定并行,也可以说并发事件之间不一定要同一时刻发生。

区别:并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。

【Jvm基础篇1】内存管理_java_02

2.创建对象的过程?

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用 TLAB,这一工作过程也可以提前至 TLAB 分配时进行。这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例 如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

从 Java 程序的视角来看,对象创建才刚刚开始 init 方法还没有执行,所有的字段都还为零。所以,一般来说,执行 new 指令之后会接着执行 init 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

3.指针碰撞和空闲列表

假设 Java 堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种内存分配方式称为“指针碰撞”(Bump the Pointer)。

如果 Java 堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用 Serial ParNew 等带 Compact 过程的收集器时,系统采用的分配算法是指针碰撞,而使用 CMS 这种基于 Mark-Sweep 算法的收集器时,通常采用空闲列表。

4.什么是 TLAB

除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理–实际上虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local AllocationBuffer,TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁定。虚拟机是否使用 TLAB,可以通过-XX:±UseTLAB 参数来设定。

并发安全问题的解决方案:

JVM 提供的解决方案是,CAS 加失败重试和 TLAB

TLAB 分配内存:为每一个线程在 Java 堆的 Eden 区分配一小块内存,哪个线程需要分配内存,就从哪个线程的 TLAB 上分配 ,只有 TLAB 的内存不够用,或者用完的情况下,再采用 CAS 机制

5.对象的内存布局?

在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

HotSpot 虚拟机的对象头包括两部分信息

第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32bit 和 64bit,官方称它为“MarkWord”。对象需要存储的运行时数据很多,其实已经超出了 32 位、64 位 Bitmap 结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,MarkWord 被设计成一个非固定的数据结松以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如,在 32 位的 HotSpot 虚拟机中,如果对象处于未被锁定的状态下,那么 MarkWord 的 32bit 空间中的 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0.

【Jvm基础篇1】内存管理_java_03

对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说。查找对象的元数据信息并不一定要经过对象本身,另外,如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中却无法确定数组的大小。

实例数据:是真正存储有效信息的部分.父类信息,子类信息都会记录下来.相同宽度的字段总是分配在一起.子类较窄的变量也可能插入到父类变量的缝隙之中.

对其填充:不是必须的,主要是占位符的作用,对象的大小必须是 8 字节的整数倍,对象头是 8 字节的整数倍,实例数据需要被对齐填充.

#32位JVM
Object Header: 8 字节
Instance Data: 0 字节 (因为没有任何成员变量)
----------------------
Total: 8 字节
#64位JVM
Object Header: 12 字节 (64位 JVM下对象头通常为12字节)
Padding: 4 字节 (填充字节,用于对齐)
Instance Data: 0 字节 (因为没有任何成员变量)
----------------------
Total: 16 字节

6.对象的访问定位的方式?

Student student = new Student();

具体是如何操作 student 对象呢?有以下两种方式

  • 使用句柄
  • 直接指针

如果使用句柄访问的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息.

【Jvm基础篇1】内存管理_java_04

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

【Jvm基础篇1】内存管理_Java_05

这两种对象访问方式各有优势,使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针。而 reference 本身不需要修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

7.方法调用的 2 种形式?

一种形式是解析,另一种是分派:

所有方法调用的目标方法在 class 文件的常量池中都有一个对应的符号引用,在类加载阶段,会将符号引用转换为直接引用,前提条件是调用之前就知道调用的版本,且在运行期间是不可变的,这种方法的调用被称为解析.

调用不同类型的方法,使用的字节码指令不同,具体如下:

  • invokestatic。用于调用静态方法。
  • invokespecial。用于调用实例构造器 init()方法、私有方法和父类中的方法。
  • invokevirtual。用于调用所有的虚方法。
  • invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象。
  • invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。

只要能被 invokestatic 和 invokespecial 调用的方法都能在解析阶段确定调用的版本,符合这个条件的方法有静态方法,私有方法,实例构造器,父类方法.再加上 final 修饰的方法.尽管 final 修饰的方法是通过 invokevirtual 调用的.这 5 种方法会在类加载阶段就将符号引用转化为直接引用,这些方法统称为“非虚方法”.与之相反的,被称为虚方法.

分派分为静态分派和动态分派:

所有依赖静态类型来决定执行方法版本的,统称为静态分派,最典型的就是方法重载,静态分派发生在编译阶段,这一点也是有些资料将其归为解析而不是分派的原因

动态分派–重写.动态定位到实现类的方法进行调用.

8.说说对 invoke 包的理解?

与反射调用的区别?

jdk1.7 开始引入 java.lang.invoke 包,这个包的主要作用是在之前的单纯依符号引用来确定调用的目标方法外,提供一种新的动态确定目标方法的机制,称为“方法句柄(method handler)”.

Reflection 和 MethodHandler 机制本质上都是在模拟方法调用,Reflection 是 java 代码级别的模拟,MethodHandler 是字节码级别的模拟.在 java.lang.invoke 包下的 MethodHandlers.LookUp(内部类)有 3 个重载的方法,findStatic,findVirtual,findSpecial3 个方法正是对应 invokeStatic,invokeVirtual,invokeSpecial 三个字节码指令.这 3 个方法是为了校验字节码权限的校验.这些底层的逻辑 Reflection 是不用去处理的.

Reflection 中的 java.lang.reflect.Method 对象远比 MethodHandler 机制中的 java.lang.invoke.MethodHandler 对象所包含的信息多,前者是 java 代码的全面映射,包含方法签名,描述符和方法属性表中各种属性,还包含执行权限等信息,而方法句柄仅包含该方法的相关信息,通俗来讲,反射是重量级的,方法句柄是轻量级的.

9.新创建对象占多少内存?

64 位的 jvm,new Object()新创建的对象在 java 中占用多少内存

MarkWord 8 字节,因为 java 默认使用了 calssPointer 压缩,classpointer 4 字节,对象实例 0 字节, padding 4 字节因此是 16 字节。

如果没开启 classpointer 默认压缩,markword 8 字节,classpointer 8 字节,对象实例 0 字节,padding 0 字节也是 16 字节。

-XX:+UseCompressedOops #相当于在64位机器上运行32位
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
public class Wang_09_jol_03 {

    static MyObject myobject = new MyObject();

    public static void main(String[] args) throws InterruptedException {
        System.out.println(ClassLayout.parseInstance(myobject).toPrintable());
    }

    static class MyObject {
        int a = 1;
        float b = 1.0F;
        boolean c = true;
        String d = "hello";
    }
}

【Jvm基础篇1】内存管理_jvm_06

对象头 8 字节,class 压缩关闭 8 字节,int 字段 a 占用 4 字节,flout 字段 b 占用 4 字节,boolean 字段 c 占用 1 字节,内部对齐填充 7 字节

String 类型指针占用 8 字节,一共 40 字节.

10.内存申请的种类?

java 一般内存申请有两种:

  • 静态内存
  • 动态内存

编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如 int 类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如 java 对象的内存空间。根据上面我们知道,java 栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是 java 堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的是堆和方法区。

12.java 异常分类?

运行时异常与非运行时异常的区别:

运行时异常:是 RuntimeException 类及其子类的异常,是非受检异常,如 NullPointerException、IndexOutOfBoundsException 等。由于这类异常要么是系统异常,无法处理,如网络问题;要么是程序逻辑错误,如空指针异常;JVM 必须停止运行以改正这种错误,所以运行时异常可以不进行处理(捕获或向上抛出,当然也可以处理),而由 JVM 自行处理。Java Runtime 会自动 catch 到程序 throw 的 RuntimeException,然后停止线程,打印异常。

非运行时异常:是 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类,是受检异常。非运行时异常必须进行处理(捕获或向上抛出),如果不处理,程序将出现编译错误。一般情况下,API 中写了 throws 的 Exception 都不是 RuntimeException。

常见运行时异常:

异常类型

说明

ArithmeticException

算术错误,如被 0 除

ArrayIndexOutOfBoundsException

数组下标出界

ArrayStoreException

数组元素赋值类型不兼容

ClassCastException

非法强制转换类型

IllegalArgumentException

调用方法的参数非法

IllegalMonitorStateException

非法监控操作,如等待一个未锁定线程

IllegalStateException

环境或应用状态不正确

IllegalThreadStateException

请求操作与当前线程状态不兼容

IndexOutOfBoundsException

某些类型索引越界

NullPointerException

非法使用空引用

NumberFormatException

字符串到数字格式非法转换

SecurityException

试图违反安全性

StringIndexOutOfBoundsException

试图在字符串边界之外索引

UnsupportedOperationException

遇到不支持的操作

常见非运行时异常:

异常类

意义

ClassNotFoundException

找不到类

CloneNotSupportedException

试图克隆一个不能实现 Cloneable 接口的对象

IllegalAccessException

对一个类的访问被拒绝

InstantiationException

试图创建一个抽象类或者抽象接口的对象

InterruptedException

一个线程被另一个线程中断

NoSuchFieldException

请求的字段不存在

NoSuchMethodException

请求的方法不存在

13.StringBuffer 为什么是可变类?

public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
}
public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
 }
private void ensureCapacityInternal(int minimumCapacity) {
  // overflow-conscious code
    if (minimumCapacity - value.length > 0)
      expandCapacity(minimumCapacity);
  }

void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

StringBuffer 的 append 的实现其实是 System 类中的 arraycopy 方法实现的,这里的浅复制就是指复制引用值,与其相对应的深复制就是复制对象的内容和值;

14.jvm 中几种常见的 JIT 优化?

在 JVM(Java 虚拟机)中,有几种常见的即时编译(Just-In-Time,JIT)优化技术,它们用于将 Java 字节码转换成本地机器码,以提高 Java 应用程序的性能。以下是几种常见的 JIT 优化:

  1. 内联(Inlining)优化: 内联是指将方法调用处直接替换为被调用方法的实际代码,避免了方法调用的开销。JIT 编译器会分析程序的执行热点,对其中的短小方法进行内联优化,减少了方法调用的开销,提高了代码执行效率。
  2. 逃逸分析(Escape Analysis)优化: 逃逸分析是 JIT 编译器对对象的动态作用域进行分析,判断一个对象是否逃逸出方法的作用域。如果对象不逃逸,可以将其分配在栈上而不是堆上,避免了垃圾回收的开销,提高了程序的性能。
  3. 标量替换(Scalar Replacement)优化: 标量替换是指 JIT 编译器将一个对象拆解成其各个成员变量,并将这些成员变量分别使用标量(如基本数据类型)进行优化。这样可以避免创建和销毁对象的开销,减少了堆上的内存分配和垃圾回收压力。
  4. 循环展开(Loop Unrolling)优化:循环展开是指 JIT 编译器对循环体进行优化,将循环体中的代码重复展开多次,减少循环的迭代次数。这样可以减少循环控制的开销和分支预测错误的影响,提高了循环的执行效率。
  5. 方法内联缓存(Monomorphic/Megamorphic Inline Cache)优化: 方法内联缓存是 JIT 编译器为了优化虚方法调用而采取的一种策略。它会为不同的目标类型建立缓存,并根据目标类型来直接调用对应的方法,避免了虚方法查找的开销。
  6. 常量折叠(Constant Folding)优化: 常量折叠是指 JIT 编译器在编译时将常量表达式计算得到结果,并用结果直接替换表达式。这样可以避免在运行时重复计算相同的常量表达式,提高了程序的执行效率。

15.逃逸分析

逃逸分析是一种在即时编译(JIT)优化过程中用于分析对象的动态作用域的技术。它的目标是确定一个对象是否"逃逸"出了方法的作用域,即是否被方法外的其他部分所引用。如果对象没有逃逸,那么 JIT 编译器可以将其优化为栈上分配而不是堆上分配,从而提高程序的性能。

逃逸分析的实现过程通常包括以下几个步骤:

  1. 标记阶段::JIT 编译器通过静态分析,标记方法中哪些对象是被分配在堆上的,并且记录下这些对象的创建点。
  2. 逃逸分析阶段: JIT 编译器在动态执行过程中进行逃逸分析,观察对象的引用情况,判断对象是否逃逸出方法的作用域。如果一个对象的引用从方法内传递到方法外,那么它就被认为是逃逸的。

逃逸分析主要有以下两种类型:

  • 全局逃逸:对象的引用逃逸到了方法外部,可能被其他线程访问,或者返回给了调用者。
  • 栈上分配:对象的引用没有逃逸,仅在方法内部可见,可以将其分配在栈上,而不需要在堆上分配。栈上分配的对象在方法返回时自动释放,不需要进行垃圾回收。

逃逸分析带来的好处:

  1. 减少堆内存分配:通过栈上分配非逃逸对象,可以减少垃圾回收的压力,降低堆内存分配的开销,提高程序的执行效率。
  2. 锁消除:对于逃逸对象,由于可能被其他线程访问,需要使用锁进行同步。但对于栈上分配的对象,由于其仅在方法内部可见,可以进行更加精确的锁消除,避免不必要的同步开销。
  3. 标量替换:逃逸分析可以帮助 JIT 编译器进行标量替换优化,将对象拆解成标量(如基本数据类型)进行优化,减少了对象访问的开销。

需要注意的是,逃逸分析并非总是带来性能提升,它会增加编译器的复杂度和开销,而且逃逸分析的准确性也受到程序的复杂性和运行环境的影响。因此,JIT 编译器通常会在逃逸分析和栈上分配之间进行权衡,根据具体的情况来决定是否进行逃逸分析和优化。

16.JVM 表示浮点数

JVM(Java 虚拟机)使用 IEEE 754 标准来表示浮点数,这是一种广泛应用于计算机中的浮点数表示方法。IEEE 754 标准定义了两种精度的浮点数格式:单精度(32 位)和双精度(64 位)。Java 虚拟机中采用这两种格式来表示浮点数。

单精度浮点数(float): 单精度浮点数占用 32 位,其中包含三个部分:符号位、指数位和尾数位。具体结构如下:

  • 符号位(1 位):用来表示浮点数的符号,0 表示正数,1 表示负数。
  • 指数位(8 位):用来表示浮点数的指数部分,使用移码表示,通常需要对真实指数值进行偏移,使其在表示范围内。
  • 尾数位(23 位):用来表示浮点数的尾数部分,通常为一个二进制小数。

双精度浮点数(double): 双精度浮点数占用 64 位,也包含符号位、指数位和尾数位。具体结构如下:

  • 符号位(1 位):同单精度浮点数,用来表示浮点数的符号。
  • 指数位(11 位):同样使用移码表示,表示浮点数的指数部分,对真实指数值进行偏移。
  • 尾数位(52 位):同样用来表示浮点数的尾数部分,通常为一个二进制小数。

浮点数的表示采用科学计数法的形式,即M x 2^E,其中 M 为尾数,E 为指数。根据指数的位数不同,单精度和双精度浮点数可以表示的范围和精度也不同。单精度浮点数的有效位数约为 7 位,双精度浮点数的有效位数约为 15 位,因此双精度浮点数具有更高的精度和更大的表示范围。

需要注意的是,由于浮点数的特性,它们在进行算术运算时可能会出现舍入误差,因此在比较浮点数时应当谨慎使用等号判断,而应该使用一个小的误差范围来比较。

17.匿名内部类只能访问 final 变量?

在 Java 中,匿名内部类(Anonymous Inner Class)是一种特殊的内部类,它没有显式的类名,通常用于创建一个只需要使用一次的简单类或接口实例。匿名内部类可以访问外部类的成员变量和方法,但对于外部类方法中的局部变量,有一个限制条件:匿名内部类只能访问被final修饰的局部变量。

这是由于 Java 编译器的限制和内部类的生命周期导致的。当创建匿名内部类时,如果允许访问非final的局部变量,那么这些变量的值可能在匿名内部类的生命周期内发生改变。这会导致不稳定的行为,因为匿名内部类的实例可以在外部类方法执行完毕后继续存在,而此时外部方法中的局部变量已经被销毁。

通过将局部变量声明为final,Java 编译器可以保证这些变量的值不会发生改变,从而避免了潜在的线程安全问题。一旦将局部变量声明为final,编译器会在匿名内部类的实例中创建一个拷贝,以保证在匿名内部类中访问的是一个不可变的值。

示例:

public void someMethod() {
    final int x = 10; // 使用final修饰局部变量

    Runnable r = new Runnable() {
        @Override
        public void run() {
            System.out.println(x); // 可以访问final变量x
        }
    };

    // 使用r执行一些操作
}

如果尝试在匿名内部类中访问非final变量,编译器会给出错误提示。但从 Java 8 开始,对于局部变量,如果它们实际上没有发生改变,而且在整个匿名内部类的生命周期中始终没有发生改变,那么 Java 编译器允许在匿名内部类中访问非final的局部变量。这种情况下,编译器会自动将这些局部变量视为final。但是,这种特性只适用于局部变量,并不适用于方法参数或实例变量。

18.Java 参数值传递

Java 中的参数传递是通过传值来实现的,而不是传引用。

在 Java 中,基本数据类型(如 int、float、char 等)和引用数据类型(如对象、数组等)都是按值传递的。这意味着当将一个参数传递给方法时,实际上传递的是该参数的值的副本,而不是原始变量本身。

基本数据类型(传值): 当将基本数据类型的变量作为参数传递给方法时,传递的是该变量的值的副本。在方法内对参数进行修改不会影响原始变量的值。

public void modifyInt(int num) {
    num = num + 1;
}

int x = 10;
modifyInt(x);
System.out.println(x); // 输出:10,原始变量x的值不受方法内部修改的影响

引用数据类型(传值): 当将引用数据类型(如对象或数组)作为参数传递给方法时,传递的是该引用的值的副本,也就是对象在堆内存中的地址。因此,方法内对参数所指向的对象进行修改,会影响原始对象的内容。但是,如果在方法内部重新分配了一个新的对象,那么原始对象的引用不会受到影响。

public void modifyArray(int[] arr) {
    arr[0] = 99;
}

int[] nums = {1, 2, 3};
modifyArray(nums);
System.out.println(nums[0]); // 输出:99,原始数组被修改
javaCopy code
public void createNewArray(int[] arr) {
    arr = new int[]{4, 5, 6}; // 在方法内部重新分配了一个新的数组
}

int[] nums = {1, 2, 3};
createNewArray(nums);
System.out.println(nums[0]); // 输出:1,原始数组引用未受影响

虽然在传递引用数据类型时,方法内部的修改会反映在原始对象上,但仍然可以认为这是按值传递,因为传递的是引用的值(地址)的副本。

19.finally 返回时机

在正常情况下,当在 try 块或 catch 块中遇到 return 语句时,finally 语句块会在方法返回之前被执行。

不论 try 块或 catch 块中是否遇到 return 语句,当执行到 finally 语句块时,它的代码都会被执行。然后,如果在 try 块中遇到了 return 语句,方法会立即返回,并且 catch 块(如果有的话)会被忽略。但在返回之前,finally 块的代码会被执行完毕。这意味着,即使在 try 块中遇到了 return 语句,finally 语句块中的代码也会得到执行。

如果没有遇到 return 语句,或者在 catch 块中遇到 return 语句,finally 语句块依然在方法返回之前执行,并在最终返回结果之前执行完毕。

这样的设计是为了确保在执行 try 块或 catch 块的过程中,能够进行一些必要的清理操作,不论是正常返回还是异常返回。finally 块通常用于释放资源或执行一些必须要在方法返回前完成的操作,从而确保程序的稳定性和正确性。

觉得有用的话点个赞 👍🏻 呗。
❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄

💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍

🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙

【Jvm基础篇1】内存管理_jvm_07