JAVA基础

68 阅读21分钟

JVM类加载机制

JVM的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由jvm的具体实现指定的

jvm组成结构之一就是类装载器子系统,

image.png

类的生命周期 类的生命周期包括:加载、链接、初始化、使用和卸载,其中加载、链接、初始化,属于类加载的过程:

  • 类加载的过程 image.png

image.png 一. 第一步:Loading装载

  1. 通过类的全限定名(包名+类名),获取到该类的.class文件的二进制字节流
  2. 将二进制字节流所代表的静态存储结构,转化为方法去运行时的数据结构
  3. 在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

总结:加载二进制数据到内存->映射成jvm能识别的结构->在内存中生成class文件

二. 第二步:Linking链接

链接是指将上面创建好的class类合并到Java虚拟机中,使之能够执行的过程,可分为验证、准备、解析三个阶段。

验证:

确保class文件中的字节流包含信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。

  • 文件格式验证
    • 验证字节流是否符合Class文件格式的规范,并且能够被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能够正确地解析并存储于方法区之内。这阶段的验证是基于二进制字节流进行的,只有经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面验证都是基于方法区的存储结构进行的。

举例:

  1. 是否以16进制cafebabe开头
  2. 版本号是否正确
  • 元数据验证

    • 对类的元数据信息进行语义校验(其实就是对Java语法校验),保证不存在不符合Java语法规范的元数据信息。
  • 字节码验证

    • 进行数据流和控制流分析,确定程序语义是合法的、符合逻辑的。对类的方法进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机的行为。获取类的二进制字节流的阶段是我们JAVA程序员最关注的阶段,也是操控性最强的一个阶段。因为这个阶段我们可以对于我们的类加载器进行操作,比如我们的想自定义类加载器进行操作以完成加载,又或者我们想通过JAVA Agent来完成我们的字节码增强操作。

举例: 字节码的验证会相对来说较为复杂。

  1. 运行检查
  2. 栈数据类型和操作码操作参数温和(比如栈空间只有4个字节,但是我们实际需要的远大于4个字节,那么这个时候这个字节码就是有问题的)
  3. 跳转指令指向合理的位置
    • 符号引用验证

    这是最后一个阶段的验证,它发生在虚拟机符号引用转化为直接引用的时候(解析阶段),可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。符号引用验证的目的是确保解析动作能正常执行。

举例:

  1. 常量池中描述的类是否存在
  2. 访问的方法或者字段是否存在具有足够的权限

注意:但是,我们很多情况下可能认为我们的代码肯定是没问题的,验证的过程完全没必要,那么我们可以添加参数 -Xverify:none 取消验证

准备

为类的静态变量分配内存,并将其初始化为默认值

image.png

  • 这里不包含final修饰的static,因为final在变异的时候就会分配了,准备阶段会显式初始化‘
  • 这里不会为实例变量(也就是美甲static)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起被分配到JAVA堆中

image.png

解析

把类中的符号引用转换为直接引用

符号引用就是一组符号来描述目标,可以是任何字面量 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引进

直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定存在内存中

三. 初始化

初始化阶段是执行类构造器方法Class.init()的过程

在准备阶段,类变量已经赋予一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,比如赋值。

在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量是指定初始值
  • 使用静态代码块为类变量指定初始值

按照程序员的逻辑,你必须把静态变量定义在静态代码块的前面,因为两个的执行时会根据代码编写的顺序来决定的,顺序搞错了可能会影响你的业务代码

JVM初始化步骤:

  • 假如这个类还没有被加载和连接,则程序先加载并连接该类
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句

G1 垃圾回收

Root Searching 根可达 (什么是根 一般是main方法里面的对象) image.png

GC Algorithm 三种算法

  • Mark-Sweep(标记清除) 这样会有很多碎片

image.png

  • Copying(拷贝)空间浪费 每次只用一半内存 GC的时候把有用的copy到另一半 然后清空原来那一半的内容

image.png

  • Mark-Compact(标记压缩)把没用的对象清除,并把有用的对象全部挪到前面去
分代管理方法

image.png

当系统创建一个新对象的时候,总是在Eden区操作,当Eden区满了以后,会触发一次young GC,没有被清理掉的对象会被放到survivor1区;下一次Eden满了的时候survivor0和Eden的存活对象会被放到survivor2区,如此反复。。反复若干次(一般是15或6)后还存活的对象就会被移动到tenured(老年代),等tenured也满了的时候就会触发full GC,对整个堆内存进行垃圾收集

