【八股】Java基础、集合、JVM

面向对象三大特性

1 封装:

将 方法 和 属性 写到同一个类中,并将属性 私有化,生成 get set方法,外部访问属性需要通过get和set方法,内部可以直接访问属性,这样的一个类我们认为它完成了封装。

2 继承:

子类继承父类,子类实例可以调用父类除private修饰以外的所有属性和方法,一个子类只能继承一个父类,但是可以多重继承,一个父类可以拥有多个子类。

3 多态:

父类的引用指向子类的实例,多个子类继承同一个父类实现了这些类共通的方法,每一个子类实现的结果都不同。

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

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

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

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

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

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

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

包装类型的缓存机制

缓存机制是一种用资源创建提升性能的手段
Byte Integer Short Long默认创建了[-128,127]的缓存数据
Character创建了数值在[0,127]的缓存数据
Boolean创建false和true的缓存数据
Double和Float则没有创建缓存数据

自动拆箱和装箱

装箱:把基本数据类型包装成引用数据类型 valueOf()
拆箱:把包装数据类型中转换成基本数据类型 xxxvalue()

String、StringBuilder、StringBuffer的区别

  • String:不可变、线程安全、性能较低、适用于常量字符串和少量字符串拼接。
  • StringBuilder:可变、线程不安全、性能较高、适用于频繁字符串拼接。
  • StringBuffer:可变、线程安全、性能较高、适用于多线程环境下字符串拼接。

变量

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

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

静态变量有什么作用?

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

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

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

方法

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

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

重载和重写有什么区别?

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

什么是可变长参数?

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

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

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

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

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

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

Object

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

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

object为什么要设计wait方法?

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

Object类中的常用方法

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

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

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

初始化时间

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

异常

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

Java引发异常的原因

运行时错误:由于代码逻辑错误或运行环境错误导致的异常,例如除以零、数组越界等。

输入错误:由于用户输入的数据不符合预期导致的异常,例如输入格式错误、输入超出范围等。

资源错误:由于对资源的错误使用导致的异常,例如打开不存在的文件、网络连接错误等。

环境错误:由于运行环境的问题导致的异常,例如内存不足、硬件故障等。

异常情况:由于程序逻辑的异常情况导致的异常,例如数据错误、业务逻辑错误等。

异常处理方法

try-catch语句:这是最常用的异常处理方法。在try块中编写可能抛出异常的代码,如果try块中的代码抛出异常,程序会跳转到相应的catch块中处理该异常。

throws关键字:可以将异常抛给调用该方法的上一级方法处理。如果方法内部抛出了异常,而没有捕获处理,那么必须使用throws声明,否则编译会报错。

finally关键字:用于编写一段无论是否发生异常都会被执行的代码块。通常用于释放资源或进行一些清理操作。

Error和Exception的父类

他们父类是Throwable。Throwable是Java异常处理机制的核心类,它代表了一个可以被抛出或捕获的对象。所有的错误和异常都是Throwable类的子类。

它们之间的主要区别在于,Error通常表示严重的问题,这些问题通常是JVM无法解决的,如系统内部错误或资源耗尽等,因此程序员通常不编写特定的代码来处理这些错误。而Exception则表示程序运行过程中可能发生的异常条件,这些异常条件通常是由于编程错误或外部因素导致的,程序员可以通过编写特定的代码来捕获和处理这些异常。

怎么做全局异常处理

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

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

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

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

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

集合

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

List 有序 可重复 实现Collection接口

ArrayList 和 LinkedList 的区别是什么?

AL是基于数组实现的,增删慢,查询快【支持随机访问O(1)】

LL是基于链表实现的,增删快,查询慢【O(n)】,消耗更多内存

Set 无序 不重复 实现Collection接口

HashSet 和 TreeSet 的区别是什么?

HS 无序 基于哈希表

TS 有序 基于红黑树

Map 无序 不重复 实现Map接口

HashMap 和 LinkedHashMap 的区别是什么?

HM 基于哈希表

LHM 基于链表

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

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

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

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

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

HashMap的负载因子

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

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

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

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

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

怎么选用集合

需要存储键值,选Map。要求有序,TreeMap;要求无序,HashMap;要求线程安全,ConcurrentMap

只需要存储元素,选Collection。要求元素唯一:Set;不要求元素唯一:List

Hash冲突有哪些解决方法

👉开放寻址

线性(往后找空位置) | 二次(按固定步长找空位置) | 伪随机(随机找空位置)

👉拉链法

将冲突的元素链接到一个链表中

JVM

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

👉JVM就是Java虚拟机,全称Java Virtual Machine,用于将Java字节码转换成机器码并执行Java代码。

👉功能是加载、验证、执行java字节码;管理Java程序的内存;提供垃圾回收机制

👉优点:能够让Java程序仅通过一次编译但在不同的操作系统上执行;提供垃圾回收机制

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

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

  1. 类加载器把Java代码转换为字节码。
  2. 内存管理器把字节码加载到内存中,而字节码文件只是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中变得非常简单和直观。

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

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

当方法调用的深度超出了栈内存的限制【栈帧过多】,比如无限递归,或者是方法定义的局部变量过多会导致栈溢出【栈帧过大】。

解决方案:动态扩展栈(HotSpot不支持),减少递归调用深度、减少局部变量。

对象在 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,实现自定义类加载规则。

双亲委派模型是什么?

👉类加载机制,在类加载的时候会先加载父类的Java类,如果父类加载不了才会加载子类

👉why this -> 唯一性:避免相同的类被重复多次加载;安全性:保证类库API不会被修改

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

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

GC

什么是GC

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

GC与内存区的关系

👉GC 主要工作在 Java 堆内存区,负责回收不再使用的 Java 对象所占用的内存空间。GC 会根据对象的存活状态来判断哪些对象需要被回收,并清理这些对象的内存空间。

👉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的两种情况

👉①在 MinorGC之前,会判断老年代中是否容得下新生代的对象,容不下会触发FullGC。

👉②在MinorGC之后老年代发生OOM。

分代收集算法

👉根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代【占1/3】和老年代【占2/3】,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

👉 在新生代中,每次收集都会有大量对象死去,所以可以选择【复制】算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择【标记-清除】或【标记-整理】算法进行垃圾收集。

对象回收分代回收的过程

👉新创建的对象首先会分配到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的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

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

👉强引用:最传统的"引用"的定义,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

👉软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

👉弱引用:强度比软引用更弱,当垃圾回收器开始工作时,无论内存空间是否充足都会对弱引用的对象进行回收。

👉虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。主要用来跟踪对象被垃圾回收器回收的活动。

相关推荐
何曾参静谧6 分钟前
「QT」文件类 之 QTextStream 文本流类
开发语言·qt
monkey_meng9 分钟前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss17 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
我救我自己17 分钟前
UE5运行时创建slate窗口
java·服务器·ue5
2401_8532757338 分钟前
ArrayList 源码分析
java·开发语言
zyx没烦恼38 分钟前
【STL】set,multiset,map,multimap的介绍以及使用
开发语言·c++
lb363636363638 分钟前
整数储存形式(c基础)
c语言·开发语言
feifeikon41 分钟前
Python Day5 进阶语法(列表表达式/三元/断言/with-as/异常捕获/字符串方法/lambda函数
开发语言·python
爪哇学长42 分钟前
SQL 注入详解:原理、危害与防范措施
xml·java·数据库·sql·oracle
大鲤余1 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust