JAVA基础整理

目录

JAVA的基本特点

  面对对象

  抽象

  接口

语法

  == 和 equals()、hashCode()

  String、StringBuffer、StringBuilder

  ArrayList和LinkedList

  反射

  静态方法

集合

  Collection

  ArrayList

  LinkedList

  Set

  Queue

Map

HashMap

Hashtable

ConcurrentHashMap

设计模式

  单例模式

  工厂模式

  装饰器模式

异常

  Exception

  try-catch-finally

多线程并发

创建线程

线程池

synchronized

ReentrantLock

ReadWriteLock

volatile

死锁情况

类加载

JVM

运行时数据区

方法区(Method Area)

Java堆(Java Heap)

程序计数器(Program Counter Register)

Java虚拟机栈

本地方法栈(Native Method Stack)

垃圾回收

内存分配

标记算法

垃圾收集算法

引用

IO

Spring

MVC

容器

AOP

IOC

Beam

Springboot

Springcloud

Mybatis

结构

流程

优点

$ 和 #

Mybatis-plus


JAVA的基本特点

  • Java属于解释型语言,源代码不直接翻译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。

  • Java 适用于跨平台的企业级应用开发;C# 适用于构建 Windows 平台的应用程序;C++ 适用于需要高性能和直接内存控制的系统级开发。Python 适用于快速开发、数据处理和科学计算。

  面对对象

  • 封装:把一个对象的属性隐藏在对象内部,不允许外部直接访问,但可以提供一些可以被访问的方法来操作属性,保证了对象的数据安全。

  • 继承:使用已存在的类的定义作为基础建立新类,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过继承可以快速地创建新的类,提高代码的重用性。(父类中的私有属性和方法子类是无法访问,只是拥有)

  • 多态:一个对象具有多种的状态,例如父类中定义的属性和方法,子类继承后可以有不同的数据类型或表现出不同行为,体现代码的重用性。

    • 编译时多态:通过方法重载(overload)来实现,方法名相同,java根据方法参数列表的不同来区分不同的方法。

    • 运行时多态:通过方法重写(override)来实现,让子类继承父类并重写父类中已有的或抽象的方法。子类中与父类中的方法具有相同的方法名、参数列表和返回类型。

  抽象

  • 当一个类没有足够的信息来描绘一个具体对象,这样的类就是抽象类,无法实例化不能new一个抽象类

  • 举例:猫和狗都是动物,都同时具备喝水睡觉等属性,这些属性就可以在动物这个抽象类里实现,通过创建猫狗类继承这些属性,并实现更多功能,体现了代码复用性。其实就是把多个类当中的共性集合起来作用一个通用方法节省资源。

  • 实现方式:abstract关键字来修饰类或方法。不能被 final 修饰,因为需要被子类继承,而final类不能被继承。

  • 作用:抽象类主要是用来将差异性的方法抽象化,由子类扩展发挥;共同性的方法具体化,由所有子类继承。

  • 特性:抽象类中可以没有抽象方法,但如果一个方法是抽象方法,其所在的类必须是抽象类。

  接口

  • 接口是一种引用类型,不能new接口的对象

  • 特点:接口中的方法不能在接口实现,且只能public修饰,接口中变量默认被final public static修饰。

  • 实现方式:通过implements关键字。

  • 作用:接口用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。