一般young GC能清理掉90%的对象,所以Eden survivor1 survivor2的比例是8:1:1

分代算法基础上的垃圾回收器

image.png

  • Serial

a stop-the-world,copying collector which uses a single GC thread

  • Parallel Scavenge

a stop-the-world, copying collector with uses a multiple GC threads

  • CMS
  1. concurrent mark sweep 并发回收
  2. a mostly concurrent, low-pause collector
  3. remark
  4. concurrent sweep
  • 4 phases

initial mark concurrent mark remark concurrent sweep

三色标记算法
  • CMS方案:Incremental Update image.png

CMS的remark阶段必须从头扫一遍

  • G1方案 SATB Snapshot At the Begining

image.png

每当有灰色指向白色消失,就把这件事记录下来,当线程回来的时候首先检查记录,看还有没有别的指向白色的

*如何知道有没有别的对象指向白色对象?: G1: 物理分区 逻辑分代

Region 分区

image.png

新老年比例:

5%-60%

  • 一般不用手工指定
  • 也不要手工指定,因为这是G1预测停顿时间的基准

Minor GC和FUll GC分别发生在什么时候

Minor GC 是新生代GC,指的是发生在新生代的垃圾收集动作。由于Java对象大都是朝生夕死的,所以Minor GC非常平凡,一般回收速度也比较快 Major GC/Full GC 是老年代GC,指的是发生在老年代的GC,出现Major GC一般经常伴有Minor GC,Major GC的速度比Minor GC慢得多

何时发生?

  1. Minor GC发生:当jvm无法为新生的对象分配空间的时候就会发生Minor GC,所以分配对象的频率越高,也就越容易发生Minor gc
  2. Full GC:发生GC有两种情况:
  • 当老生代无法分配内存的时候,会导致Minor GC
  • 当发生Minor GC的时候可能触发Full GC,由于老年代要对年轻代进行担保,由于进行一次垃圾回收之前是无法确定有多少对象存活,因此老年代并不能清楚自己要担保多少空间,因此采取采用动态估算的方法:也就是上一次回收发送时晋升到老年代的对象容量的平均值作为经验值,当这个平均值大于老年代的可用内存时,就会触发Full GC
  • 调用System.gc时,系统建议执行Full GC,但是不必然执行

抽象类和接口的区别

语法层面上的区别:

  1. 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法
  2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
  3. 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
  4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

JAVA多线程

进程和线程

线程 线程是操作系统能够进行运算调度的最小单位,它包含在进程之中,是进程的实际运作单位 进程 进程是程序的基本执行实体

线程池

三步:

  1. 创建线程池

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。

image.png

CachedThreadPool 是没有上限的线程池,虽然说是没有上限,实际上是有上限的,上限为Integer.MAX_VALUE,但是在创建出来这么多线程之前,电脑就会崩溃,所以说没有上限

image.png 2. 提交任务 3. 所有的任务全部执行完毕,关闭线程池

FixedThreadPool是有上限的线程池corePoolSize=MaxiumPoolSize

自定义线程池

七个核心参数:

  1. corePoolSize – the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set
  2. maximumPoolSize – the maximum number of threads to allow in the pool
  3. keepAliveTime – when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
  4. unit – the time unit for the keepAliveTime argument
  5. workQueue – the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.
    • 两种选择:
      • ArrayBlockingQueue
      • LinkedBlockingQueue
  6. handler – the handler to use when execution is blocked because the thread bounds and queue capacities are reached

image.png

  • 当核心线程池满了且任务队列也满了的时候才会创建临时线程,所以线程的执行顺序不一定是线程的创建顺序
  • 当核心线程,队列,空闲线程全都满了的时候,就会触发线程拒绝机制
    • RejectedExecutionHandler(了解一下就行)
      • ThreadPoolExecutor.AbortPolicy(默认策略,丢弃任务并抛出RejectedExecutionException)
      • ThreadPoolExecutor.DiscardPolicy(丢弃任务 但是不抛出异常)
      • ThreadPoolExecutor.DiscardOldestPolicy(抛弃任务中等待最久的任务,然后把当前任务加入队列中)
      • ThreadPoolExecutor.CallerRunsPolicy(调用任务的run()方法绕过线程直接执行)

image.png

任务的拒绝策略是ThreadPoolExecutor类的静态内部类

JAVA反射机制

反射基础

