JVM八股

JVM虚拟机

创建类的方法

  • new关键字,可以去调用无参构造器或者有参构造器

  • 克隆一个对象:使用object的clone方法

  • 反序列化,使用对象流ObjectInputStream的readObject方法序列化对象

  • 在class类中有一个newInstance方法,该方法会调用类中公开的无参构造方法

  • 使用反射机制:
    (1)用类派生一个对象:类.class.newInstance()
    (2)动态加载一个对象: 类.class.forName(类全路径).newInstance()
    (3)构造一个对象:类.class.getConstructor().newInstance()

  • 在Constructor类中,也有一个newInstance方法帮助我们来创造对象,通过这个方法我们可以调用有参的构造或私有的构造方法

    1. 两者创建对象的方式不同,前者是实用类的加载机制,后者则是直接创建一个类:
    2. newInstance创建类是这个类必须已经加载过且已经连接,new创建类是则不需要这个类加载
    3. newInstance: 弱类型(GC是回收对象的限制条件很低,容易被回收)、低效率、只能调用无参构造,new 强类型(GC不会自动回收,只有所有的指向对象的引用被移除是才会被回收,若对象生命周期已经结束,但引用没有被移除,经常会出现内存溢出)

静态变量和成员变量 & 静态构造和普通构造函数

静态变量和成员变量

  • 两个变量的生命周期不同:成员变量随着对象的创建而存在,随着对象被回收而释放。静态变量随着类的加载而存在,随着类的消失而消失
  • 调用方式不同:成员变量只能被对象调用。静态变量可以被对象调用,还可以被类名调用。
  • 数据存储位置不同:成员变量存储在堆内存的对象中,所以也叫对象的特有数据静态变量数据存储在方法区(共享数据区)的静态区,所以也叫对象的共享数据
    静态构造函数和普通构造函数的区别:
    1、静态构造函数是在类被加载时被调用,它不能访问类的非静态成员,而且只能调用静态方法:而普通构造函数是在类实例化时被调用,它可以访问类的非静态成员,也可以调用静态方法。
    2、静态构造函数不能被继承,而普通构造函数可以被继承,

Java类生命周期

Java类从被虚拟机加载开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段;其中验证、准备和解析又统称为连接(Linking)阶段。
加载:当程序需要使用某个类时,Java虚拟机会通过类加载器将该类的字节码文件加载到内存中,并为之创建一个java.lang.Class对象,用于表示该类在内存中的信息。

验证:Java虚拟机对加载的类进行格式和语义的检查,以确保其符合Java语言规范和字节码规范,不会危害到虚拟机的安全性。

准备:Java虚拟机为类中的静态变量分配内存,并设置默认初始值,例如int类型的变量默认为0,引用类型的变量默认为null。

解析:Java虚拟机将类中的符号引用(如类名、方法名、字段名等)解析为直接引用(如内存地址、偏移量等),以便于后续的访问和调用。

初始化:Java虚拟机执行类中的静态初始化代码块和静态变量的显式赋值语句,以初始化类的状态。这一步是在第一次使用该类时触发的,也称为类的主动引用。有以下几种情况会导致类的主动引用2:

创建类的实例对象,即使用new关键字或反射机制。
调用类的静态方法或访问类的静态变量。
使用子类引用父类的静态变量或静态方法。
使用反射机制对类进行操作。
初始化一个类的时候,如果发现其父类还没有初始化,则先触发其父类的初始化。
当虚拟机启动时,用户指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。
使用:当类被初始化后,就可以正常地使用该类创建对象、调用方法、访问字段等。

卸载:当一个类不再被任何引用指向,并且其所在的类加载器也被回收时,该类就会被卸载,释放其占用的内存空间。一般情况下,由系统自带的根加载器加载的类不会被卸载,而由用户自定义的类加载器加载的类可能会被卸载。

在这里插入图片描述

多态实现方法