语法

  == 和 equals()、hashCode()

  • 对于基本数据类型来说,== 比较的是值;对于引用数据类型来说,== 比较的是对象的内存地址。

  • 在Object 类 equals() 方法就是==,在String类中重写了equals() 方法使其仅仅比较值。

  • hashCode() 的作用是获取哈希码来判断对象是否相等,当两个对象的hashCode 值相等,这两个对象不一定相等(哈希碰撞)

  String、StringBuffer、StringBuilder

  • String 类使用 final 关键字修饰,在进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。所以仅适用于操作少量的数据,同时也是线程安全的,

  • StringBuffer 对方法或者对调用的方法加了同步锁,是线程安全的。StringBuilder 是非线程安全的。

  ArrayList和LinkedList

    ArrayList是Array(动态数组)的数据结构,LinkedList是Link(链表)的数据结构。

    当随机访问List(get和set操作)时,ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,需要移动指针从前往后依次查找。

    当对数据进行增加和删除的操作(add和remove操作)时,LinkedList比ArrayList的效率更高,因为ArrayList是数组,进行增删操作时,会对所有数据的下标索引造成影响,需要进行数据移动。

    ArrayList自由性较低,需要手动的设置固定大小的容量,但是使用比较方便,只需要创建然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但不便于使用。

  反射

  • 在运行状态中对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用它的方法和属性;这种动态获取的信息以及动态调用对象方法的功能称为反射机制。

  • 使用场景:像 Spring/Spring Boot、MyBatis 等框架中大量使用了动态代理,而动态代理的实现也依赖反射。

  • 特点:让代码更加灵活、为各种框架提供开箱即用的功能提供了便利,但也增加了安全问题。

  静态方法

  类的静态成员属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问,优处就是不用生成类的实例就可以直接调用,不需要再消耗资源反复创建对象。

  非静态成员属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过实例去访问。

  在一个类的静态成员中去访问其非静态成员之所以会出错是因为在类的非静态成员不存在的时候类的静态成员就已经存在了,访问一个内存中不存在的东西当然会出错。

集合

  Java 集合也叫作容器,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 又有三个主要的子接口:ListSetQueue

  List: 存储的元素是有序的、可重复的。

  Set: 存储的元素不可重复的。

  Queue: 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。

  Map: 使用键值对存储,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值

  Collection

  ArrayList

  ArrayList 内部基于动态数组实现,比 Array(静态数组) 使用起来更加灵活:

  • ArrayList创建时不需要指定大小,会根据实际存储的元素动态地扩容或缩容,而 Array 必须指定大小,被创建之后就不能改变它的长度了。

  • ArrayList 中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。Array 可以直接存储基本类型数据,也可以存储对象。

  • ArrayList 支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如 add()remove()等。Array 只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。

  Vector:线程安全的ArrayList

  LinkedList

  一个双向链表,每个节点都包含对前一个节点和后一个节点的引用,不保证线程安全,插入、删除元素的操作非常高效,时间复杂度为 O(1)。LinkedList 中的元素是分散存储在内存中的各个节点中。这使得在任意位置插入、删除元素的操作都很快,不需要移动大量元素。但是指定位置插入/删除需要先移动到指定位置,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。

  Set
  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素。

  • LinkedHashSet: LinkedHashSet 是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。

  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)。

  Queue
  • PriorityQueue: Object[] 数组来实现小顶堆。

  • DelayQueue:PriorityQueue。详细可以查看:

  • ArrayDeque: 可扩容动态双向数组。

Map

HashMap

JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。

JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。设置一个负载因子(指HashMap中已存储的元素数量与当前容量的比值)当负载因子超过设定的阈值时,就会触发扩容操作。扩容操作一般会创建一个两倍的数组,然后将原有数组中的元素重新分配到新数组中。

线程不安全:在 HashMap 中,多个键值对可能会被分配到同一个桶,并以链表或红黑树的形式存储。多个线程对 HashMap 的 put 操作会导致线程不安全

HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。(尽量把数据分配均匀)

Hashtable

数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的。Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。

使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

ConcurrentHashMap

JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。

JDK1.7

ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。Segment 的个数一旦初始化就不能改变。 Segment 数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。

Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。也就是说,对同一 Segment 的并发写入会被阻塞,不同 Segment 的写入是可以并发执行的。

JDK1.8

ConcurrentHashMap 取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全。数据结构跟 HashMap 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。

这样使得锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。

到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;

LinkedHashMapLinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。

TreeMap:红黑树(自平衡的排序二叉树)。