RTTI(Run-Time Type Identification)运行时类型识别。其作用是在运行时识别一个对象的类型和类的信息。主要有两种方式

  1. “传统的”RTTI,它假定我们在编译时已经知道了所有的类型
  2. 另一种是“反射”机制,它允许我们在运行时就发现和使用类的信息

反射就是把JAVA类中的各种成分映射成一个个Java的对象

例如:一个类有:成员变量、方法、构造方法、包等信息,利用反射技术可以对一个类进行解剖,把一个个组成部分映射成一个个对象

Class类

Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。Class类的实例表示java应用运行时的类(class and enum)或接口(interface and annotation)(每个java类运行时都在JVM里表现为一个class对象,可通过类名.class、类型.getClass()、Class.forName("类名")等方法获取class对象)。数组同样也被映射为class对象的一个类,所有具有相同元素类型和维数的数组都共享该Class对象。基本类型boolean,byte,char,short,int,long,float,double和关键字void同样表现为class对象。


public final class Class<T> implements java.io.Serializable, GenericDeclaration, Type, AnnotatedElement { 
private static final int ANNOTATION= 0x00002000;
private static final int ENUM = 0x00004000; 
private static final int SYNTHETIC = 0x00001000; 

private static native void registerNatives(); 
static { registerNatives(); }
/* * Private constructor. Only the Java Virtual Machine creates Class objects. //私有构造器,只有JVM才能调用创建Class对象 * This constructor is not used and prevents the default constructor being * generated. */
private Class(ClassLoader loader) {
// Initialize final field for classLoader. The initialization value of non-null
// prevents future JIT optimizations from assuming this final field is null. classLoader = loader; }


在Java中,java.lang.Class类中的这些私有静态常量表示了类的各种属性和标志,这些标志用于描述类的不同特性。以下是这些常量的含义:

  1. ANNOTATION(0x00002000): - 这个标志表示一个类是一个注解类型。 - 注解类型是一种特殊的类,用于添加元数据和注释到代码中,通常用于描述代码的行为和属性。 - 通过反射,可以使用这个标志来判断一个类是否是注解类型。
  2. ENUM(0x00004000): - 这个标志表示一个类是一个枚举类型。 - 枚举类型是一种特殊的类,它表示一组常量值,通常用于表示一组相关的命名常量。 - 通过反射,可以使用这个标志来判断一个类是否是枚举类型。
  1. SYNTHETIC(0x00001000): - 这个标志表示一个类是合成类(synthetic class)。 - 合成类是由编译器生成的,通常用于支持某些语言特性或优化。 - 合成类通常不是开发人员显式创建的,而是由编译器生成的辅助类。

这些常量可以通过Java的反射机制来访问和使用,以便在运行时了解类的特性和属性。例如,你可以使用反射来检查一个类是否是枚举类型,或者是否带有特定的注解。

可以使用javap -verbose xxxx.class查看class文件内容

  • class二进制文件内容:

image.png

从上面的内容我们可以得到以下几点信息:

  • Class类也是类的一种,与class关键字是不一样的
  • 手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的文件中(字节码文件)
  • 每个通过关键字class表示的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是同一个Class对象。
  • Class类只存在私有构造函数,因此对应Class对象只能由JVM创建和加载
  • Class类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要

类加载

JVM基础-类字节码详解

软件开发规范(SOLID)

  • S(Single Responsibility Principle):单一职责原则

    • 只有一个导致类发生变化的原因
  • O(Open-Closed Principle):开闭原则

    • 软件中的对象(类,模块,函数等)应该对扩展开放对修改封闭
  • L(Liskov Subsitution Principle),里氏替换原则:

    • 在不影响程序正确性的基础上,所有使用基类的地方都能使用其子类的对象来替换。
  • I(Interface Segregation Principle):接口隔离原则

    • 客户端不应该依赖那些它不需要的接口。客户端应该只依赖它实际使用的方法,因为如果一个接口具备了若干个方法,那就意味着它的实现类都要实现所有接口方法,从代码结构上就十分臃肿。
  • D(Dependency Inversion Principle):依赖倒置原则

    • 高层模块不应该依赖底层模块,应该共同依赖抽象
    • 抽象不应该依赖细节,细节应该依赖抽象

这里的抽象是指接口和抽象类,而细节就是实现接口或继承抽象类而产生的类。

如果高层模块依赖底层模块,那么底层模块的改动很有可能影响到高层模块,从而导致高层模块被迫改动,这样依赖让高层模块的重用变得非常困难

继承,关联,组合,聚合的区别

继承

继承是面向对象最显著的一个特性。 继承是从已有的类(父类,父接口)中派生出新的类(子类、子接口),新的类能吸收已有类的属性和行为,并能扩展新的能力。在Java中此类关系通过关键字extends明确标识。

image.png

实现

实现是类和接口之间最常见的关系。指的是一个类实现接口的功能(一个类可以实现多个接口)。在Java中此类关系通过关键字implements明确标识。 image.png

依赖

依赖关系是指一个类对另一个类的依赖。这种关系是一种非常弱、临时性的关键。依赖关系在Java语言中体现为局域变量、方法的形参,或者对静态方法的调用。

(比如Employee类中有一个方法叫做TakeMoney(Bank bank)这个方法,在这个方法的参数中用到了Bank这个类。那么这个时候可以说Employee类依赖了Bank这个类,如果Bank这个类发生了变化那么会对Employee这个类造成影响)

image.png

关联 关联关系是类与类之间的联接,它使一个类知道另一个类的属性和方法。关联可以是双向的,也可以是单向的,它是依赖关系更强的一种关系。

在Java语言中,关联关系一般表现为被关联类B以类属性的形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量

image.png

聚合

聚合是一种特殊的关联关系,它是较强的一种关联关系,强调的是整体与部分之间的关系,从语法上是没办法区分的,只能从语义上区分

如雁群和大雁的关系、学校和学生的关系

聚合的整体和部分之间在生命周期上没什么必然的联系,部分对象可以在整体对象创建之前创建,也可以在整体对象销毁之后销毁。

image.png

组合

组合也是关联关系的一种特例,这种关系比聚合更强。它强调了整体与部分的生命周期是一致的,而聚合的整体和部分之间在生命周期上没什么必然的联系。

在组合关系中,整体和部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束。

例如大雁和大雁的翅膀是组合关系。一般用带实心菱形(整体的一端)的实现来表示。

image.png

总结:

对于继承、实现这两种关系比较简单,他们体现的是一种类与类、或者类与接口间的纵向关系;其他的四者关系则体现的是类与类、或者类与接口间的引用、横向关系。总的来说这几种关系所表现的强弱程度依次为: 组合>聚合>关联>依赖

spring,springboot,springcloud的区别

  • spring:spring是一个支持快速开发Java EE应用程序的框架。它提供了一系列底层容器和基础设施,并可以和大量常用的开源框架无缝集成,可以说是开发Java EE应用程序的必备。

  • springboot:是一个快速开发框架,它简化了传统的MVC的XML配置,使配置变得更加方便、简洁。

  • springcloud:是建立在springboot上的服务框架,进一步简化了配置,它整合了一全套简单、便捷且通俗易用的框架。

Synchronize和ReentrantLock区别

  • 相似点

    • 这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说如果当一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态和内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)
  • 区别

