当前位置:网站首页 > Java基础 > 正文

java虚拟栈基础



面向对象三大特性

基本数据类型和引用数据类型

在这里插入图片描述

为什么浮点数运算的时候会有精度丢失的风险,如何解决

👉因为在计算机底层实际上都是用二进制的方法表示数据的,而基本上大多数十进制的小数都很难用二进制表示出来,会存在循环截断的现象

👉用BigDecimal解决,比如说两个浮点数相加,就先创建两个对应的BigDecimal对象,【但是这里不能直接用new (double)的方法而是得用new(String)的方法,或者是用valueOf(double)来创建】创建完成后调用BigDecimal的加减乘除方法即可(add subtract multiply divide)

【扩展】比较两个BigDecimal最好用CompareTo的方法,因为用equals还会比较值的精度

基本类型和包装类型的区别?

  • 在用途方面,基本数据类型在定义除了常量和局部变量之外很少用到,而方法参数,对象里面总是用到包装类型
  • 在存储空间方面,基本数据类型的局部变量和被static修饰的成员变量是存在在栈内存中;包装类型大多都是在堆内存中。
    什么情况会出现包装类型不会出现在堆内存
    HotSpot引入了JIT优化,所以如果发现一个对象没有逃逸到方法外,就会用标量替换将它放在栈内存,不会为它分配堆内存
  • 在占用空间方面,基本数据类型占的很小
  • 在对比方面,基本用==,包装用equals()

包装类型的缓存机制

 

自动拆箱和装箱

 

String、StringBuilder、StringBuffer的区别

  • String是Java语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的Immutable类,被声明成为final class,所有属性也都是final的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的String对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。
  • StringBuffer是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用append或者add方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐StringBuilder。
  • StringBuilder是Java 1.5中新增的,在能力上和StringBuffer没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

可变字符串底层实现

为了实现修改字符序列的目的,StringBuffer和StringBuilder底层都是利用可修改的(char,JDK 9以后是byte)数组,二者都继承了AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了synchronized。内部数组目前的实现是,构建时初始字符串长度加16(如果没有构建对象时输入最初的字符串,那么初始值就是16)。如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行arraycopy。

==和equals的区别

:用于比较两个变量是否引用同一个对象。如果是基本类型,则比较的是它们的值;如果是引用类型,== 比较的是它们在内存中的地址。

 

Java 的 Integer 缓存机制:在 Java 中,Integer 的值在 -128 到 127 之间时,JVM 会缓存这些值。这意味着当创建 Integer 对象时,如果值在这个范围内,JVM 会返回同一个对象。对于 a = 5 和 b = 5,由于 5 落在 -128 到 127 之间,JVM 会使用缓存,因此 a 和 b 指向相同的对象。特殊情况:值大于 127,a 和 b 不再使用缓存,会是两个不同的对象:那么会返回false,因为 a 和 b 是两个不同的对象。

 
  1. stra 是通过 new String(“30”) 创建的新字符串对象。
    strb 是字符串字面量 “30”,在Java中,字符串字面量会被优化存储在常量池中。
    由于 stra 和 strb 引用的是两个不同的字符串对象,所以 的结果是 false。
  2. String.valueOf(a) 会将 int 类型的变量 a 转换为字符串 “30”。对于这种小的字符串,Java 会使用常量池中的字符串。strb 也是常量池中的字符串,因为它是通过字面量创建的。所以指向的是同一个常量池中的 “30” 字符串。
  3. equals() 比较的是字符串的内容。stra 和 strb 的值都是 “30”,尽管它们是不同的对象。
  4. Java 会在比较时自动将 a 转换为 double 类型,进行数值比较。

变量

成员变量与局部变量的区别?

  • 成员变量随着对象创建而生,对象销毁而亡;存储在堆内存;有默认值
  • 局部变量则是方法调用而生,方法调用而亡;存储在栈内存;不能被等权限修饰符修饰,也不能被修饰;没有默认值

静态变量有什么作用?

静态变量是属于类的变量,在内存中独一份,类的所有实例对象都共享这一份静态变量,可以节约内存。通过类名来访问,如果还被final修饰那么就成为了常量。

字符型常量和字符串常量的区别?

  • 字符型常量占两个字节,可以进行表达式的运算,底层是用ASCII码存储的
  • 字符串常量占多个字节,不能进行表达式运算,其在在内存中存储的是地址

方法

静态方法和实例方法有何不同?

  • 静态方法是属于类的,调用静态方法无需创建对象,可以用或者的方式来调用,但是后者不推荐;静态方法只允许访问静态变量、方法,不能访问实例的
  • 而实例方法是属于对象实例的,只能用来调用,所以必须创建对象。访问无限制。