多态提高了代码的通用性和扩展性,基于继承关系,父类引用可以调用不同子类的功能,只需要维护父类的代码。

  • 接口实现
  • 继承父类进行方法重写(运行时多态),执行期间通过判断所引用对象的类型,实现向上转型;
  • 同一个类中方法重载(编译时多态),通过传入不同的参数得到不同结果;

java修饰符

非访问修饰符:用来修饰类、成员变量和成员方法的其他特性。有六种非访问修饰符,分别是static、final、abstract、synchronized、transient和volatile。
static可以用来修饰类、变量和方法,表示它们属于类而不属于对象,可以直接通过类名调用;
final表示可以用来修饰类、变量和方法,表示它们不能被修改或重写;
abstract可以用来修饰类和方法,表示它们没有具体的实现,需要被子类或实现类实现或重写;
synchronized表示同步的,可以用来修饰方法或代码块,表示它们只能被一个线程执行,用于实现多线程的同步;
transient可以用来修饰变量,表示它们不会被序列化或反序列化;
volatile可以用来修饰变量,表示它们在多线程中是可见的,不会被缓存

接口和抽象类

共同点:都不能被实例化。都可以包含抽象方法。都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
区别:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。一个类只能继承一个类,但是可以实现多个接口。接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

object常用方法

public final native Class<?> getClass()
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
//返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
public String toString()
//唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notify()
public final native void notifyAll()
//暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
public final native void wait(long timeout) throws InterruptedException

//多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上
public final void wait(long timeout, int nanos) throws InterruptedException
//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
public final void wait() throws InterruptedException
//实例被垃圾回收器回收的时候触发的操作
protected void finalize() throws Throwable { }

JVM五大内存区域

在这里插入图片描述

程序计数器

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。它记录虚拟机字节码指令的地址,如果为native方法,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

Java栈(虚拟机栈)

栈描述的是Java方法执行的内存模型。每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。平时说的栈一般指局部变量表部分。局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

Java虚拟机栈可能出现两种类型的异常:

线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常

本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是 虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

堆是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代。

根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。

当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

方法区用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

在老版jdk,方法区也被称为永久代。

不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。

java类加载器

在这里插入图片描述

启动类加载器(引导类加载器)

类加载器使用C/C++语言实现,嵌套在JVM内部。用来加载Java核心类库。并不继承于java.lang.ClassLoader没有父加载器。负责加载扩展类加载器和应用类加载器,并为它们指定父类加载器。出于安全考虑,引用类加载器只加载器包名为java,javax,sun等开头的类。

扩展类加载器

Java的扩展类加载器是系统提供的类加载器中的一种,它由 sun.misc.Launcher$ExtClassLoader 类实现,继承自 URLClassLoader。它主要加载扩展目录下的jar包,也就是 %JAVA_HOME%/lib/ext 目录下或者由系统变量 -Djava.ext.dirs 指定的路径中的类库。它可以加载一些系统类库的扩展类,比如 DNS、加密、ZIP等。它的父类加载器是引导类加载器。

应用程序类加载器

应用程序类加载器是系统提供的类加载器中的一种,它由 sun.misc.Launcher$AppClassLoader 类实现,继承自 URLClassLoader。它主要加载应用程序classpath路径下的所有jar和class文件,也就是我们编写的代码以及使用的第三方库。它是 Java 程序中的最高层次的类加载器,可以通过 ClassLoader.getSystemClassLoader () 方法获取到。它的父类加载器是扩展类加载器。

自定义类加载器

自定义类加载器是指程序中由开发人员自定义的一类加载器,它们继承自抽象类ClassLoader,重写它的findClass方法或loadClass方法。自定义类加载器的目的是为了加载一些系统类加载器无法加载的类,或者对类文件进行加密和解密等特殊处理。自定义类加载器也要遵循双亲委派机制,即在加载一个类时,先委托给父类加载器尝试加载,如果父类加载器无法加载该类,才由自己去加载。

JVM类加载机制

全盘负责

所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入。

双亲委派

所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

缓存机制

缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

