Java八股文

问题汇总

Java

  • [Java三大特性和详细聊聊多态](# 详细说一说Java的多态)
  • [static可以修饰什么?静态代码块什么时候执行?执行几次。](# static可以修饰什么?静态代码块什么时候执行?执行几次。)
  • [Java多态的底层原理](# 多态的底层实现(涉及到JVM方法调用))
  • [你觉得Java有什么缺点?](# 你觉得Java有什么缺点?)
  • [线程与进程的区别](# 线程与进程的区别)
  • [原子性、可见性、有序性](# 原子性、可见性、有序性)
  • [wait()和block()区别](# wait()和block()区别)
  • [死锁成因及四个条件,怎样避免死锁](# 死锁成因及四个条件,怎样避免死锁)
  • [线程池原理和参数](# 线程池原理和参数)
  • [Synchronized和Volatile关键字](# Synchronized和Volatile关键字)
  • [乐观锁和悲观锁](# 乐观锁和悲观锁)

Java

Java基础

Java三大特性

封装、继承、多态

分别说说对三大特性的理解

封装:封装就是指利用抽象数据类型将数据和基于数据的操作封装在一起,数据被保护在抽象类型的内部,系统的其他部分只有通过包裹在数据外面的被授权的操作,才能够与这个抽象数据类型交流与交互!

继承:继承实际上是存在于面向对象程序中的两个类之间的关系。当一个类拥有另一个类的所有数据和操作时,就称这两个类之间具有继承关系!

多态:多态是指一个程序中同名的不同方法共存的情况。面向对象的程序中多态的情况有多种,可以通过子类对父类方法的覆盖实现多态,也可以利用重载在同一个类中定义多个同名的不同方法!

详细说一说Java的多态

面向对象的三大特性是封装、继承和多态,从一定意义上讲,封装、继承都是为多态准备的。

多态的定义:允许不同类对象对同一操作做出不同的响应。

实现多态的技术:动态绑定(Dynamic Binding),指在运行期间判断所引用对象实际类型,根据其实际的类型调用其相应的方法

多态的作用:消除类型之间的耦合关系

多态的好处:可替换性、可扩充性、接口性、灵活性、简化性 1. 可替换性(substitutability)。多态对已存在代码具有可替换性。
例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
2. 可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
3. 接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。
4. 灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
5. 简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。
多态的底层实现(涉及到JVM方法调用)

多态的底层实现是动态绑定,即在运行时才把方法调用方法实现关联起来。

invokevirtual 和 invokeinterface 用于动态绑定。可以看出,动态绑定主要应用于虚方法接口方法

从JVM的角度来看Java多态的底层原理

Java 多态的实现机制 - 掘金

Tips:

JVM 的方法调用指令有五个:

  • 静态绑定

    • invokestatic:调用静态方法
    • invokespecial:调用实例构造器方法私有方法父类方法
  • 动态绑定

    • invokevirtual:调用虚方法
    • invokeinterface:调用接口方法运行时确定具体实现;
    • invokedynamic:运行时动态解析所引用的方法,然后再执行,用于支持动态类型语言

Java是用过方法表来实现的,C++是通过虚表来实现的。

核心思路:方法表中排列顺序为,Object类方法,父类方法,子类方法。无论子类有无重写父类方法,都视为父类方法,因此被重写和没有被重写的两种情况下,父类方法在方法表中的偏移量是固定的。当方法被重写时,方法表中记录的方法引用则只想子类方法区的重写代码,否则指向原父类方法区的代码。

static可以修饰什么?静态代码块什么时候执行?执行几次。

static可以修饰成员变量方法静待代码块内部类

静态代码块在类被加载的时候就运行,且只运行一次。

优先级:静态代码块>构造代码块>构造函数>普通代码块 

面向过程和面向对象的区别和性能对比

  • 面向过程 :分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
  • 面向对象 :把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
性能对比
1
2
1. 面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低。
2. 面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。

Java集合

ArrayQueue与LinkedListQueue的取舍

ArrayQueue由循环数组实现,容量有限,但实现效率高。

LinkedListQueue由链表实现,理论上容量无限,但实现效率较低。

HashMap底层实现

Java集合系列之四:HashMap底层原理- SegmentFault 思否

ArrayList底层实现

你觉得Java有什么缺点?

  • 使用大量的内存。 靠虚拟机运行,运行速度相对较慢。
  • 不能和底层打交道,不支持底层操作。
  • 启动时间慢。
  • 因为Java删除了指针,所以不如C/C++等语言灵活。

多线程

线程、进程、协程

进程是相对于操作系统而言,每个运行一个应用程序就是运行一个进程。

线程是相对于进程而言,一个进程可以有多个线程,至少有一个线程。

进程依托于操作系统,可以理解为操作系统的子项目,多个进程可以在一个操作系统下运行。

线程依托于进程,可以理解为一个进程的子项目,一个进程可以同时运行多个线程完成特定任务。

协程是更加轻量级的实现,在线程内部,可以实现不同代码的交替执行(函数间的并发)。

  • 解释:一般的函数执行都是通过函数调用栈顺序执行。协程间可以交替运行。也拥有自己的寄存器和栈,被切换时,将寄存器和栈保存起来,恢复时继续运行。

与线程与进程的对比:

  • 协程既可以由线程拥有,也可以由进程独立拥有
  • 协程是非抢占性的

并发与并行

并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在CPU上运行。

并行:当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。

实现线程安全的几种基本方法

  • synchronized
  • volatile
  • 无状态设计
  • ThreadLocal

原子性、可见性、有序性

原子性:一个操作是不可中断的,要么全部执行成功要么全部执行失败

有序性:对应指令重排,一串代码由于某种需求只能顺序执行,要禁止指令重排。

可见性:对应JVM内存模型中线程的局部内存,当一个线程对共享变量作出修改时,其它线程能立即获取到修改。

Java内存模型中定义了8中操作都是原子的,不可再分的。

lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;

unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用;

load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本

use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;

assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;

store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用;

write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

waiting和blocked区别

java线程中含有waiting与blocked两种状态

  • WAITING: 当线程调用调用Object.wait()时,线程,进入waiting状态进入等待队列 。等待其它线程同一对象调用notify()notifyAll()
  • BLOCKED: 出现到notify()notifyAll()的调用时,线程解除waiting状态。但同一时间可能有其他线程竞争对象监视器,因此可能无法立即运行,因此会进入同步队列,准备竞争对象监视器,此时线程处于blocked状态。

Tips: 什么是对象监视器?

锁和监视器之间的区别 – Java并发

死锁成因及四个条件,怎样避免死锁

死锁是什么?如何避免死锁?

什么是死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

死锁产生的原因
  1. 因为系统资源不足。
  2. 进程运行推进的顺序不合适。
  3. 资源分配不当等。

如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

死锁的四个必要条件
  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
怎么避免死锁

教科书回答:哲学家就餐问题解法:服务生解法、资源分级解法、Chandy/Misra解法。

避免死锁就是破坏造成死锁的若干条件中的任意一个

线程池原理和参数

Synchronized和Volatile关键字

Synchronized关键字

通过指定关联一个对象(关联的对象具体分类),利用monitor机制(说说monitor机制),实现同一时刻只有一个线程对所辖区域(临界区)的对象、方法、代码块等的访问和执行。

底层实现原理是在相关字节码两端添加monitorenter和monitorexit。

每个线程执行到monitorenter时,会检查相关monitor对象的计数器是否为0,

  • 不为0等待,转换为阻塞状态。
  • 为0,当前线程成为monitor的拥有者,所著该监视器对象并计数器加1。

每个线程执行到monitorexit时,monitor计数器减1。其它线程竞争monitor。

volatile关键字的作用

40个Java多线程问题总结

两个作用
  • 维护线程可见性:使用volatile关键字修饰的对象在某一线程内部内存中被修改后会被立即同步到主内存。避免线程内部更新之后,其它线程对被更新对象不可见。
  • 禁止指令重排:禁止由于指令重排造成多线程操作同一对象引起的线程不安全。
不具有原子性

由于它只是及时将修改更新到主内存,但不能保证同一时刻只有一个线程对对象进行操作。

一个线程在操作局部内存的变量时,另一个线程也可能同时在操作对应局部内存的相同变量。因此在进行复杂操作时,不能保证原子性。

举例:

i=1操作为,赋值。是原子操作。

i++操作为,取i的值,自增,赋值三个操作。不是原子操作。

可以使用Java多线程的封装包自带的线程安全包实现原子性,如Integer对应的AtomInteger。

说一说Synchronized和Volatile以及区别
共同点

都可以在功能上实现可见性、有序性。

不同点

实现方式不同,只是都能实现功能。

  • Synchronized通过锁住整个代码块,其它相关进程阻塞,执行完成之后将所有修改同步到主内存,实现可见性,其他进程继续竞争监视器,继续执行,实现有序性

  • volatile:一个线程对共享变量发生修改之后立即同步到主内存,并使其它局部内存相关共享变量副本失效,下次别的线程使用改变量时,从主内存同步,实现可见性。禁止指令重排实现有序性

volatile更轻量级,但无法实现原子性。

乐观锁和悲观锁

参考

乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

  • 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
  • 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

解决线程安的四种方案

synchronize
volatile
无状态编程

无状态的方法:方法只包含使用

  • 方法调用传递进来的参数数据
  • 方法内部定义的局部变量
threadlocal

每个线程使用ThreadLocal类包装一个对于共享变量的独立副本,操作只对副本操作。空间换时间。

线程池常用参数

线程池(ThreadPoolExecutor)存储线程的一个”容器”,方便我们从中取线程用,负责托管线程操作。

corePoolSize(核心线程数量)(最小线程数)

workQueue(任务队列)

maximumPoolSize(最大线程数)

RejectedExecutionHandler

keepAliveTime

JVM

JVM 底层原理最全知识总结

JVM内存结构

类的加载过程

img

加载

类加载过程是指从字节码文件中读取二进制数据到内存中准备运行。

字节码文件:一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译

从开发者的角度而言,类的加载器有四种

  • 启动(Bootstrap)类加载器:由C++编写。负责将 Java_Home/lib下面的java核心类库加载到内存中。无法直接调用。

    并不是继承自java.lang.ClassLoader,它没有父类加载器

    它加载扩展类加载器应用程序类加载器,并成为他们的父类加载器

  • 标准扩展(Extension)类加载器:由java编写。它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

    派生继承自java.lang.ClassLoader,父类加载器为启动类加载器

  • 应用程序(Application)类加载器:由java编写。它负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库。

    程序默认类加载器。

  • 自定义类加载器。

链接
验证

主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。

对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?

对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?

对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。

对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?

准备

主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值

特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。

  • 8种基本类型的初值,默认为0;
  • 引用类型的初值则为null;
  • 常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456
解析

将常量池内的符号引用替换为直接引用的过程。

两个重点:

  • 符号引用。即一个字符串,这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
  • 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量

如调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名符号引用)这些符号引用替换直接引用

初始化

这个阶段主要是对类变量初始化,是执行类构造器的过程。

只对static修饰的变量或语句进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

GC算法:GarbageCollection垃圾收集

  • Java中,GC的对象是Java堆和方法区(即永久区)
  • 标记-清除算法:最经典的GC算法
  • 标记-整理算法:老年代GC
  • 复制收集算法:新生代GC
  • 分代收集算法:新生代+老年代GC
  • 引用计数算法:没有被Java采纳
  • 问题提炼:
    • GC算法说明
    • 解释可触及性
    • Stop-the-world的原因和危害
标记-清除算法

从根开始将可能被引用的对象用递归的方式进行标记(标记称为可达标识),然后将没有标记到的对象作为垃圾进行回收
7de44970-2e02-46a1-a5d0-0663b21906c6

问题:

  • 效率问题:首先,它的缺点就是效率比较低(递归与全堆对象遍历)
  • 空间问题:主要的缺点,则是这种方式清理出来的空闲内存是不连续的
    • 空间碎片太多会导致大内存对象无法生成而频繁进行GC。
    • JVM就不得不维持一个内存的空闲列表,这又是一种开销。
    • 而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
标记-整理算法:(老年代的GC)

标记-清除算法一样,从根节点开始,标记所有可达对象;

之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端;

之后,清理边界外所有的空间。

cc79889a-0856-4018-92c3-c51108c9caea

  • 标记-压缩算法适合用于存活对象较多的场合,如老年代。

  • 内存空间整齐

复制收集算法:(新生代的GC)

将原有的内存空间分为两块,每次只使用其中一块,使用块称为内存1,未使用称为内存2

在垃圾回收时,将内存1存活对象复制到内存2

之后,清除内存1所有对象,交换两个内存的角色,完成垃圾回收。

ff1e1846-e49c-4663-aee1-7c63628f567c

问题

  • 最大问题:空间浪费:实际可用内存缩小为原来的一半。

  • 对象存活率较高时就要进行较多的复制操作,效率变低

    • 不适用于存活对象较多的场合,如老年代(复制算法适合做新生代的GC

现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间

分代收集算法:(新生代的GC+老年代的GC)

把Java堆分为新生代和老年代:短命对象归为新生代,长命对象归为老年代

  • 少量对象存活,适合复制算法

    新生代中,每次GC时都发现有大批对象死去,只有少量存活。

    选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。

  • 大量对象存活,适合用标记-清理/标记-整理

    老年代中,对象存活率高、没有额外空间对他进行分配担保,必须使用“标记-清理”/“标记-整理”算法进行GC。

引用计数算法:(没有被Java采纳)

给对象中添加一个引用计数器

每有一个地方引用它,计数器值加1

引用失效时,计数器值就减1

任何时刻计数器为0的对象就是不可能再被使用的。

应用举例

  • 微软COM技术ComputerObjectModel
  • Python

问题

  • 引用和去引用伴随加法和减法,影响性能

  • 无法处理循环引用(致命)

    1a489e67-e047-408f-a97e-4a141e6ab3b0

其它术语

可触及性:

所有的算法,需要能够识别一个垃圾对象,因此需要给出一个可触及性的定义。

可触及的:

  从根节点可以触及到这个对象。

  其实就是从根节点扫描,只要这个对象在引用链中,那就是可触及的。

可复活的:

  一旦所有引用被释放,就是可复活状态

  因为在finalize()中可能复活该对象

不可触及的:

  在finalize()后,可能会进入不可触及状态

  不可触及的对象不可能复活

  要被回收。

Stop-The-World:

1、Stop-The-World概念:

  Java中一种全局暂停的现象。

全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互

多半情况下是由于GC引起
少数情况下由其他情况下引起,如:Dump线程、死锁检查、堆Dump。

2、GC时为什么会有全局停顿?

(1)避免无法彻底清理干净

  • 防止新的垃圾同时产生,永远无法彻底清理干净

(2)GC的工作必须在一个能确保一致性的快照中进行。

  • 这里的一致性的意思是:在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性无法得到保证。

  • 简而言之:减轻GC算法难度,动态运行容易使GC难以判断哪些是垃圾

  • 这点是导致GC进行时必须停顿所有Java执行线程的其中一个重要原因

3、Stop-The-World的危害:

长时间服务停止,没有响应(将用户正常工作的线程全部暂停掉)

遇到HA系统,可能引起主备切换,严重危害生产环境。

  备注:HA:HighAvailable,高可用性集群。

https://www.cnblogs.com/qianguyihao/p/4744233.html

垃圾回收器

7种垃圾回收器

JVM堆内存结构

image.png

堆内存被划分为新生代老年代,默认情况下配比为1:2(可以通过参数–XX:NewRatio 来指定)。

新生代中,划分为Eden取和两个Survivor区(s0,s1)默认比例为8:1:1。

JVM内存回收流程
  • 新生代区域
    • 新生成的对象进入Eden区。Eden满了触发Minor GC。存活的对象进入s0。
    • s0满了触发Minor GC。存活的对象进入s1。
    • s1满了触发Minor GC。存活的对象进入s0。
    • … 重复2,3操作,被转移对象每一次转移空间就长一岁,当到达某个值(15)时,进入老年代空间 …
  • 老年代区域
    • 每发生一次Minor GC,老年代都可能出现Major GC,视垃圾回收器而定。

Stop the World问题:任何一种GC算法发生,都会导致Stop the World。

垃圾回收器

image.png

新生代可配置的回收器:Serial、ParNew、Parallel Scavenge

老年代配置的回收器:CMS、Serial Old、Parallel Old

纵向概括:

  • 第一代:串行(Serial,Serial Old)
  • 第二代:并行(ParNew,Parallel Old)
  • 第三代:CMS,Parallel Scavenge
  • 第四代:G1(Garbage First)

新生代和老年代区域的回收器之间进行连线,说明他们之间可以搭配使用。

新生代垃圾回收器
  • Serial回收器

    最基本的、历史最悠久的回收器,俗称”串行回收器“,使用复制算法

    • 优势:单线程,减少上下文切换的系统开销。
    • 劣势:会STW(Stop The World),但对于新生代,存活对象少,停留时间短。
  • ParNew(Parallel New)

    就是Serial的并行版本,同样使用复制算法,其它参数也和Serial一样。

    这里的并行指的是垃圾收集是并行的,并非垃圾收集和程序运行并行

    • 优势:多线程执行,效果会比Serial好。
    • 劣势:在单核CPU上,涉及到线程切换,反而会比Serial差。
  • Parallel Scavenge

    与ParNew相似,都是使用并行收集和复制算法

    特点是更关注吞吐量
    $$
    吞吐量=\frac{代码运行时间}{代码运行时间+垃圾收集时间}
    $$
    这个结果越高,说明垃圾收集时间越短。

    可以使用两个参数来控制:

    1. -XX:MaxGCPauseMillis最大垃圾回收停顿时间。这个参数的原理是空间换时间,收集器会控制新生代的区域大小,从而尽可能保证回收少于这个最大停顿时间。简单的说就是回收的区域越小,那么耗费的时间也越小。 所以这个参数并不是设置得越小越好。设太小的话,新生代空间会太小,从而更频繁的触发GC。

    2. -XX:GCTimeRatio垃圾回收时间与总时间占比。这个是吞吐量的倒数,原理和MaxGCPauseMillis相同。

老年代垃圾回收器
  • Serial Old

    Serial的老年代版本,区别在于使用标记-整理算法

  • Parallel Old

    Parallel Scavenge的老年代版本,区别在于使用标记-整理算法

  • CMS(Concurrent Mark Sweep)

    更加关注最短回收停顿时间。使用标记-清除算法

    亮点在于Concurrent,意为和在程序代码执行的同时进行并发垃圾收集

    整个过程分为4个步骤:

    (1)初始标记;

    (2)并发标记;

    (3)重新标记;

    (4)并发清除。

    其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”

    • 优势
      • 并发收集
      • 低停顿
    • 劣势
      • 并发收集决定了它需要一定内存,如果内存增长很快,它自己的内存不够了,就会失败。此时JVM就会启动Serial Old来擦屁股。
      • 标记-清除算法导致内存不连续,出现内存碎片可以通过参数设置来决定回收之后或多少次回收之后进行一次内存整理。
G1

JDK1.7后的新收集器,用于取代CMS。(独立于新生代和老年代垃圾回收器)

深入剖析JVM:G1收集器+回收流程+推荐用例

  • 特点
    • 引入分区概念,弱化分代概念。
    • 合理利用垃圾收集各个周期的资源,解决其它收集器的缺陷。
  • G1把堆平均分为几个区域Region,每个区域中虽然还存在新生代和老年代(逻辑上),但GC针对整个区域。
  • G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做。
  • G1会在Young GC中使用、而CMS只能在O区使用。
JDK 11:ZGC

同时保证吞吐量、低延迟。

双亲委派

自定义的类和Java库里的类名字一样,JVM如何区分它们?

在Java语言中,任何一个类,都要由加载它的类加载器和这个类自己共同确定它在JVM中的唯一性。
也就是说两个类相等必须满足两个条件:

  1. 这两个类是被同一个类加载器加载的
  2. 这两个类的**.class路径和名字**是相同的。
    另外,双亲委派:如果你自己定义了一个与jdk自带类名包名一致的类,那么java也不会去加载该类。
双亲委派

(1)如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。

(2)如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader

(3)如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式

img

Java框架

Java 程序员必备的15 个框架,前3 个地位无可动摇 …


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!