重载和重写有什么区别?

  • 重载发生在同一个类下,它可以实现相同名字的方法有不同访问修饰符、不同参数(顺序、类型、个数只要有一个不同就算不同)、不同方法体、不同返回值。它一般用于需要根据输入数据的不同做出不同的操作的业务场景【编译期】
  • 重写发生在父类和子类中,子类重写父类的同名方法。一般用于一个同名方法,但子类需要做出和父类不同的响应的业务场景。但是重写的方法参数一定不能修改,且返回值只能比父类的更小或相等,抛出异常的返回更小或相等,权限修饰符更大或相等【运行期】
    • 返回值必须相等的情况:void + 基本数据类型

什么是可变长参数?

:可以接受0个或多个参数,如果有固定参数,则可变长参数一定要写在固定参数的后面。在重载的情况下,会优先匹配固定参数

静态方法为什么不能调用非静态成员

因为静态方法是属于类的,在类加载的时候就会分配内存,不需要创建对象就存在了,而非静态成员是属于实例的,必须要创建对象实例才存在。如果静态方法调用了非静态成员,则可能存在因为没有创建对象实例而导致的访问的非静态成员不存在的非法操作

lambda表达式是什么?有什么作用?优缺点有哪些?【这里还需要补充】

Lambda表达式,也称为闭包,是一种匿名函数,它可以传递到方法作为参数,并且可以在方法中使用。它是Java 8引入的一个新特性,用于简化代码的编写,特别是在使用函数式接口时。

  • 基本语法:
  • 优点
    • 匿名性:Lambda表达式没有显式的名称,因此可以被当做一种匿名函数使用。
    • 简洁性:Lambda表达式可以大大减少代码的冗余,使代码更加简洁。
    • 传递性:Lambda表达式可以作为参数传递给方法,从而实现更灵活的代码组织。
  • 缺点
    • 若不用并行计算,很多时候计算速度没有比传统的 for 循环快。(并行计算有时需要预热才显示出效率优势,并行计算目前对 Collection 类型支持的好,对其他类型支持的一般);不方便调试;阅读稍微有点困难。
  • 应用场景
    • 简化集合操作
    • 线程与并发编程
    • 事件处理
    • 函数式编程
  • 注意事项
    • 参数类型推断
    • 局部变量限制
    • 方法引用

Object

object、class和泛型的关联和区别?

对象是类的实例,而类是对象的模板。类定义了对象的属性和方法,而对象是根据类实例化的具体实体。泛型是一种编程机制,与类和对象的概念有些不同。它允许在编写类、接口或方法时使用参数化类型,使得这些构造能够处理多种数据类型,提高了代码的灵活性和可重用性。

object为什么要设计wait方法?

  • 线程同步:当一个线程调用 wait() 方法时,它会释放对象的锁,并进入等待状态,直到其他线程调用了相同对象上的 notify() 或 notifyAll() 方法来唤醒它。
  • 避免忙等待:在一些需要等待特定条件的情况下,如果不使用 wait() 方法,可能会出现忙等待(busy waiting)的情况,这会消耗大量的 CPU 资源。使用 wait() 方法可以使线程进入等待状态,直到条件满足。
  • 防止竞争条件:wait() 方法的设计也是为了防止竞争条件的发生。通过在等待时释放对象的锁,可以确保其他线程能够执行并可能修改共享资源,从而避免了竞争条件的出现。
  • 线程间通信:wait() 方法也提供了一种简单的线程间通信的机制,允许线程在一定条件下等待其他线程的通知,然后再继续执行。

Object类中的常用方法

:这个方法用于比较当前对象与给定对象是否相等。如果两个对象在逻辑上是相等的,该方法应返回true;否则返回false。在Object类中,默认的equals方法实现是比较对象的内存地址,但通常子类会重写此方法以提供更有意义的比较逻辑。
:此方法返回对象的哈希码值。哈希码在哈希表等数据结构中非常有用,因为它们可以帮助快速定位对象。在Object类中,默认的hashCode方法实现通常与对象的内存地址有关,但子类可能会根据对象的内容来重写此方法。
:此方法返回对象的字符串表示形式。Object类中的默认实现返回一个包含对象类名、哈希码的无符号十六进制表示形式的字符串。子类通常会重写此方法以提供更有意义的字符串表示。
:此方法返回当前对象的运行时类。它返回一个Class对象,该对象描述了对象的实际类型。这对于在运行时检查对象类型或获取类的元信息(如类名、父类、实现的接口等)非常有用。
:此方法创建并返回此对象的一个副本。默认实现是保护级别的,因此子类可以重写它以提供公共访问。需要注意的是,默认的clone方法是浅拷贝,即它只复制对象的字段值,而不复制字段引用的对象。如果需要深拷贝,子类必须重写此方法。
:当垃圾收集器确定不存在对该对象的更多引用时,由对象的垃圾收集器调用此方法。子类可以重写此方法以执行清理操作,如释放系统资源。但需要注意的是,依赖finalize方法进行资源清理是不推荐的做法,因为它不能保证及时执行。