双亲委派机制

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器区执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父加载器无法完成此加载任务,子加载器才会尝试自己去加载,如果均加载失败,就会抛出ClassNotFoundException异常,这就是双亲委派模式。即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了了时,儿子自己才想办法去完成。

在这里插入图片描述

优点

  1. 安全,可避免用户自己编写的类动态替换Java的核心类,如java.lang.String。java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
  2. 避免全限定命名的类重复加载(使用了findLoadClass()判断当前类是否已加载)。Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

沙箱安全机制

防止恶意代码污染java源代码。比如我们定义了一个类名为String所在包也命名为java.lang,因为这个类本来属于jdk的,如果没有沙箱安全机制,这个类将会污染到系统中的String,但是由于沙箱安全机制,所以就委托顶层的引导类加载器查找这个类,如果没有的话就委托给扩展类加载器,再没有就委托到系统类加载器。但是由于String就是jdk源代码,所以在引导类加载器那里就加载到了,先找到先使用,所以就使用引导类加载器里面的String,后面的一概不能使用,这就保证了不被恶意代码污染。

JVM类装载方式

隐式装载

程序在运行过程中当碰到通过new等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。

显式装载

通过class.forName()等方法,显式加载需要的类。

JVM垃圾回收

判断是否可以回收

引用计数法

在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。

主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。发生循环引用的对象的引用计数永远不会为0,结果这些对象就永远不会被释放。

可达性分析算法

从GC Roots 为起点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

Java 中,GC Roots 是指:

  1. Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 本地方法栈中引用的对象
  3. 方法区中常量引用的对象
  4. 方法区中类静态属性引用的对象

四种引用类型

强引用(Strong Reference)
MyClass obj = new MyClass(); // 强引用``obj = null // 此时‘obj’引用被设为null了,前面创建的'MyClass'对象就可以被回收了

只要强引用存在,垃圾收集器永远不会回收被引用的对象,只有当引用被设为null的时候,对象才会被回收。但是,如果我们错误地保持了强引用,比如:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,会产生内存泄漏。

软引用(Soft Reference)

软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。

软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

Object o1 = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue);
弱引用(Weak Reference)

弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

WeakReference<MyClass> weakReference = new WeakReference<>(new MyClass());
幻象引用/虚引用(Phantom References)

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

垃圾回收算法

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

该方法简单快速,但是缺点也很明显,一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。后续的收集算法都是基于这种思路并对其不足进行改进而得到的。

复制(Copying)算法

复制算法改进了标记-清除算法的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点也是明显的,可用内存缩小到了原先的一半。

标记-整理算法

复制算法主要用于回收新生代的对象,但是这个算法并不适用于老年代。因为老年代的对象存活率都较高(毕竟大多数都是经历了一次次GC千辛万苦熬过来的,身子骨很硬朗 )

根据老年代的特点,提出了另外一种标记-整理(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

分代收集算法

根据对象存活周期的不同,将内存分块。在Java 堆中,内存区域被分为了新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

就如我们在介绍上面的算法时描述的,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记—清除” 或者 “标记—整理” 算法 来进行回收。
如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

垃圾收集器

  1. Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  2. ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  3. Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  4. Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  5. Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
    CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  6. G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

工作机制

新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

  • 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To Survivor区”,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中。

清空 Eden 和 From Survivor 分区;

这时From Survivor 和 To Survivor 分区会互换角色,分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。

老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。

Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。
大对象直接进入老年代:

新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。

所谓大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来 “安置” 它们。
虚拟机提供了一个XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用的是复制算法)。

长期存活对象将进入老年代:

虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每过一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。

JVM 1.8及以前版本中,永久代是堆的一部分,它的垃圾回收是由Full GC来完成的。在JVM 1.8之后,永久代被移除了,取而代之的是元空间(Metaspace),它是使用本地内存来存储类的元数据信息。元空间的垃圾回收是由JVM自动完成的。

如果您使用的是JVM 1.8及以前版本,那么永久代是有垃圾回收的,但是它是由Full GC来完成的。如果您使用的是JVM 1.8及以后版本,那么永久代被移除了,取而代之的是元空间(Metaspace),它是使用本地内存来存储类的元数据信息。元空间的垃圾回收是由JVM自动完成的。

