基础.md 12 KB

为什么重写 equals() 时必须重写 hashCode() 方法?

根据Java规范的规定,如果两个对象相等,那么它们的哈希值必须相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

例如hashset集合中插入的对象重写 equals()但是没有重写 hashCode() 就会导致插入重复的元素。

在java中,mian线程中申请某一个线程,main线程运行结束,申请的新线程为什么还会继续运行?

在 Java 中,与其他操作系统一样,线程是通过调度器来管理执行的。当一个进程中所有的非守护线程都执行完毕后,该进程会自动结束并终止所有守护线程。因此,如果在 main 线程中创建了一个新线程,main 线程结束之后,该新线程仍然会继续运行。

这是因为在Java中,除了守护线程以外,每个线程都是独立执行的,并且拥有自己独立的执行堆栈和执行上下文。在 main 线程中创建一个新线程后,该新线程会被 JVM 调度器分配 CPU 时间片,并独立运行。这时即使 main 线程结束,该新线程仍然可以继续运行,直到执行完毕或者被显式地终止。

需要注意的是,如果该新线程是守护线程,那么在所有非守护线程执行完毕后,JVM 会自动终止所有守护线程并退出进程。因此,如果该新线程是守护线程,则在 main 线程结束之后可能会被强制终止,而不会继续运行。

java线程生命周期

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • NEW: 初始状态,线程被创建出来但没有被调用 start()
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED:阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换

image-20230508161022413

JMM内存管理

Java 内存模型(JMM) 抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。

主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)

本地内存:每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。

JMM(Java 内存模型)

object类包含哪些方法

Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

/**
 * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
 */
public final native Class<?> getClass()
/**
 * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
 */
public native int hashCode()
/**
 * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
 */
public boolean equals(Object obj)
/**
 * native 方法,用于创建并返回当前对象的一份拷贝。
 */
protected native Object clone() throws CloneNotSupportedException
/**
 * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
 */
public String toString()
/**
 * native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
 */
public final native void notify()
/**
 * native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
 */
public final native void notifyAll()
/**
 * native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
 */
public final native void wait(long timeout) throws InterruptedException
/**
 * 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。
 */
public final void wait(long timeout, int nanos) throws InterruptedException
/**
 * 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
 */
public final void wait() throws InterruptedException
/**
 * 实例被垃圾回收器回收的时候触发的操作
 */
protected void finalize() throws Throwable { }

finalize会不会立即清理和释放资源

  1. 对 finalize 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中,它包含了名为 unfinalized 的静态变量(双向链表结构),Finalizer 也可被视为另一种引用对象(地位与软、弱、虚相当,只是不对外,无法直接使用)
  2. 当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 链表中

img

  1. Finalizer 类中还有另一个重要的静态变量,即 ReferenceQueue 引用队列,刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的 Finalizer 对象加入此引用队列
  2. 但此时 Dog 对象还没法被立刻回收,因为 unfinalized -> Finalizer 这一引用链还在引用它嘛,为的是【先别着急回收啊,等我调完 finalize 方法,再回收】
  3. FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从链表断开并真正调用 finallize 方法

img

  1. 由于整个 Finalizer 对象已经从 unfinalized 链表中断开,这样没谁能引用到它和狗对象,所以下次 gc 时就被回收了

finalize 缺点

  • 无法保证资源释放:FinalizerThread 是守护线程,代码很有可能没来得及执行完,线程就结束了

  • 无法判断是否发生错误:执行 finalize 方法时,会吞掉任意异常(Throwable)

  • 内存释放不及时:重写了 finalize 方法的对象在第一次被 gc 时,并不能及时释放它占用的内存,因为要等着 FinalizerThread 调用完 finalize,把它从 unfinalized 队列移除后,第二次 gc 时才能真正释放内存

设计模式的原则

  1. 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。一个类应该只负责完成单一的职责或功能,这样可以提高代码的可读性和可维护性。
  2. 开闭原则(Open Closed Principle,OCP):软件实体(类、模块等)应该对扩展开放,对修改关闭。通过使用抽象和接口,可以使得系统容易进行扩展,而不需要修改已有的代码。
  3. 里氏替换原则(Liskov Substitution Principle,LSP):子类应该能够替换其父类并且保持程序的逻辑正确性。子类应该遵守父类定义的接口和规范。
  4. 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该直接依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于具体实现细节,而是应该依赖于抽象。
  5. 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要的接口。一个类或模块应该只有与其相关的接口,而不应该依赖于不需要的接口。
  6. 迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有尽可能少的了解。减少对象之间的直接依赖关系可以提高代码的可维护性和灵活性。
  7. 组合/聚合复用原则(Composition/Aggregation Reuse Principle,CARP):优先使用组合或聚合关系,而不是继承关系,来实现代码复用。组合和聚合关系更加灵活,避免了类之间的紧耦合。