关键字

final、finally、 finalize有什么不同?

  • final可以用来修饰类、方法、变量,分别有不同的意义,final修饰的class代表不可以继承扩展,final的变量是不可以修改的,而final的方法也是不可以重写的。
    • 我们可以将方法或者类声明为final,这样就可以明确告知别人,这些行为是不许修改的。使用final修饰参数或者变量,也可以清楚地避免意外赋值导致的编程错误,甚至,有人明确推荐将所有方法参数、本地变量、成员变量声明成final。
    • final变量产生了某种程度的不可变(immutable)的效果,所以,可以用于保护只读数据,尤其是在并发编程中,因为明确地不能再赋值final变量,有利于减少额外的同步开销,也可以省去一些防御性拷贝的必要。
  • finally则是Java保证重点代码一定要被执行的一种机制。我们可以使用try-finally或者try-catch-finally来进行类似关闭JDBC连接、保证unlock锁等动作。
    这种case finally内的代码不会被执行
  • finalize是基础类java.lang.Object的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize机制现在已经不推荐使用(因为无法保证finalize什么时候执行,执行的是否符合预期。使用不当会影响性能,导致程序死锁、挂起等),并且在JDK 9开始被标记为deprecated。

Java 中静态代码块 / 初始块 / 构造方法的执行顺序

  • 静态代码块会先初始化,然后非静态代码块再初始化,调用无参构造方法后
  • 父类比子类先行执行
  • 静态代码块,在类第一次加载的时候,会初始化一次,适合项目中初始化全局参数,常量等
  • 初始块与构造方法是一家子,但是初始块会在构造函数前执行,初始块适合重载构造函数存在相同代码,可以抽出来使用

初始化时间

  • 非静态属性:创建对象后,系统会自动给对象中的非静态属性做初始化赋默认值,也正是因为这个原因,非静态属性只有在创建对象后,使用对象才能访问。
  • 静态属性:类加载到内存中(方法区)的时候,系统就会给类中的静态属性做初始化赋默认值,所以,即使还没有创建对象,只要这个类加载到了内存,就可以直接使用类名来访问静态属性,因为这个时候静态属性已经完成了初始化赋默认值的操作。静态属性存储在方法区

异常

异常是指在程序执行过程中可能出现的不正常情况或错误。这些异常情况会干扰程序的正常执行流程,并可能导致程序出现错误或崩溃。

Java引发异常的原因

运行时错误:由于代码逻辑错误或运行环境错误导致的异常,例如除以零、数组越界等。
输入错误:由于用户输入的数据不符合预期导致的异常,例如输入格式错误、输入超出范围等。
资源错误:由于对资源的错误使用导致的异常,例如打开不存在的文件、网络连接错误等。
环境错误:由于运行环境的问题导致的异常,例如内存不足、硬件故障等。
异常情况:由于程序逻辑的异常情况导致的异常,例如数据错误、业务逻辑错误等。

异常处理方法

try-catch语句:这是最常用的异常处理方法。在try块中编写可能抛出异常的代码,如果try块中的代码抛出异常,程序会跳转到相应的catch块中处理该异常。
throws关键字:可以将异常抛给调用该方法的上一级方法处理。如果方法内部抛出了异常,而没有捕获处理,那么必须使用throws声明,否则编译会报错。
finally关键字:用于编写一段无论是否发生异常都会被执行的代码块。通常用于释放资源或进行一些清理操作。

怎么做全局异常处理

使用AOP):AOP允许你在不修改业务逻辑代码的情况下,横切多个关注点。可以定义一个切面来拦截所有可能抛出异常的方法,并在切面中处理这些异常。

使用@ControllerAdvice和@ExceptionHandler注解(针对Spring MVC):如果使用的是Spring MVC框架,可以利用@ControllerAdvice和@ExceptionHandler注解来全局处理Web层的异常。@ControllerAdvice用于定义全局的控制器增强类,而@ExceptionHandler用于指定处理特定异常的方法。这样,当控制器中的方法抛出异常时,Spring MVC会自动调用相应的异常处理方法。

使用Filter或Servlet容器:可以编写一个Filter来捕获所有通过Servlet容器的请求和响应。在Filter中,你可以捕获并处理任何抛出的异常。这种方法适用于整个Web应用程序的全局异常处理。