Full GC是指完全垃圾回收,它会回收整个Java堆,包括年轻代和老年代。当永久代满了或者是超过了临界值,会触发完全垃圾回收。查看垃圾收集器的输出信息,就会发现永久代也是被回收的。

Minor GC和Major GC

Minor GC 是指针对新生代(Young Generation)的垃圾回收,它使用复制算法,将存活的对象从 Eden 区和一个 Survivor 区复制到另一个 Survivor 区,然后清空原来的区域。Minor GC 通常发生在 Eden 区满了或者无法容纳新对象时。Minor GC 的速度较快,但是频繁发生。

Major GC 是指针对老年代(Old Generation)的垃圾回收,它使用标记-清除或者标记-整理算法,将不可达的对象清理掉,然后整理剩余的对象。Major GC 通常发生在老年代满了或者无法分配担保空间时。Major GC 的速度较慢,但是不太频繁发生。

方法区与原空间

方法区是JVM规范定义的一个内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、动态生成的类等数据。方法区是所有线程共享的,它的大小可以固定或者可扩展。方法区并不是一个具体的实现,不同的JVM可以有不同的实现方式。

元空间是HotSpot虚拟机在Java8版本以后用来实现方法区的一种技术。在Java8之前,HotSpot虚拟机用永久代来实现方法区,但是永久代有一些缺点,比如空间大小难以确定,容易出现内存溢出,垃圾回收效果不佳等。因此,在Java8中,永久代被移除,取而代之的是元空间。

元空间和永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。这样就可以避免永久代中常见的内存溢出错误,并且可以利用本地内存管理工具进行监控和调优。元空间的大小默认是无限的,但是也可以通过参数来限制它的使用。

总之,方法区是一个抽象的概念,元空间是一种具体的实现。元空间是HotSpot虚拟机在Java8中对方法区进行了优化和改进。

jvm虚拟机内存泄漏

JVM虚拟机内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。随着垃圾回收器活动的增加以及内存占用的不断增加,程序性能会逐渐表现出来下降,极端情况下,会引发OutOfMemoryError导致程序崩溃。

JVM虚拟机内存泄漏的原因有很多,比如:

  • 代码中没有及时释放对象,导致内存无法回收。
  • 资源未关闭造成的内存泄漏,如数据库连接、网络连接和IO连接等。
  • 全局缓存持有的对象不使用的时候没有及时移除,导致一直在内存中无法移除。
  • 静态集合类如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。
  • 堆外内存无法回收,如使用ByteBuffer申请的直接内存等。

JVM虚拟机内存泄漏的解决办法有以下几种:

  • 尽量减少使用静态变量,或者使用完及时赋值为null。
  • 明确内存对象的有效作用域,尽量缩小对象的作用域,能用局部变量处理的不用成员变量,因为局部变量弹栈会自动回收。
  • 减少长生命周期的对象持有短生命周期的引用。
  • 使用StringBuilder和StringBuffer进行字符串连接,避免产生大量临时字符串。
  • 对于不需要使用的对象手动设置null值,标记为可被清理的对象。
  • 各种连接(数据库连接,网络连接,IO连接)操作,务必显示调用close关闭。

JVM虚拟机内存泄漏的排查方法有以下几种:

  • 查看进程内存信息,如使用pmap命令查看进程的内存映像信息。
  • 分析GC是否正常执行,如使用jstat命令查看GC统计信息。
  • 确认代码改动和逻辑问题,尽快从代码上找出问题。
  • 导出堆内存快照和直接内存快照,如使用jmap命令导出堆内存日志文件,并使用MAT或JVisualVM工具分析日志文件。