设计模式

  单例模式

  单例模式是一种设计模式,用于确保类在应用程序中只有一个实例,并提供全局访问点以访问该实例。它的主要特点包括:

  1. 单一实例: 单例模式确保一个类只有一个实例对象存在。无论在应用程序的任何地方创建多少次该类的实例请求,始终只会得到同一个实例。

  2. 全局访问点: 单例模式提供了一个全局访问点,允许应用程序中的其他对象通过该访问点获取该类的唯一实例。

  单例模式通常用于以下情况:

  • 当类的实例只能存在一个,而且需要被全局访问时。

  • 当需要控制资源的分配,例如数据库连接池或线程池时。

  • 当需要在整个应用程序中共享某个资源或状态时。

  单例模式可以确保该类的实例只被创建一次,节省了系统资源;允许其他对象通过全局访问点获实例,简化了对象之间的通信协作;可以确保该类的实例始终保持一致性;在实现单例模式时,需要考虑线程安全、延迟初始化和性能等因素。

  常见的单例模式。

  1. 懒汉式: 在首次使用时才创建实例。非线程安全的懒汉式单例模式简单易实现,但在多线程环境下可能会出现多个实例被创建的问题,需要考虑加锁解决。线程安全的懒汉式单例模式通常使用双重检查锁定或静态内部类来实现,能够确保在多线程环境下只创建一个实例。

  2. 饿汉式: 在类加载时就创建实例。能够保证在多线程环境下只创建一个实例,但可能会造成资源的浪费,因为没有被使用也会被创建。

  3. 静态内部类: 使用静态内部类来延迟初始化,实现了懒加载且线程安全。静态内部类在首次加载时不会被初始化,只有在调用 getInstance() 方法时才会加载并创建实例。这种方式既避免了饿汉式单例模式的资源浪费问题,又解决了懒汉式单例模式的线程安全问题。

  4. 枚举: 利用枚举类型的特性,保证了实例的唯一性和线程安全性。枚举单例模式是最简洁、最安全的单例模式之一,因为枚举类型的实例是在加载枚举类时被创建,并且由 JVM 保证了线程安全性和唯一性。

 

  工厂模式

  工厂模式是一种创建型设计模式,它提供了一种创建对象的方式,不需要在客户端代码中直接使用 new 关键字来实例化对象。工厂模式将对象的实例化过程封装在一个工厂类中,客户端只需要通过工厂类来获取所需的对象,而无需关心对象的具体实现细节。

  1. 简单工厂模式: 通过一个工厂类来创建对象,客户端只需要提供一个参数,工厂类根据这个参数决定创建哪种具体的对象。简单工厂模式适用于创建对象的逻辑相对简单的情况。

  2. 工厂方法模式: 定义了一个创建对象的接口,但将具体的创建逻辑延迟到子类中实现。每个具体的子类都可以根据需要来创建自己特定类型的对象,客户端只需要通过调用工厂方法来获取对象实例即可。

  3. 抽象工厂模式: 提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。它通过一个工厂接口和多个具体工厂实现类来创建一组相关的产品对象,使得客户端可以在不必知道具体实现的情况下创建一组对象。

  工厂模式的主要优点包括:

  • 将对象的创建和使用分离,降低了客户端与具体对象之间的耦合度。

  • 通过工厂类来集中管理对象的创建过程,提高了代码的可维护性和可扩展性。

  • 便于扩展和替换具体对象的实现,满足不同的需求。

  装饰器模式

  装饰器模式是一种结构型设计模式,它允许在不改变对象接口的情况下,动态地给对象添加新的行为。装饰器模式通过将对象封装在装饰器类中,并在装饰器类中添加额外的功能来实现这一点。

  1. 角色和结构:

    1. Component(组件): 定义了一个抽象接口,可以是一个接口或者抽象类,是被装饰的对象的基类。

    2. ConcreteComponent(具体组件): 实现了 Component 接口,是被装饰的具体对象,也是装饰器模式中的目标对象。

    3. Decorator(装饰器): 实现了 Component 接口,并持有一个指向 Component 对象的引用,可以添加额外的行为或状态。

    4. ConcreteDecorator(具体装饰器): 扩展了 Decorator 类,实现了具体的装饰功能,并在调用父类方法时调用了被装饰对象的方法。

  2. 工作原理:

    1. 客户端通过 Component 接口与具体组件进行交互,装饰器模式允许客户端在运行时动态地将装饰器对象包装在目标对象上。

    2. 装饰器模式采用递归组合的方式来实现对对象的装饰。具体装饰器通过继承或实现装饰器接口,并在构造函数中接收一个 Component 对象的引用。

  3. 优点:

    1. 装饰器模式遵循开闭原则,可以动态地添加、移除或更改对象的功能,而不需要修改现有的代码。

    2. 装饰器模式避免了使用子类继承来扩展对象的功能,使得系统更加灵活、可扩展。

  4. 缺点:

    1. 装饰器模式会导致类的数量增加,增加了系统的复杂性。

    2. 如果装饰器的层级结构过深,会影响代码的可读性和维护性。

  5. 适用场景:

    1. 当需要动态地为对象添加额外的功能时,而且这些功能可以灵活地组合时,可以考虑使用装饰器模式。