自定义异常处理器:对于非Web应用程序,可以创建一个自定义的异常处理器类,并在程序启动时注册它。这个处理器类可以实现一个接口或继承一个特定的基类,以便在异常发生时被调用。然后,在代码中的适当位置抛出自定义异常,由异常处理器统一处理。

使用线程局部存储(ThreadLocal):用于需要全局范围内跟踪异常信息,而不是立即处理它们的这种情况下,可以使用ThreadLocal来存储异常对象,并在合适的时机进行处理。不过,这种方法需要谨慎使用,以避免内存泄漏和并发问题。

try-with-resources

Java 7引入,使用可以确保代码块执行完毕后,系统会自动关闭资源,从而避免资源泄漏和错误。

 

能够使用try-with-resources的类是实现了接口

Exception和Error有什么区别

Exception和Error体现了Java平台设计者对不同异常情况的分类。Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序(比如JVM自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError之类,都是Error的子类。

Exception又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。前面介绍的不可查的Error,是Throwable不是Exception。不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。

异常处理原则

第一,尽量不要捕获类似Exception这样的通用异常,而是应该捕获特定异常:这是因为在日常的开发和合作中,我们读代码的机会往往超过写代码,软件工程是门协作的艺术,所以我们有义务让自己的代码能够直观地体现出尽量多的信息,而泛泛的Exception之类,恰恰隐藏了我们的目的。另外,我们也要保证程序不会捕获到我们不希望捕获的异常。比如,我们可能更希望RuntimeException被扩散出来,而不是被捕获。进一步讲,除非深思熟虑了,否则不要捕获Throwable或者Error,这样很难保证我们能够正确程序处理OutOfMemoryError。

第二,不要生吞(swallow)异常。这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的,但是千万不要在产品代码做这种假设!如果我们不把异常抛出来,或者也没有输出到日志(Logger)之类,程序可能在后续代码以不可控的方式结束。没人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异常。

第三,Throw early, catch late原则。比如说自己判断出现这段代码会出现NP异常,在这段代码之前做非空校验,就是Throw early。而catch late其实是我们经常苦恼的问题,捕获异常后,需要怎么处理呢?最差的处理方式,就是“生吞异常”,本质上其实是掩盖问题。如果实在不知道如何处理,可以选择保留原有异常的cause信息,直接再抛出或者构建新的异常抛出去。在更高层面,因为有了清晰的(业务)逻辑,往往会更清楚合适的处理方式是什么。

第四,对于自定义异常,需要考虑是否需要定义成Checked Exception,因为这种类型设计的初衷更是为了从异常情况恢复,作为异常设计者,我们往往有充足信息进行分类。其次,在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。比如Java的标准类库,类似java.net.ConnectException这种异常,出错信息是类似“ Connection refused (Connection refused)”,而不包含具体的机器名、IP、端口等,一个重要考量就是信息安全。类似的情况在日志中也有,比如,用户数据一般是不可以输出到日志里面的。

集合

Java 中有哪些常用的集合类?它们的区别?

List 有序 可重复 实现Collection接口

ArrayList 和 LinkedList 的区别是什么?

ArrayList扩容机制

的扩容机制采用按需扩容的策略,默认扩容为当前容量的 1.5 倍,通过 方法将旧数组内容复制到新数组中。合理地设置初始容量和减少 操作频率可以优化 的性能表现。扩容操作会创建一个新的数组,并将旧数组中的元素复制到新数组中,这个过程的时间复杂度是 O(n),其中 是旧数组的长度。因此,频繁的扩容操作会导致性能下降。

  1. 的初始容量。 有一个初始容量(默认为 10),当我们使用无参构造方法创建 时,内部会初始化一个长度为 10 的数组。
     

    这里的 是一个空的数组,当第一次添加元素时,会将其初始化为一个默认容量为 10 的数组。

  2. 的扩容机制:当 添加元素超过其当前容量时,调用 方法时会触发扩容逻辑:
     

    扩容的核心方法是 : 方法首先计算出所需的最小容量 ,并检查 是否超过当前数组的容量。如果超过,则调用 方法进行扩容。源码如下:

     

扩容的实现: 方法

  • 扩容倍数:默认情况下,新容量是旧容量的 1.5 倍 ()。这个计算方式使用了右移运算符 ,相当于旧容量加上旧容量的一半。
  • 最低容量保证:如果计算出的新容量仍然小于所需的最小容量 ,则将新容量设置为 。
  • 最大容量限制: 的最大容量限制是 (约 2^31 - 9)。如果新容量超出该限制,调用 方法进行处理,确保不超出 的最大值。
     

方法:用于处理大容量的极限情况。 如果 超过了 ,返回 作为最大容量。

 

Set 无序 不重复 实现Collection接口

HashSet 和 TreeSet 的区别是什么?

底层结构

实现类底层结构存储原理是否有序线程安全插入/删除/查找复杂度哈希表()使用对象的 方法计算元素的哈希值,并将该元素存储在哈希表相应的桶(bucket)中。每个桶是一个链表或红黑树(Java 8+ 中的优化)来处理哈希冲突。否(无序)否O(1) 平均,O(n) 最坏哈希表 + 双向链表通过双向链表链接每一个插入的元素,从而保持了插入的顺序。插入的顺序可以是自然的插入顺序,也可以是访问顺序。是(插入顺序)否O(1) 平均,O(n) 最坏红黑树()元素是有序的,排序的规则可以是自然顺序(元素实现了 接口)或者是定制顺序(传入一个 对象)。是(排序)否O(log n)跳表()跳表是一种多层的有序链表。每一层有可以包含多个节点,每个节点通过指针连接起来。每一层里会存储以及(记录两个节点之间的距离),每个跳表节点都有一个指向前一个节点。是(排序)是O(log n)

Map 无序 不重复 实现Map接口

HashMap 和 LinkedHashMap 的区别是什么?

ConcurrentHashMap的底层原理,是怎么保证线程安全的?

👉在Java1.7以前,ConcurrentHashMap是基于 分段数组+链表 的方式实现的

  • 使用segment分段锁,底层用的是ReentrantLock
  • 将Map数组分成了16段,每段都存储了一个HashEntry数组,每次操作只会访问当前key所在的其中的一个段,并使用ReentrantLock锁住当前段,进行CAS操作。

👉Java1.8及以后,ConcurrentHashMap是使用 数组 + 链表/红黑树 实现的

  • 使用CAS添加新节点,用Synchronized锁住链表或红黑树的首节点。
    区别:后者性能更好,因为它分段的颗粒度更细,只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写

Map的Put操作的执行过程

  1. 对 key 调用 hash() 方法,计算 key 的哈希值。btw:HashMap 的 hash() 方法会对 key.hashCode() 进行一些额外处理(如右移和异或操作),以减少哈希冲突,提高性能。
  2. 通过哈希值计算数组索引(哈希值对数组长度取模),定位到具体的哈希桶。
  3. 检查哈希桶中是否有节点:
    • 如果桶为空,直接插入新节点。
    • 如果桶中有节点,遍历链表或红黑树。
      • 如果发现相同的 key,替换旧值。
      • 如果没有发现相同的 key,将新节点插入链表尾部或红黑树。(如果链表的长度大于等于 8,HashMap 会将链表转换为红黑树)
  4. 检查是否需要扩容。根据负载因子(默认是 0.75)和当前元素数量决定是否需要扩容。扩容的操作会将数组大小加倍,并重新计算所有键的哈希值,以将它们重新分布到新的数组中。
  5. 返回旧值或 null。

HashMap的负载因子

表示HashMap被填充的程度,越大的话说明填充程度越高。默认是0.75

为什么HashMap容量是2的幂次方

HashMap使用哈希函数将键映射到哈希值,然后根据哈希值计算出该键在数组中的位置。如果数组长度是2的幂次方,则可以使用位运算来代替除法运算来计算哈希值,从而提高哈希函数的效率。【hash值 = key & (数组长度 - 1)】

如果说有100个数据,怎么确定hashmap的容量,使得它不用扩容

根据负载因子确定hashmap的容量:100/0.75 = 133。且必须为2的幂次方,所以向上取2的八次方是256。

怎么选用集合

Hash冲突有哪些解决方法

👉开放寻址
线性(往后找空位置) | 二次(按固定步长找空位置) | 伪随机(随机找空位置)
👉拉链法
将冲突的元素链接到一个链表中

反射机制,动态代理

反射机制赋予程序在运行时自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。
动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装RPC调用、面向切面的编程(AOP)。
实现动态代理的方式很多,比如JDK自身提供的动态代理,基于反射机制实现的。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似ASM、cglib(基于ASM)、Javassist等。

JVM

JVM 是什么?它有哪些功能?

👉JVM就是Java虚拟机,全称Java Virtual Machine,用于将Java字节码转换成机器码并执行Java代码。
👉功能是加载、验证、执行java字节码;管理Java程序的内存;提供垃圾回收机制
👉优点:能够让Java程序仅通过一次编译但在不同的操作系统上执行;提供垃圾回收机制

JVM 的组成部分有哪些?运行流程

在这里插入图片描述

👉类加载器、执行引擎、内存管理器、垃圾回收器、本地方法接口

  1. 类加载器把Java代码转换为字节码。
  2. 内存管理器把字节码加载java虚拟栈基础到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行。
  3. 执行引擎将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口来实现整个程序的功能。

Java 内存区域有哪些?分别存储什么数据?

方法区、堆内存、虚拟栈内存,这三个是线程公有的;程序计数器、本地方法栈,这两个是线程私有的

  • 方法区:存储Java程序加载完成后的类变量、常量、静态变量、JIT编译后的机器码
    • 虚拟机启动的时候创建,关闭虚拟机时释放。
    • 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace。
    • 实现
      👉1.7 永久代,在堆中
      👉1.8 元空间,在本地内存中。好处是发生OOM的概率比永久代小
    • 运行时常量池:可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息;当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
  • 堆内存:对象实例
    • 堆是线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
    • 组成:年轻代+老年代;【年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区】【老年代主要保存生命周期长的对象,一般是一些老的对象】
    • jdk1.7和1.8的区别:1.7中有有一个永久代(方法区),存储的是类信息、静态变量、常量、编译后的代码。1.8移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出。
  • 虚拟栈内存:方法执行的内存模型,每个方法执行时会创建一个栈帧,存储了局部变量、操作数栈(数据执行先进后出)、动态链接(方法所属类、方法名等)和方法出口(返回值,也可以是需要执行下一个方法的地址)信息。
    • GC不涉及栈内存,垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放。
    • 栈帧内存的分配不是越大越好的,默认的栈内存通常为1024k 。栈内存不变下,栈帧过大会导致线程数变少。
  • 程序计数器:当前程序运行位于字节码的哪一行
    • jvm对于多线程任务是通过线程轮流切换并分配线程执行时间来完成的。如果当前被执行的这个线程所分配的执行时间用完了【挂起】,处理器会切换到另外的一个线程上执行。当这个线程的执行时间用完了,接着处理器就会又来执行被挂起的这个线程。 为了使线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储。
    • 程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
  • 本地方法栈:与虚拟栈内存类似,但是服务的是native方法

堆和栈的区别

👉栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。

👉堆有GC垃圾回收,而栈没有。

👉栈内存是线程私有的,而堆内存是线程共享的。

👉两者异常错误不同,栈内存或者堆内存不足都会抛出异常

  • 栈空间不足:java.lang.StackOverFlowError
  • 堆空间不足:java.lang.OutOfMemoryError

👉在多线程环境下,堆和栈的线程安全性是不同的。

  • 在大多数情况下,堆是线程安全的,因为操作系统会采取相应的措施来确保多个线程可以安全地访问和修改堆中的内存。但是,在某些情况下,如果没有正确同步线程的访问,可能会导致堆上的数据损坏或者内存泄漏。
  • 栈在单个线程中是线程安全的。但是,在多线程环境下,不同线程的栈是相互独立的,彼此不受影响。如果多个线程同时访问同一块栈空间,可能会导致数据混乱和未定义行为。

为什么java调用方式设计成栈的形式

  1. 内存管理方便:栈内存分配和释放的速度非常快,因为栈内存分配是按照后进先出(LIFO)的顺序进行的。这意味着当我们进入一个新的方法时,新的栈帧会被压入栈中;当方法返回时,栈帧会被弹出。这种简单而一致的内存管理模型使得JVM(Java虚拟机)可以高效地进行内存分配和垃圾回收。
  2. 线程安全:每个线程都有自己的栈,因此每个线程都有自己独立的调用栈。这种设计使得多线程环境下的调用栈管理变得简单且安全。每个线程都可以独立地在其栈上进行方法调用和返回,而不会受到其他线程的影响。
  3. 支持递归调用:栈结构天然地支持递归调用。在递归调用中,每次方法调用都会生成一个新的栈帧,并压入调用栈中。当递归返回时,栈帧会按照相反的顺序弹出,直到最初的调用者。这种机制使得递归调用在Java中变得非常简单和直观。

栈溢出、内存泄漏和内存溢出

什么是栈溢出?如何避免栈溢出?

对象在 JVM 中是如何分配内存的?

加载类 -> 堆内存分配内存空间 -> 初始化对象 -> 将对象引用压入栈中

String在内存里的过程

  1. 对象创建:当使用双引号直接赋值的方式创建String对象时(例如:String str = “abc”;),JVM首先会检查字符串常量池中是否已存在该字符串对象。如果字符串常量池中已经存在该字符串对象,则不会重新创建,而是直接返回该字符串在常量池中的引用。如果不存在,则会在字符串常量池中创建该字符串对象,并返回其引用。
  2. 内存区域:字符串常量池位于方法区中,从Java 7开始,它位于堆内存中。在Java 8及以后的版本中,JVM中的方法区实现为元空间。
    字符串常量池存储的是字符串对象的引用,而不是对象本身。实际的String对象(字符数组)存储在堆内存中。
  3. 使用new关键字创建String对象:当使用new关键字创建String对象时(例如:String str2 = new String(“def”);),会在堆内存中创建一个新的String对象,并在字符串常量池中检查是否已存在对应的字符串。如果存在,常量池中该字符串的引用不会被使用来初始化新创建的String对象。无论常量池中是否存在该字符串,都会在堆内存中创建一个新的String对象,并且这个新对象的引用会赋值给变量(在这个例子中是str2)。
  4. 引用比较:使用双引号直接创建的String对象引用和通过new创建的String对象引用在比较时通常不相等,因为即使它们表示的字符串内容相同,它们也是堆中不同的对象,有不同的内存地址。
  5. 内存回收:当String对象不再被引用时,它们所占用的堆内存空间会成为垃圾,等待垃圾收集器(GC)进行回收。GC会标记不再可达的对象,并在适当的时机释放它们所占用的内存。

类加载

讲讲类加载机制,为什么需要类加载,

👉类加载机制是加载Java的字节码文件到JVM内存中,在内存中生成一个代表该类的class对象
👉why
将 Java 类文件加载到内存中,以便 JVM 可以执行它们。
验证 Java 类文件的正确性,确保它们符合 Java 规范。
为 Java 类分配内存空间。
初始化 Java 类中的静态变量。
将 Java 类中的符号引用(指向名称)转换为直接引用(指向地址)。

类加载的过程?

加载 -> 链接(即验证、准备、解析)-> 初始化

  1. 加载 : 把代码数据加载到内存中
  2. 验证 : 验证类是否符合JVM规范,安全性检查
  3. 准备(⭐) : 为类变量(静态变量)分配内存并设置类变量初始值(jvm的初始值)。(不会为类成员变量分配,非静态变量属于对象,不属于类,类加载的几个阶段都只针对类变量)
  4. 解析 : 把常量池中的符号引用转换为直接引用
    • 符号引用:以一组符号来描述所引用的目标
    • 直接引用:可以理解为一个内存地址,或者一个偏移量。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在
  5. 初始化(⭐) : 直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码。此阶段会对类的静态变量,静态代码块执行初始化操作(开发人员设置的初始值)
  6. 使用 : JVM 从入口方法开始执行用户的程序代码
  7. 卸载 : 当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象

有哪些类加载器

👉启动类加载器:由C++实现,用于加载 Java 虚拟机自身需要【JAVA_HOME/jre/lib】的类,不继承ClassLoader类。
👉扩展类加载器:用于加载 Java 扩展类库【JAVA_HOME/jre/lib/ext】中的类,是ClassLoader的子类。
👉系统类加载器:由 Java 应用程序实现,用于加载应用程序ClassPath中的类,是ClassLoader的子类。
👉自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则。

双亲委派模型是什么?

什么是 JIT 编译器?它有什么作用?

是一种即时编译器,一些热门代码在由字节码编译成机器码的过程中时,会被JIT永久保存下来。作用是提高运行效率

GC

什么是GC

👉回收不再使用的 Java 对象【没有任何的引用指向该对象】所占用的内存空间,防止内存泄漏。

GC与内存区的关系

GC定位算法,如何判断对象已经死亡

🎵如果一个对象不再被任何引用所引用,则说明该对象已经死亡。
👉引用计数算法:通过引用计数器来判断对象的存活状态。当对象的引用计数为 0 时,则说明该对象不再被任何引用所引用,可以被回收。【缺点:循环引用时会出现内存泄漏】
👉可达性分析算法:通过分析对象之间的引用关系来判断对象的存活状态。从 GC Roots 出发,沿着引用链进行遍历,如果某个对象无法通过任何引用链到达,则说明该对象不再被任何引用所引用,可以被回收。

什么是GC Roots

是指在 GC 过程中,始终可达的对象集合,他们不能被当成GC的对象。包括:虚拟机栈中的局部变量、方法区中的类静态变量和常量、本地方法栈中的 JNI 本地引用、所有被同步锁持有的对象、其他一些特殊引用,如 Java 虚拟机内部使用的对象

GC策略,即垃圾回收算法

👉标记-清除算法:先标记所有需要回收的对象,然后再清除这些对象的内存空间。【选要删的照片、删掉】缺点:清理出来的内存碎片化较为严重。
👉标记-整理算法:先标记所有需要回收的对象,然后再将存活的对象整理到一起,并清理空闲的内存空间。比起标记清除多了整理这一步,从而解决了碎片化问题,但是缺点是对象需要移动,效率还是较低的
👉复制算法:将存活的对象复制到新的内存空间中,然后再清理旧的内存空间。
无碎片、效率高;【选不删的照片、复制到新文件夹,删掉原来文件夹的所有照片】缺点是内存使用率低

GC的种类

👉新生代 GC:发生在新生代内存区,主要回收生命周期短的对象。
👉老年代 GC:发生在老年代内存区,主要回收生命周期长的对象。
👉全 GC:回收整个 Java 堆内存区中的所有对象。

这里再回顾一下堆内存的组成:年轻代+老年代;【年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区】【老年代主要保存生命周期长的对象,一般是一些老的对象】

MinorGC、 Mixed GC 、 FullGC的区别是什么?

👉MinorGC: 发生在新生代的垃圾回收,暂停时间短( STW )。【STW ( Stop-The-World ):暂停所有应用程序线程,等待垃圾回收的完成 。】
👉FullGC : 新生代 + 老年代完整垃圾回收,暂停时间长( STW ),应尽力避免。
👉Mixed GC: 新生代 + 老年代部分区域的垃圾回收, G1 收集器特有 。
【扩展】为什么要划分
总结来说,划分是为了更好地管理和优化Java的内存使用,提高内存管理效率,减少内存泄漏和性能下降的风险。比如Minor GC针对新生代进行快速而频繁的垃圾收集,从而保持较高的内存使用效率。而Full GC则负责处理整个Java堆的内存回收任务,确保整个Java堆的内存得到有效管理。

触发FullGC的两种情况

分代收集算法

对象回收分代回收的过程

👉新创建的对象首先会分配到Eden区【占新生代的8/10】
👉当Eden区内存不足,标记Eden区和幸存区from【占新生代的1/10】中的存活对象。
👉使用复制算法将存活对象复制到to【占新生代的1/10】中,然后释放from和Eden的内存。
👉一段时间后Eden内存又不足,标记Eden和to中存活的对象,复制到from中。
👉当幸存区对象熬过几次回收(15次),转到老年代中。(幸存区内存不足或大对象会提前转)

垃圾回收器

👉串行垃圾收集器

  • 垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停( STW ),等待垃圾回收的完成,适用于单机模式的虚拟机。
  • Serial:作用于新生代,采用【复制算法】
  • Serial Old: 作用于老年代,采用【标记 - 整理算法】

👉并行垃圾收集器( JDK8 默认使用此垃圾回收器)

  • ParNew收集器:是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为与Serial收集器完全一致。(除serial之外唯一能和CMS配合的)
  • Parallel Scavenge收集器:新生代收集器,基于【标记-复制】算法实现,和ParNew非常相似,区别在于该收集器更加关注吞吐量(高效利用CPU)。
  • Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于【标记-整理】算法实现。

👉CMS收集器

以获取最短回收停顿时间为目标的收集器,给用户带来良好的交互体验。基于【标记-清除】算法实现,整个过程分四个步骤:初始标记和重新标记耗时较短,需要STW暂停其他线程;而整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程可以与用户线程并行工作。

  1. 初始标记:标记GCRoots能直接关联到的对象,速度很快。
  2. 并发标记:标记GC Roots的间接关联对象,这个过程耗时较长但不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
  3. 重新标记:修正并发标记期间,因用户程序继续运作而导致标记产生变动。
  4. 并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

👉G1收集器

应用于新生代和老年代(JDK9之后默认使用的收集器)采用【标记-复制】算法,兼顾响应时间和吞吐量。将java堆划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备。

  1. 初始标记:标记一下GC Roots能直接关联到的对象,这个阶段需要停顿线程,但耗时很短。
  2. 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
  3. 最终标记:需要对用户线程做一个短暂的暂停,解决上一阶段的漏标问题。
  4. 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把这些Region中的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

强引用、软引用、弱引用、虚引用的区别?

不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响。
👉强引用:最传统的“引用”的定义,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
👉软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
👉弱引用:强度比软引用更弱,当垃圾回收器开始工作时,无论内存空间是否充足都会对弱引用的对象进行回收。
👉虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。主要用来跟踪对象被垃圾回收器回收的活动。

强引用,就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。

软引用,是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当JVM认为内存不足时,才会去试图回收软引用指向的对象。JVM会确保在抛出OutOfMemoryError之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用,并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多缓存实现的选择。

  • 上一篇: java基础语句ppt
  • 下一篇: java所有基础运用
  • 版权声明


    相关文章:

  • java基础语句ppt2025-04-15 18:02:01
  • java基础和javascript2025-04-15 18:02:01
  • 武汉java基础培训2025-04-15 18:02:01
  • java开发培训基础2025-04-15 18:02:01
  • java基础项目案例2025-04-15 18:02:01
  • java所有基础运用2025-04-15 18:02:01
  • java基础技能英文2025-04-15 18:02:01
  • java程序设计基础答案2025-04-15 18:02:01
  • java基础 教案2025-04-15 18:02:01
  • java基础用书2025-04-15 18:02:01