具体流程:

  1. 首先,我们运行MemoryLeakExample类,并打开jvisualvm工具。
    然后,我们在jvisualvm中选择MemoryLeakExample进程,并点击监视(Monitor)选项卡。
  2. 接着,我们可以看到堆内存(Heap)的使用情况图表,其中红色部分表示已使用的内存,绿色部分表示可用的内存。
    接下来,我们点击执行垃圾回收(Perform GC)按钮,触发一次垃圾回收操作。
  3. 最后,我们可以看到堆内存图表中红色部分没有明显减少,说明有很多对象没有被回收,存在内存泄漏的可能。
  4. 为了进一步确认和定位内存泄漏的问题,我们在jvisualvm中选择MemoryLeakExample进程,并点击堆转储(Heap Dump)按钮,生成一份堆转储文件。
  5. 然后,我们在jvisualvm中打开堆转储文件,并点击类(Classes)选项卡。
  6. 接着,我们可以看到所有类及其占用的内存大小和对象数量的列表。
    接下来,我们可以根据列表中的数据排序或过滤,找出占用内存最大或对象数量最多的类。
  7. 最后,我们可以看到MemoryLeakExample类占用了约3.8MB的内存,并且有100000个对象实例。这就证明了MemoryLeakExample类存在内存泄漏的问题。

符号引用和直接引用

Java的符号引用和直接引用是两种不同的引用方式,它们主要涉及到类加载和方法调用的过程。

符号引用是一种只包含语义信息,不涉及具体实现的引用。例如,当我们在代码中调用一个方法时,我们只需要知道这个方法的名称、参数和返回值,而不需要知道这个方法在内存中的具体位置。符号引用通常存储在类文件的常量池中,它们可以是类名、字段名、方法名等。

直接引用是一种与具体实现息息相关的引用。例如,当我们在运行时调用一个方法时,我们需要知道这个方法在内存中的起始地址,以便正确地执行它。直接引用通常是指针、偏移量或者句柄等形式。

符号引用和直接引用之间的转换发生在类加载的解析阶段。解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。这个过程需要虚拟机检查符号引用所代表的类、字段或者方法是否已经被加载、链接和初始化,如果没有,则先执行相应的操作,然后根据实际内存布局分配给符号引用一个可靠的直接引用。

符号引用和直接引用的区别主要体现在两个方面:一是灵活性,符号引用可以在不同的环境中被解析为不同的直接引用,从而实现动态绑定;二是安全性,符号引用可以通过双亲委派模式和沙箱机制保证Java核心API不被恶意篡改或替换。

Java异常

在这里插入图片描述

从类来看Throwable:是java中所有异常和错误的超类,其两个子类为 Error(错误) 和 Exception(异常)

  1. Error: 是程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时JVM出现问题。通常有Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如说当jvm耗完可用内存时,将出现OutOfMemoryError。此类错误发生时,JVM将终止线程。非代码性错误。因此,当此类错误发生时,应用不应该去处理此类错误。
  2. Exception: 是程序本身可以捕获并且可以处理的异常。其中可分为运行时异常(RuntimeException)和非运行时异常,也叫做受检异常
    • 运行时异常(不受检异常): RuntimeException类极其子类表示JVM在运行期间可能出现的错误。编译器不会检查此类异常,并且不要求处理异常,比如用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
    • 非运行时异常(受检异常): Exception中除RuntimeException极其子类之外的异常。编译器会检查此类异常,如果程序中出现此类异常,比如说IOException,必须对该异常进行处理,要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。

从程序执行的过程来看编译时异常和运行时异常

  1. 编译时异常:程序在编译时发生的异常(javac 源文件名.java)
  2. 运行时异常: 程序在运行时发生的异常(java 字节码文件名)

异常处理

  • try-catch-finally
  • throws + 异常类型

常见的异常

2.1 运行时异常

NullPointerException (空指针异常):指针指向的对象为空(null)

ArrayIndexOutOfBoundException (数组角标越界异常) StringIndexOutOfBoundException (字符串越界异常)

ClassCastException (类型转换异常)

2.2 编译时异常(编译时异常必须进行处理否则无法进行)
  1. IOException(FileNotFoundException):当发生某种IO异常时;
  2. ClassNotFoundException:当程序运行时尝试使用类加载器加载class时,没有在classpath中查找到指定类;