    • API层面
      • 这两种方式最大区别就是对于Syncronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句来完成。
      • syncronized既可以修饰方法,也可以修饰代码块
    • 等待可中断
      • 等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可等待特性对处理执行时间非常长的同步块很有帮助。
      • 具体来说,加入业务代码中有两个线程,Thread1 Thread2.假设Thread1 获取了对象object的锁,Thread2将等待Thread1释放object的锁

1.使用synchronized。如果Thread1不释放,Thread2将一直等待,不能被中断。synchronized也可以说是Java提供的原子性内置锁机制。内部锁扮演了互斥锁(mutual exclusion lock,mutex)的角色,一个线程引用锁的时候,别的线程阻塞等待。

  1. 使用ReentrantLock。如果Thread1不释放,Thread2等待了很长的时间以后可以中断等待,转而去做别的事情。
- 公平锁
    - 公平锁是指多个线程在等待同一个锁时,必须按照申请的时间顺序来依次获得锁;而非公平锁则不能保证这一点。非公平锁在锁被释放时,任何一个等待锁的线程都有机会获得锁。
    - synchronized的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,单可以通过带布尔值的构造函数要求使用公平锁。

ReentrantLock如何实现公平锁和非公平锁

image.png

非公平锁和公平锁的两处不同:  

  1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。

  2. 非公平锁在CAS失败后,和公平锁一样都会进入到 tryAcquire 方法,在tryAcquire方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

什么时候选择用ReentrantLock

  • 使用场景:时间锁等待、可中断锁等待、无块结构锁、多个条件变量或者锁投票