异常

  • Throwable 是 Java 语言中所有错误与异常的超类,包涵两个子类

  Exception

  • 程序本身可以处理的异常可以通过 catch 来进行捕获。

  • Checked Exception (受检查异常):如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。除了RuntimeException(运行时异常)及其子类以外,其他的Exception类及其子类都属于受检查异常 。

  • Unchecked Exception (不受检查异常 ):不处理不受检查异常也可正常通过编译。RuntimeException 及其子类都统称为非受检查异常,包括NullPointerException、ArrayIndexOutOfBoundsException等。

  • Error 程序中无法处理的错误

  try-catch-finally

  • try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。

  • catch块:用于处理 try 捕获到的异常。

  • finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。(除非终止虚拟机System.exit(1))

  • throw和throws

    • throws(异常的申明):若方法中存在检查异常并不对其捕获,那必须在方法头中显式声明该异常。

    • throw(异常的抛出):如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它。

多线程并发

创建线程

继承Thread类,重写run方法

public class ExtendsThread extends Thread {
    @Override
    public void run() {
        System.out.println("1......");
    }
    public static void main(String[] args) {
        new ExtendsThread().start();
    }
}

实现Runnable接口并重写run方法

public class ImplementsRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("2......");
    }
    public static void main(String[] args) {
        ImplementsRunnable runnable = new ImplementsRunnable();
        new Thread(runnable).start();
    }
}

实现Callable接口