Java泛型

泛型的本质就是把类型参数化,所操作的数据类型被指定为参数,根据动态传入进行处理。泛型里面数据类型不能是基本类型,因为虚拟机在编译时会把带泛型的转换成Object类型,而基本类型不属于Object类型,所以泛型里面数据类型不能是基本类型。

泛型的好处

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

泛型的使用方式有哪几种?

  • 泛型类
  • 泛型接口
  • 泛型方法

泛型通配符

  1. 无边界的通配符(Unbounded Wildcards), 就是, 比如List
    无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.
  2. 固定上边界的通配符(Upper Bounded Wildcards),采用<? extends E>的形式
    使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。
    要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界。
    注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类
  3. 固定下边界的通配符(Lower Bounded Wildcards),采用<? super E>的形式
    使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据.。
    要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界.。
    注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界。

常用泛型参数

E: Element (在集合中使用,因为集合中存放的是元素)
T:Type(Java 类)
K: Key(键)
V: Value(值)
N: Number(数值类型)
?: 表示不确定的java类型

泛型擦除

泛型擦除是Java泛型的一种实现方式,它是指在编译时期,Java编译器会将泛型类型参数擦除为它的上界类型,也就是最接近的父类类型 例如,List 和 List 在编译后都会变成 List ,因为 Object 是 String 和 Integer 的共同父类。这样做的目的是为了保证 Java 泛型的向后兼容性,让使用泛型的代码能够和不使用泛型的代码相互调用。

泛型擦除也带来了一些问题,比如类型信息的丢失,导致运行时无法获取泛型的真实类型;或者多态性的破坏,导致子类重写父类的泛型方法时发生方法签名不匹配的问题。为了解决这些问题,Java 编译器会在必要的地方插入强制类型转换或者桥接方法,以保证程序的正确运行。

public class Caculate<T> {
private T num;
}
反编译后
public class Caculate{
    private Object num;
	public Caculate(){}
}

发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型。

springboot

分层,五层

    View层:视图根据接收到的数据最终展示页面给用户浏览。与Controller层结合比较紧密,需要二者结合起来协同工作。

  Controller层:负责具体的业务模块流程的控制,响应用户的请求,调用Service层的接口来控制业务流程,决定使用何种视图并准备响应数据。并把接收到的参数传给Mapper,调用Mapper的方法接口。

    Service层:主要负责业务模块的逻辑应用设计,同时有一些是关于数据库处理的操作,但是不是直接和底层数据库关联,而是首先设计接口,再设计其实现的类,在接口实现方法中需要导入Mapper层,接着再Spring的配置文件中配置其实现的关联。这样就可以在应用中调用Service接口来进行业务处理。

    Mapper层:主要是做数据持久层的工作,同时提供增删改查(CRUD)工作,Mapper层一样也是先设计接口,而具体实现在mapper.xml 文件里,然后就可在模块中调用此接口来进行数据业务的处理,而不用关心此接口的具体实现类是哪个类,显得结构非常清晰。

    Model层:存放了页面需要传递数据对应字段的实体类,它和数据库中对应表字段的属性值保持一致,实现该类的set/get方法。

注解

@Autowired:默认按类型进行自动装配。在容器查找匹配的Bean,当有且仅有一个匹配的Bean时,Spring将其注入@Autowired标注的变量中。
注入方式:@Autowired默认按照byType 注入,也提供byName;@Resource默认按byName自动注入,也提供按照byType 注入;
byType是通过类型进行装配,byName是通过名称进行装配。

启动注解 @SpringBootApplication
@Controller 控制器,处理http请求。
@RequestBody 通过HttpMessageConverter读取Request Body并反序列化为Object(泛指)对象
@RequestMapping 是 Spring Web 应用程序中最常被用到的注解之一。这个注解会将 HTTP 请求映射到 MVC 和 REST 控制器的处理方法上

IOC