这些设计模式通常是为了遵循面向对象设计中的一些基本原则而产生的,虽然并非每个模式都严格对应一个特定的原则,但它们可以相互支持以实现更好的软件设计。以下是一些设计模式与相应原则之间的关系示例:

  1. 工厂模式 (Factory Pattern): 原则:依赖倒置原则 (Dependency Inversion Principle, DIP)。通过工厂模式,高层模块可以依赖于抽象的工厂接口,而不依赖于具体产品的创建细节。
  2. 单例模式 (Singleton Pattern): 原则:无特定对应。单例模式旨在确保一个类只有一个实例,以实现全局访问点和避免多次实例化。
  3. 策略模式 (Strategy Pattern): 原则:开闭原则 (Open/Closed Principle, OCP)。通过策略模式,可以在不修改客户端代码的情况下添加、修改或替换算法策略。
  4. 观察者模式 (Observer Pattern): 原则:开闭原则 (Open/Closed Principle, OCP)、依赖倒置原则 (Dependency Inversion Principle, DIP)。观察者模式允许主题与观察者之间保持松散的耦合,新的观察者可以随时添加,不会影响主题的代码。
  5. 适配器模式 (Adapter Pattern): 原则:里氏替换原则 (Liskov Substitution Principle, LSP)。适配器模式将一个类的接口适配为另一个类的接口,确保适配后的类能够替代原始类。
  6. 装饰器模式 (Decorator Pattern): 原则:开闭原则 (Open/Closed Principle, OCP)。通过装饰器模式,可以在不修改现有代码的情况下添加新的功能,扩展对象的行为。
  7. 代理模式 (Proxy Pattern): 原则:代理模式可以与多个原则关联,如单一职责原则 (Single Responsibility Principle, SRP)(代理负责控制访问)、开闭原则 (Open/Closed Principle, OCP)(通过代理可以添加额外行为而不修改原始类)等。
  8. 模板方法模式 (Template Method Pattern): 原则:无特定对应。模板方法模式定义了算法的框架,允许子类重写算法的特定步骤,但保持了整体的结构。

OOM如何排查

为什么会发生OOM

OOM 全称 “Out Of Memory”,表示内存耗尽。当 JVM 因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个错误

为什么会出现 OOM,一般由这些问题引起

  1. 分配过少:JVM 初始化内存小,业务使用了大量内存;或者不同 JVM 区域分配内存不合理
  2. 代码漏洞:某一个对象被频繁申请,不用了之后却没有被释放,导致内存耗尽

内存泄漏:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了。因为申请者不用了,而又不能被虚拟机分配给别人用

内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出

内存泄漏持续存在,最后一定会溢出,两者是因果关系

排查方法

命令方式

  1. 输入命令查看 JVM 内存分布 jmap -heap PID
  2. jmap -histo:live PID| more,JVM 内存对象列表按照对象所占内存大小排序

Dump 文件分析

Dump 文件是 Java 进程的内存镜像,其中主要包括 系统信息虚拟机属性完整的线程 Dump所有类和对象的状态 等信息

当程序发生内存溢出或 GC 异常情况时,怀疑 JVM 发生了 内存泄漏,这时我们就可以导出 Dump 文件分析

JVM 启动参数配置添加以下参数

  • -XX:+HeapDumpOnOutOfMemoryError
  • -XX:HeapDumpPath=./(参数为 Dump 文件生成路径)

当 JVM 发生 OOM 异常自动导出 Dump 文件,文件名称默认格式:java_pid{pid}.hprof

上面配置是在应用抛出 OOM 后自动导出 Dump,或者可以在 JVM 运行时导出 Dump 文件

jmap -dump:file=[文件路径] [pid]
 
# 示例
jmap -dump:file=./jvmdump.hprof 15162

使用visualVM分析