public class ImplementsCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("3......");
        return "zhuZi";
    }
    public static void main(String[] args) throws Exception {
        ImplementsCallable callable = new ImplementsCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

线程池

  1. 任务队列:用于存放待执行的任务。任务可以是Runnable或Callable类型的对象。

  2. 线程池管理器:负责创建、销毁和管理线程池中的线程。它根据需要动态地调整线程池的大小。

  3. 工作线程:线程池中的实际执行任务的线程。这些线程从任务队列中获取任务并执行。

线程池的优点包括:

  • 降低资源消耗:由于线程的创建和销毁开销较大,线程池可以重用已经创建的线程,避免频繁地创建和销毁线程,从而降低了资源消耗。

  • 提高响应速度:线程池中的线程可以立即执行任务,而不需要等待线程创建。

  • 控制并发度:通过合理调整线程池的大小,限制并发执行的任务数量,避免系统资源被过度占用。

Java中的线程池通常使用java.util.concurrent包下的ExecutorService接口及其实现类来实现。常用的线程池实现类包括ThreadPoolExecutorScheduledThreadPoolExecutor

Java中提供了三种常用的内置线程池:

  1. FixedThreadPool(固定大小线程池):该线程池创建固定数量的线程,当线程池中的线程都在执行任务时,新的任务会被放入任务队列中等待。适用于执行长期的任务,性能稳定。

  2. CachedThreadPool(缓存线程池):该线程池会根据任务的数量动态地创建新的线程,空闲线程会保留60秒钟,并且在需要时会被重用,避免了创建线程的开销。适用于执行短期异步任务的场景。

  3. SingleThreadExecutor(单线程线程池):该线程池只创建一个单线程执行任务,所有任务按顺序在该线程中执行。适用于需要保证任务按顺序执行、线程安全的场景。

这些线程池都是ExecutorService接口的实现类,并且都是通过Executors工厂类创建的。

synchronized

在 Java 中,synchronized 关键字用于实现线程同步,确保多个线程在访问共享资源时的安全性。synchronized 可以用来修饰方法或代码块,实现不同粒度的同步。

当 synchronized 修饰一个实例方法时,它获取的是该实例对象的锁。在同一时刻,只有一个线程可以持有该对象的锁,其他线程在访问该方法时会被阻塞

ReentrantLock
  1. 可重入性:ReentrantLock 支持锁的重入,即同一个线程可以多次获取同一个锁,而不会导致死锁。

  2. 公平性:ReentrantLock 支持公平锁和非公平锁,默认情况下是非公平锁。公平锁会按照线程的请求顺序获取锁,而非公平锁允许当前正在等待的线程立即获取锁,可能会导致等待时间较长的线程饥饿。

  3. 条件变量:ReentrantLock 提供了 Condition 接口,可以使用它来实现线程的等待和通知机制,用于替代 Object 类的 wait()、notify() 和 notifyAll() 方法。使用条件变量可以更精细地控制线程的等待和唤醒。

  4. 锁中断:ReentrantLock 支持锁的中断功能,即在等待获取锁的过程中可以响应中断请求,避免线程长时间等待而导致的性能问题。

  5. 锁超时:ReentrantLock 提供了尝试获取锁的方法 tryLock(),可以在指定的时间内尝试获取锁,避免线程长时间等待而导致的性能问题。

ReadWriteLock
volatile

volatile 是 Java 中的一个关键字,用来修饰变量。它的主要作用是确保变量的可见性和有序性,但不能保证原子性。

  1. 可见性:当一个变量被 volatile 关键字修饰时,在一个线程中修改了该变量的值,其他线程能够立即看到修改后的值。这是因为在每次访问 volatile 变量时,都会从主内存中重新读取最新的值,而不是使用线程的本地缓存。

  2. 有序性:volatile 关键字可以确保被修饰的变量的读写操作按照程序代码的顺序执行,不会被重排序。这可以防止指令重排序导致的程序执行顺序错误。

  3. 原子性:volatile 不能保证原子性,即不能保证复合操作的原子性。例如,对 volatile 变量进行自增操作 count++ 是一个复合操作,包括读取变量的值、增加变量的值、写入变量的值三个步骤。在多线程环境下,如果多个线程同时对同一个 volatile 变量进行自增操作,就会出现竞态条件,从而导致最终结果出现错误。

死锁情况
public class DeadLockExample {
    private static Object lock1 = new Object();// 创建两个用于死锁的对象锁
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() { // 创建第一个线程t1
            public void run() {
                synchronized (lock1) {// t1线程尝试获取lock1对象锁
                    try {
                        // 打印信息表示t1线程已经获取了lock1锁,并正在等待lock2锁
                        System.out.println("Thread 1: Holding lock 1...Waiting for lock 2...");
                        // 线程t1休眠1秒,模拟等待其他操作
                        Thread.sleep(1000);// 尝试获取lock2对象锁
                        synchronized (lock2) {
                            // 如果t1线程成功获取了lock2锁,打印信息
                            System.out.println("Thread 1: Holding lock 1 and lock 2");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();// 如果线程被中断,打印堆栈跟踪
                    }
                }
            }
        });
        // 创建第二个线程t2
        Thread t2 = new Thread(new Runnable() {
            public void run() {
                synchronized (lock2) { // t2线程尝试获取lock2对象锁
                    try {
                        // 打印信息表示t2线程已经获取了lock2锁,并正在等待lock1锁
                        System.out.println("Thread 2: Holding lock 2...Waiting for lock 1...");
                        // 线程t2休眠1秒,模拟等待其他操作
                        Thread.sleep(1000); // 尝试获取lock1对象锁  
                        synchronized (lock1) {// 如果t2线程成功获取了lock1锁,打印信息
                            System.out.println("Thread 2: Holding lock 1 and lock 2");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();// 如果线程被中断,打印堆栈跟踪
                    }
                }
            }
        });
        t1.start();// 启动线程t1
        t2.start();// 启动线程t2
    }
}

类加载

Java类加载过程是指将Java类的字节码文件加载到内存中,并将其转换成Java虚拟机可以识别的数据结构的过程。Java类加载器负责执行这一过程,它是Java虚拟机的一部分,主要任务是将类加载到内存中,并生成对应的Class对象。

  1. 加载: 类加载的第一个阶段,负责查找并加载类的字节码文件。当程序中使用到某个类时,类加载器会先搜索该类的字节码文件,通常是从文件系统或网络中加载。加载完成后将字节码文件的数据存储在方法区(Java 8及之前称为永久代,Java 8之后称为元空间),并创建一个对应的Class对象。

  2. 链接: 链接阶段包括三个步骤:验证、准备、解析。

    1. 验证:确保类的字节码文件符合Java虚拟机规范,并且没有安全方面的问题,如类型转换错误、访问权限错误等。

    2. 准备:为类的静态变量分配内存,并设置默认初始值。

    3. 解析:将符号引用转换为直接引用,解析类、方法、字段等符号引用。

  3. 初始化: 初始化阶段是类加载的最后一个阶段,它负责执行类的初始化代码,包括静态变量赋值和静态代码块的执行。初始化是类加载的最后一步,它是类加载过程中的一个重要步骤,且是一个线程安全的过程,确保类的初始化只会发生一次。

Java类加载器主要分为三种类型:启动类加载器、扩展类加载器和应用程序类加载器。按照一定的委托关系形成了类加载器层次结构,负责不同路径下类的加载和查找,保证类加载的顺序和正确性。

JVM

运行时数据区

方法区(Method Area)

方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

Java堆(Java Heap)

java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。所有的对象实例以及数组都要在堆上分配。java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。

从内存回收角度来看java堆可分为:新生代和老生代。从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。

java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,它可以看作是保存线程所正在执行的字节码指令的地址。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

Java虚拟机栈

java虚拟机是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。

本地方法栈(Native Method Stack)

本地方法栈和栈很像,只不过方法上带了 native 关键字的栈字,它是虚拟机栈为虚拟机执行Java方法(也就是字节码)的服务。native关键字的方法是看不到的。

为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

垃圾回收

内存分配

程序在运行过程中,会产生大量的内存垃圾(已经没有引用指向的内存对象都属于内存垃圾),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。

GC是不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行那个对象,即使程序员能明确地判断出有一块内存已经无用了,程序员也不能强制垃圾收集器回收该内存块。唯一能做的就是通过调用System.gc 方法来"建议"执行垃圾收集器,但是他是否执行,什么时候执行却都是不可知的。

新生代

新生代内存按照8:1:1的比例分为一个 eden区和两个survivor(survivor0,survivor1) 区。大部分对象在Eden区中生成,JVM会给每个对象定义一个年龄计数器,存储在对象头中

回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区。当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的然后将survivor0区和survivor1区交换,即保持survivor1区为空,如此往复。

当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。当 eden 区没有足够空间进行分配时,虚拟机将发起一次新生代GC(Minor GC)。Minor GC非常频繁,回收速度一般也比较快。

老年代

长期存活的对象进入老年代,内存比新生代也大很多(大概比例是1:2)。每经过一次Minor GC后对象仍然存活,并且能被Survivor区域容纳的话,对象则会被移动到Survivor区域,同时将对象的年龄+1岁。增加到一定年龄(默认15,可通过-XX:MaxTenuringThreshold参数设置),就会被移动到老年代中。

老年代GC(Major GC/Full GC):指发生在老年代的GC,新生代老年代都进行回收。出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。

大对象直接进入老年代大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。可通过-XX:PretenureSizeThreshold参数进行设置,当对象内存大于设定的值的话,会直接被分配到老年代。

持久代

永久代不属于堆内存,属于方法区。主要用于静态文件,如Java类、方法等,Class在类加载的时候被放入永久代。GC不会在主程序运行期对永久代进行清理,会随着加载的Class的增多而爆满,最终抛出OOM异常。永久代触发垃圾回收的条件比较困难

内存泄漏

内存泄漏是指程序中已经不再使用的内存仍然被占用,无法被回收和释放,导致系统内存占用不断增加,最终可能导致系统性能下降或崩溃。

  1. 未释放动态分配的内存: 程序中使用动态分配内存(如使用 mallocnew 等分配内存),但忘记释放这些内存,导致内存泄漏。

  2. 循环引用: 当对象之间存在循环引用关系时,即使这些对象已经不再被程序使用,它们之间的引用关系也会导致内存泄漏。

  3. 资源未释放: 程序中使用了一些资源(数据库连接、网络连接等),但在使用完毕后未正确释放这些资源,导致资源泄漏。

  4. 缓存未清理: 程序中使用了缓存,但在缓存中的数据已经不再需要时未进行清理,导致内存泄漏。

标记算法

引用计数算法

在每个对象中添加一个计数器,当有一个地方引用它的时候计数器的值就会增加1;当引用失效的时候计数器的值则会减1。当计数器的值为0时,则可认为这个对象已经不再使用。

优点:效率很高,不需要遍历所有对象。

缺点:无法解决对象之间循环引用的问题。目前Java虚拟机都没有选用引用计数算法来进行标记。

可达性分析算法

用一系列的“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径被称为”引用链“。如果一个对象到”GC Roots”没有任何的引用链相连,则证明此对象可能不再被使用。

哪些可以作为根对象:始终处于活跃状态,不会轻易被被垃圾收集器回收的。

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象。

  2. 在方法区中引用的对象,如字符串常量池(String Table)里的引用

  3. 本地方法栈中JNI引用的对象

  4. Java虚拟机内部的引用,如基本数据类型对应的Class对象以及一些常驻的异常对象等。

垃圾收集算法

标记-清除算法(Mark-Sweep)

统一回收掉所有被标记的对象。大量标记和清除操作,执行效率会随对象增多而降低。引起严重的内存碎片化问题。标记、清除之后会产生大量不连续的内存空间,这可能会导致在需要分配大对象时无法找到足够的连续空间,进而引发GC。

标记-复制算法(Copying)

复制算法将内存划分为大小相等的两块,分配对象时只使用其中的一块。当这块内存用完时,就将存活的对象复制到另外一块上面,然后把已使用的这块内存一次性清理掉。内存使用率太低,每次的内存回收都是对内存区间的一半进行回收。

标记-整理算法(Mark-Compact)

让所有存活的对象向内存的一端移动,然后直接清除掉边界外的内存。移动存活对象并更新所有被移动对象的引用是一个比较耗时的操作。而且,在移动对象时必须暂停所有用户线程才能进行(这一操作有个专有名词叫“Stop The World”,简称STW)。拖累了用户程序的执行效率

分代收集(Generational Collection)

新生代中对象存活率比较低,因此在新时代采用优化了的复制算法。分配对象只使用Eden和其中的一块Surivor区域,在标记完成后将存活的对象复制到另外一块Survior空间中,然后清除Eden和使用的一块Surivor。

老年代每次垃圾回收存活的对象比较多,因此这一区域采用的是标记-整理算法进行垃圾回收目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,就可以根据各个年代的特点选择合适的垃圾收集算法。

引用

强引用(HardReference)

大部分引用实际上都是强引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

软引用(SoftReference)

如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存。如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

弱引用(WeakReference)

只具有弱引用的对象拥有更短暂的生命周期。一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用(PhantomReference)

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

虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

IO

Spring

Spring 是一个轻量级的Java 开发框架,主要依存于SSM 框架,即Spring MVC + Spring + Mybatis,Spring MVC主要负责view 层的显示,Spring 利用IOC 和AOP来处理业务,Mybatis则是数据的持久化到数据库。

MVC

模型(Model):模型是应用程序的数据和业务逻辑的表示。它负责处理数据的读取、存储和操作,以及业务规则的处理。模型通常是独立于用户界面的,可以在不同的视图和控制器之间共享和重用。

视图(View):视图是用户界面的呈现部分,负责展示数据给用户,并接收用户的输入。视图通常是根据模型的数据进行渲染和更新的,它可以是Web页面、图形界面或命令行界面等。

控制器(Controller):控制器是模型和视图之间的协调者,负责接收用户的输入并根据输入调用相应的模型逻辑。控制器将用户的请求转发给模型进行处理,并将处理结果传递给视图进行展示。

MVC将程序的不同关注点(数据、业务逻辑、用户界面)分离开来,使得各个组件可以独立开发、测试和维护,提高了代码的可读性和可维护性,使得各个组件之间的耦合度降低,可以更方便地对应用程序进行扩展和修改,而不会影响其他组件的功能。

容器

容器是工厂模式的实现,是用来管理对象的。容器将创建对象,把它们连接在一起,配置它们,并由其通过IoC技术管理,串联他们的整个生命周期从创建到销毁。使得程序员不用创建对象,也不用创建对象的关联。只需要从配置的信息中告诉Spring容器。

AOP

AOP又名Aspect Oriented Programming 意为 ‘面向切面编程’通过预编译和运行期间动态代理来实现程序功能的统一维护的一种技术。

从某种角度来看,AOP也是对面对对象编程的补充,因为在面对对象编程中为了提高代码的复用性,往往将不同类中重复的功能写在一个独立的类里,然后再在调用这个类。但是这样还是一来类之间还是有一定耦合度,尤其是针对一些老系统想给每个模块新增日志功能很麻烦,这时候就可以用到AOP,把业务方法作为切入点,把打印日志的代码作为增强模块植入进去。

在Spring AOP中业务逻辑仅仅只关注业务本身,将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

IOC

控制反转,指将对象的控制权转移给Spring框架,由 Spring 来负责控制对象的生命周期(比如创建、销毁)和对象间的依赖关系。

最直观的表达就是,以前创建对象的时机和主动权都是由自己把控的,如果在一个对象中使用另外的对象,就必须主动通过new指令去创建依赖对象,使用完后还需要销毁(比如Connection等),对象始终会和其他接口或类耦合起来。而 IOC 则是由专门的容器来帮忙创建对象,将所有的类都在 Spring 容器中登记,当需要某个对象时,不再需要自己主动去 new 了,只需告诉 Spring 容器,然后 Spring 就会在系统运行到适当的时机,把你想要的对象主动给你。也就是说,对于某个具体的对象而言,以前是由自己控制它所引用对象的生命周期,而在IOC中,所有的对象都被 Spring 控制,控制对象生命周期的不再是引用它的对象,而是Spring容器,由 Spring 容器帮我们创建、查找及注入依赖对象,而引用对象只是被动的接受依赖对象,所以这叫控制反转。

Beam

Springboot

Spring Boot 的设计目的是为了解决 Spring 各版本设定工作过于繁重的问题,简化初始架设流程、降低开发难度,使开发人员只需要专注在应用程序的开发,而无须过多关注 XML 的配置。

Spring需要XML文件配置开启一些功能,Spring Boot不用XML配置,只需要写一个配置类。Spring Boot会通过启动器开启自动装配功能以@EnableAutoConfiguration扫描在spring.factories中的配置,然后通过@XxxxautoConfiguration进行扫描和配置所需要的Bean,自动的扫描SpringBoot项目引入的Maven依赖,只有用到的才会被创建成Bean,然后放到IOC容器内。

Springcloud

spring cloud 是一系列框架的有序集合。它利用 spring boot 的开发便利性巧妙地简化了分布式系统基础设施的开发。

微服务:传统模式是所有的代码在同一个工程中 部署在同一个服务器中 同一个项目的不同模块不同功能互相抢占资源。而微服务将工程根据不同的业务规则拆分成,每个业务功能模块作为独立项目开发,称为一个服务。微服务部署在不同的机器上,服务之间进行相互调用。Java微服务的框架有 dubbo,spring cloud

Mybatis

MyBatis 是持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息。

结构

  1. API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

  2. 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

  3. 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

流程

  1. 加载配置:配置来源于配置文件和Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括传入参数映射配置、执行的SQL语句、结果映射配置)存储在内存中。

  2. SQL解析:当API接口层接收到调用请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据SQL的ID找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL语句和参数。

  3. SQL执行:将最终得到的SQL和参数拿到数据库进行执行,得到操作数据库的结果。

  4. 结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。