控制反转,简单点说,就是创建对象的控制权,被反转到了Spring框架上。

通常,我们实例化一个对象时,都是使用类的构造方法来new一个对象,这个过程是由我们自己来控制的,而控制反转就把new对象的工交给了Spring容器。

传统的开发方式 :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来

使用 IoC 思想的开发方式 :不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面过去即可。
从以上两种开发方式的对比来看:我们 “丧失了一个权力” (创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情)

好处

IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。对象之间的耦合度或者说依赖程度降低;

资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。

AoP

切 :指的是横切逻辑,原有业务逻辑代码不动,只能操作横切逻辑代码,所以面向横切逻辑

面 :横切逻辑代码往往要影响的是很多个方法,每个方法如同一个点,多个点构成一个面。这里有一个面的概念

AOP 主要用来解决:在不改变原有业务逻辑的情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。

同步调用和异步调用

同步调用和异步调用是两种不同的消息通信机制:

  • 同步调用是指调用方在发出调用请求后,必须等待被调用方返回结果,才能继续执行后续的操作¹⁴。同步调用的优点是逻辑简单,缺点是可能造成线程阻塞,影响程序效率。
  • 异步调用是指调用方在发出调用请求后,不必等待被调用方返回结果,而是继续执行后续的操作¹⁴。异步调用的优点是提高了程序效率,缺点是逻辑复杂,需要额外的机制来获取返回结果或处理异常。

Maven生命周期

三个标准生命周期

标准生命周期作用
clean项目清理
default(build)项目部署
site项目站点文档创建

clean生命周期

clean生命周期通过clean插件(自带)完成,功能是删除当前项目的target目录。

mvn clean

执行mvn clean命令,该命令包含以下阶段:

  • pre-clean:执行一些需要在clean之前完成的工作
  • clean:移除所有上一次构建生成的文件
  • post-clean:执行一些需要在clean之后立刻完成的工作

这三个阶段按顺序全部执行完成后,才算完成了clean生命周期。

default(build)生命周期

在这里插入图片描述

核心阶段详解
validate验证项目是否正确,所有必要信息是否可用(很少单独使用)
compile编译项目的源代码(将src/main中的java代码编译成class文件,输出到targe目录下)
test将单元测试的资源文件和代码进行编译,生成的文件位于target/test-classes (打包部署请跳过该阶段)
package把class文件,resources文件打包成jar包(也可以是war包),生成的jar包位于target目录下
verify检查包是否有效(很少单独使用)
install将jar部署到本地仓库,本地的其他模块依赖该jar包时,可以直接从本地仓库去获取
deploy将jar包部署到远端仓库,需要在maven的setting.xml中配置私服的用户名和密码,以及在pom.xml配置

不同于 clean生命周期和site生命周期都是单独的一个阶段,default(build)生命周期里面分为七个大阶段。

这七个大阶段是 顺序执行 的

指定某个生命周期的阶段
比如执行 mvn install,会依次执行validate, compile, test, package, verify,最后执行 install 阶段,将jar包发布到本地仓库。

指定多个不同生命周期的阶段
执行 mvn clean deploy 命令,首先完成的 clean 生命周期,将以前构建的文件清理。

然后再执行 default lifecycle 的 validate, compile, test, package, verify, insstall, deploy 阶段,将 package 阶段创建的jar包发布到远程仓库中。

site生命周期

可以使用 Maven 提供的 maven-site-plugin 插件(该插件不是默认插件,需要引用)让 Maven 生成一个 Web 站点, 以站点的形式发布信息。

pom.xml中添加以下内容

    <build>
        <plugins>
            <!--添加 maven-site-plugin 插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-site-plugin</artifactId>
                <version>3.7.1</version>
            </plugin>
        </plugins>
    </build>

运行命令 mvn site,界面如下:

在这里插入图片描述

可以看到,在target文件夹下,生成了一个site文件夹,这就是站点对应的文档信息。

在这里插入图片描述

打开site文件夹,点击网页,可以看到以下界面

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值