优点

  1. 简单:本身就很小且简单。没有任何第三方依赖,最简单安装只要两个jar文件+配置几个sql映射文件易于学习,易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现.

  2. 灵活:mybatis不会对应用程序或者数据库的现有设计强加任何影响。 sql写在xml里,便于统一管理和优化。通过sql基本上可以实现我们不使用数据访问框架可以实现的所有功能,或许更多.

  3. 解除sql与程序代码的耦合:将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql和代码的分离,提高了可维护性.

$ 和 #

  #

  • # 符号会对参数进行预编译处理,生成 SQL 语句中的占位符,然后使用预编译语句的方式执行 SQL,可以防止 SQL 注入攻击。

  • 例如:SELECT * FROM table WHERE column = #{value}#{value} 将会被替换成预编译语句中的占位符。

$

  • $ 符号直接将参数的值拼接到 SQL 语句中,不进行预编译处理,相当于字符串替换。这样做的好处是可以将 SQL 语句中的表名、列名等部分作为参数进行替换,但也存在 SQL 注入的风险。

  • 例如:SELECT * FROM ${tableName}${tableName} 将会被替换成实际的表名。

Mybatis-plus

Mybatis-Plus在 MyBatis 的基础上只做增强不做改变

便捷的操作:MyBatis Plus封装了很多常用的数据库操作方法,如增删改查,使得开发者能够更快速地完成基本的数据库操作,减少了重复劳动。

自动生成SQL语句:MyBatis Plus提供了代码生成器,可以根据实体类自动生成对应的SQL语句,简化了SQL语句的编写工作。

条件构造器和分页功能:MyBatis Plus提供了条件构造器和分页插件,使得查询条件的构建和分页操作更加便捷。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值