面试问题总结java篇【详细介绍】

有些图上传起来太麻烦廖,有需要可以私信,有word版。
  1. Java

Public 可以被其他类访问;protected:自身、子类同一个包的都可以访问;default:同一个包的可以访问;private:只能被自身访问

    1. Java创建对象的过程

类加载检查、分配内存、初始化零值、设置对象头、执行init方法;

对象头的结构:

Markword:存储对象的hashcode、锁状态、分代年龄等

类型指针:类型指针指向对象的类元数据,JVM通过这个指针判断对象是哪个类的实例;

    1. 反射

对于任意一个对象,都能调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

静态编译:在编译时确定类型,绑定对象。

动态编译:在运行时确定类型,绑定对象。

      1. 反射机制的优缺点

优点:运行期类型的判断,动态加载类,提高代码灵活度

缺点:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。

反射是框架设计的灵魂。

在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。

      1. 反射机制的应用场景

①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;

②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。

Spring 通过 XML 配置模式装载 Bean 的过程:

1)将程序内所有 XML 或 Properties 配置文件加载入内存中;

2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例;

4)动态配置实例的属性

Bean

1、所有属性为private

2、提供默认构造方法

3、提供getter和setter

4、实现serializable接口

    1. 序列化和反序列化的底层实现原理是什么?
      1.  什么是序列化和反序列化

序列化将Java对象装换成字节序列对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建

序列化:反序列化把字节序列转换成Java对象客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

Java序列化中有的字段不想被序列化可以使用trainsient关键字修饰。Trainsient可以阻止变量序列化。

      1. 为什么需要序列化与反序列化

一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里).

二是利用序列化实现远程通信,即在网络上传送对象的字节序列。

三是通过序列化在进程间传递对象;

面试重点

只会序列化对象的状态(信息),而不是序列化对象结构(方法)

如果父类序列化了,那么子类自动实现了序列化,不需要再实现Serializable接口

当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化.

transient类型的成员变量不能被序列化,如果想序列化,你可以通过重写writeObject和readObject方法来实现指定字段序列化。注意:序列化和反序列化字段顺序要保持一致,需要序列化的字段都要写进方法中。

引用对象的序列化其实就是深拷贝。

    1. 浅拷贝与深拷贝

基本数据类型的特点:直接存储在栈(stack)中的数据

引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的.

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

浅拷贝的实现方式

Object.assign()注意:当object只有一层的时候,是深拷贝

Array.prototype.concat()

深拷贝的实现方式

 手写递归方法:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。

 JSON.parse(JSON.stringify()):这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。这是因为 JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数。

函数库lodash

    1. 线程池

(1) 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

(2) 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3) 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

    1. 进程、线程、协程
      1. 进程

 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位每个进程都有自己的独立内存空间,拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

      1. 线程

    线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,而拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程也由操作系统调度(标准线程是这样的)。只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

      1. 协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。

      1. 理解区分

(1)进程、线程 和 协程 之间概念的区别

对于进程、线程,都是有内核进行调度,有CPU时间片的概念,进行抢占式调度(有多种调度算法)

对于协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到

对于线程,每个线程都有一个id,这个在线程创建时就会返回,所以可以很方便的通过id操作某个线程。

    1. 如何保证线程安全
      1. 线程安全的实现方法

  保证线程安全以是否需要同步手段分类,分为同步方案和无需同步方案。

1、互斥同步

互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。临界区、互斥量和信号量都是主要的互斥实现方式

 在java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码质量,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。

此外,ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。

ReentrantLock常常对比着synchronized来分析,我们先对比着来看然后再一点一点分析。

(1)synchronized是独占锁(独占锁也叫排他锁,是指该锁一次只能被一个线程所持有,即能读数据又能修改数据。共享锁是指该锁可被多个线程所持有,只能读数据,不能修改数据),加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。

(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。

ReentrantLock好像比synchronized关键字没好太多,我们再去看看synchronized所没有的,一个最主要的就是ReentrantLock还可以实现公平锁机制。什么叫公平锁呢?也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。

https://baijiahao.baidu.com/s?id=1648624077736116382&wfr=spider&for=pc

  互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。

2、非阻塞同步

 随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步

 非阻塞的实现

CAS(compareandswap):CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,CAS指令指令时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。

CAS缺点:

ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成了1A-2B-3C。

JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

 3、无需同步方案

 要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。

  1)可重入代码

可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。

可重入代码的特点是不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入、不调用非可重入的方法等。

类比:synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁)

 2)线程本地存储

如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。

符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典的Web交互模型中的“一个请求对应一个服务器线程(Thread-per-Request)”的处理方式,这种处理方式的广泛应用使得很多Web服务器应用都可以使用线程本地存储来解决线程安全问题。

    1. Redis是什么

Redis(Remote Dictionary Server)是一个使用 C 语言编写的,高性能非关系型的键值对数据库。与传统数据库不同的是,Redis 的数据是存在内存中的,所以读写速度非常快,被广泛应用于缓存方向。Redis可以将数据写入磁盘中,保证了数据的安全不丢失,而且Redis的操作是原子性的。

Redis优缺点

优点:

基于内存操作,内存读写速度快

Redis是单线程的,避免线程切换开销及多线程的竞争问题。单线程是指网络请求使用一个线程来处理,即一个线程处理所有网络请求,Redis 运行时不止有一个线程,比如数据持久化的过程会另起线程。

支持多种数据类型,包括String、Hash、List、Set、ZSet等。

支持持久化。Redis支持RDB和AOF两种持久化机制,持久化功能可以有效地避免数据丢失问题。

支持事务。Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。

支持主从复制。主节点会自动将数据同步到从节点,可以进行读写分离。

缺点:

对结构化查询的支持比较差。

数据库容量受到物理内存的限制,不适合用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的操作。

Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

Redis为什么这么快?

基于内存:Redis是使用内存存储,没有磁盘IO上的开销。数据存在内存中,读写速度快。

单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。

IO多路复用模型:Redis 采用 IO 多路复用技术。Redis 使用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络I/O上浪费过多的时间。

高效的数据结构:Redis 每种数据类型底层都做了优化,目的就是为了追求更快的速度。

来晚一步,这篇文章已经删除了_牛客网

    1. 跳跃表

是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时, Redis就会使用跳跃表来作为有序集合健的底层实现

跳跃表在链表的基础上增加了多级索引以提升查找的效率,但其是一个空间换时间的方案,必然会带来一个问题——索引是占内存的。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略。

由zskiplistNode和skiplist两个结构定义,其中 zskiplistNode结构用于表示跳跃表节点,而 zskiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等

上图展示了一个跳跃表示例,最左边的是 skiplist结构,该结构包含以下属性:

header:指向跳跃表的表头节点,通过这个指针程序定位表头节点的时间复杂度就为O(1)

tail:指向跳跃表的表尾节点,通过这个指针程序定位表尾节点的时间复杂度就为O(1)

level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内),通过这个属性可以再O(1)的时间复杂度内获取层高最好的节点的层数。

length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内),通过这个属性,程序可以再O(1)的时间复杂度内返回跳跃表的长度。

结构右方的是四个 zskiplistNode结构,该结构包含以下属性:

层(level):节点中用1、2、L3等字样标记节点的各个层,L1代表第一层,L代表第二层,以此类推。

每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离(跨度越大、距离越远)。在上图中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。

每次创建一个新跳跃表节点的时候,程序都根据幂次定律(powerlaw,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。

后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。与前进指针所不同的是每个节点只有一个后退指针,因此每次只能后退一个节点。

分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。

成员对象(oj):各个节点中的o1、o2和o3是节点所保存的成员对象。在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)。

    1. Redis速度快的原因

5Redis6.0引入多线程:主要是为了提高网络IO读写性能。多线程只在网络数据的读写上使用了,命令执行还是单线程。

      1. 基于内存实现
      2. 高效的数据结构

内存重新分配

C 语言中涉及到修改字符串的时候会重新分配内存。修改地越频繁,内存分配也就越频繁。而内存分配是会消耗性能的,那么性能下降在所难免。

而 Redis 中会涉及到字符串频繁的修改操作,这种内存分配方式显然就不适合了。于是 SDS 实现了两种优化策略:

1、空间预分配

对 SDS 修改及空间扩充时,除了分配所必须的空间外,还会额外分配未使用的空间。

具体分配规则是这样的:SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。

2、惰性空间释放

当然,有空间分配对应的就有空间释放。

SDS 缩短时,并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。

二进制安全

你已经知道了 Redis 可以存储各种数据类型,那么二进制数据肯定也不例外。但二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 '\0' 等。

前面我们提到过,C 中字符串遇到 '\0' 会结束,那 '\0' 之后的数据就读取不上了。但在 SDS 中,是根据 len 长度来判断字符串结束的。

看,二进制安全的问题就解决了。

双端链表

压缩列表

字典

跳跃表

      1. 合理的数据编码
      2. 合适的线程模型

I/O多路复用模型:Redis 中使用 I/O 多路复用程序同时监听多个套接字,并将这些事件推送到一个队列里,然后逐个被执行。最终将结果返回给客户端。

Redis 中使用了 Reactor 单线程模型,你可能对它并不熟悉。没关系,只需要大概了解一下即可。

接收到用户的请求后,全部推送到一个队列里,然后交给文件事件分派器,而它是单线程的工作方式。Redis 又是基于它工作的,所以说 Redis 是单线程的。

    1. redis数据结构

对redis来说,所有的key(键)都是字符串.

      1. String 字符串类型

实战场景:

1、缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。

2、计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。

3、session:常见方案spring session + redis实现session共享

      1. Hash (哈希)

实战场景:

  1. 缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。
      1. 链表

实战场景:

  1. timeline:例如微博的时间轴,有人发布微博,用lpush加入时间轴,展示新的列表信息。
      1. Set   集合

实战场景;

1.标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。

2、点赞,或点踩,收藏等,可以放到set中实现

      1. zset  有序集合

排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。

Zset:排行榜

使用ziplist:元素数量小于128,每个元素长度小于64字节,根据某种规则编码在连续的内存区域;

使用字典和skiplist:字典的健保存元素的值,值保存元素的分值;

Bitmap:存储的连续二进制数字,通过一个bit表示某个元素对应的值或者状态;

插入元素:1<<i%8 | 原来的数

删除:(~(1<<(i%8))) & 原来的数

查找:数 & (1<<i)

可以用于排序、查找、去重;bitmap,基数统计 地理位置

    1. Redis的过期键删除策略
      1. 定时删除策略

定时删除策略通过使用定时器,定时删除策略可以保证过期键尽可能快地被删除,并释放过期键占用的内存。

因此,定时删除策略的优缺点如下所示:

优点:对内存非常友好

缺点:对CPU时间非常不友好

      1. 惰性删除策略

惰性删除策略只会在获取键时才对键进行过期检查,不会在删除其它无关的过期键花费过多的CPU时间。

因此,惰性删除策略的优缺点如下所示:

优点:对CPU时间非常友好

缺点:对内存非常不友好

      1. 定期删除策略

是定时删除策略和惰性删除策略的一种整合折中方案。

定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响,同时,通过定期删除过期键,也有效地减少了因为过期键而带来的内存浪费。

    1. Redis的持久化机制

Redis是内存数据库,它将自己的数据存储在内存里面,一旦Redis服务器进程退出或者运行Redis服务器的计算机停机,Redis服务器中的数据就会丢失。

为了避免数据丢失,所以Redis提供了持久化机制,将存储在内存中的数据保存到磁盘中,用于在Redis服务器进程退出或者运行Redis服务器的计算机停机导致数据丢失时,快速的恢复之前Redis存储在内存中的数据。

RDB持久化:RDB持久化是将某个时间点上Redis中的数据保存到一个RDB文件中,基于RDB持久化的上述性质,所以RDB持久化也叫做快照持久化。

AOF持久化:AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库数据的.

    1. Redis主从复制

解决了单台Redis服务器由于意外情况导致Redis服务器进程退出或者Redis服务器宕机而造成的数据丢失问题

主从复制可以实现读写分离,提高Redis的高可用性,即主服务器用来执行写命令,多个从服务器用来执行读命令,类似于数据库的读写分离

从服务器只能执行读命令,执行写命令时会报错误

    1. redis缓存数据一致性解决方案

双写模式

做法顺序:先写数据库,再写缓存

由于卡顿等原因,导致写缓存2在最前,写缓存1在后面就出现了不一致

脏数据问题:

  这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据

读到的最新数据有延迟:最终一致性

失效模式

做法顺序:先写数据库,在删除缓存

由于网络或者i/o问题导致第三个请求拿到了数据库中数据:db-1,此时第二个请求数据库写更新db-1->db-2已完成,立刻删除缓存,第三个请求又将缓存刷新成第一个请求时的数据

还是会出现脏数据问题:最终不一致性

解决方案:

无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?

如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加 上过期时间,每隔一段时间触发读的主动更新即可

如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式(比较优秀)。

缓存数据+过期时间也足够解决大部分业务对于缓存的要求。

通过加锁保证并发读写,写的时候按顺序排好队。读无所谓。所以适合使用读写锁。(业务不关心 脏数据,允许临时脏数据可忽略);

    1. Redis分布式锁

有三个购票项目,对应一个库存,每一个系统会有多个线程,和上文一样,对库存的修改操作加上锁,能不能保证这6个线程的线程安全呢?

当然是不能的,因为每一个购票系统都有各自的JVM进程,互相独立,所以加synchronized只能保证一个系统的线程安全,并不能保证分布式的线程安全。

所以需要对于三个系统都是公共的一个中间件来解决这个问题。

这里我们选择Redis来作为分布式锁,多个系统在Redis中set同一个key,只有key不存在的时候,才能设置成功,并且该key会对应其中一个系统的唯一标识,当该系统访问资源结束后,将key删除,则达到了释放锁的目的。

1、Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。

2、Redis的SETNX命令可以方便的实现分布式锁。

setNX(SET if Not eXists)

语法:SETNX key value

返回值:设置成功,返回 1 ;设置失败,返回 0 。(http://www.amjmh.com/v/)

当且仅当 key 不存在时将 key 的值设为 value,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

综上所述,可以通过setnx的返回值来判断是否获取到锁,并且不用担心并发访问的问题,因为Redis是单线程的,所以如果返回1则获取到锁,返回0则没获取到。当业务操作执行完后,一定要释放锁,释放锁的逻辑很简单,就是把之前设置的key删除掉即可,这样下次又可以通过setnx该key获取到锁了。

可能会有死锁的问题发生,比如服务器 1 设置完之后,获取了锁之后,忽然发生了宕机。

那后续的删除 Key 操作就没法执行,这个 Key 会一直在 Redis 中存在,其他服务器每次去检查,都会返回 0,他们都会认为有人在使用锁,我需要等。

为了解决这个死锁的问题,我们就需要给 Key 设置有效期了。设置的方式有 2 种:

第一种就是在 Set 完 Key 之后,直接设置 Key 的有效期 "expire key timeout" ,为 Key 设置一个超时时间,单位为 Second,超过这个时间锁会自动释放,避免死锁

这种方式相当于,把锁持有的有效期,交给了 Redis 去控制。如果时间到了,你还没有给我删除 Key,那 Redis 就直接给你删了,其他服务器就可以继续去 Setnx 获取锁。

第二种方式,就是把删除 Key 权利交给其他的服务器,那这个时候就需要用到 Value 值了,比如服务器 1,设置了 Value 也就是 Timeout 为当前时间 +1 秒

这个时候服务器 2 通过 Get 发现时间已经超过系统当前时间了,那就说明服务器 1 没有释放锁,服务器 1 可能出问题了,服务器 2 就开始执行删除 Key 操作,并且继续执行 Setnx 操作。

但是这块有一个问题,也就是不光你服务器 2 可能会发现服务器 1 超时了,服务器 3 也可能会发现,如果刚好服务器 2 Setnx 操作完成,服务器 3 就接着删除,是不是服务器 3 也可以 Setnx 成功了?

那就等于是服务器 2 和服务器 3 都拿到锁了,那就问题大了。这个时候怎么办呢?

这个时候需要用到“GETSET key value”命令了。这个命令的意思就是获取当前 Key 的值,并且设置新的值。

假设服务器 2 发现 Key 过期了,开始调用 getset 命令,然后用获取的时间判断是否过期,如果获取的时间仍然是过期的,那就说明拿到锁了。

如果没有,则说明在服务 2 执行 getset 之前,服务器 3 可能也发现锁过期了,并且在服务器 2 之前执行了 getset 操作,重新设置了过期时间。

那么服务器 2 就需要放弃后续的操作,继续等待服务器 3 释放锁或者去监测 Key 的有效期是否过期。

这块其实有一个小问题是,服务器 3 已经修改了有效期,拿到锁之后,服务器 2 也修改了有效期,但是没能拿到锁。

但是这个有效期的时间已经被在服务器 3 的基础上又增加一些,但是这种影响其实还是很小的,几乎可以忽略不计。

RDB 和 AOF 持久化各有利弊,RDB 可能会导致一定时间内的数据丢失,而 AOF 由于文件较大则会影响 Redis 的启动速度,为了能同时使用 RDB 和 AOF 各种的优点,Redis 4.0 之后新增了混合持久化的方式。

快照(RDB):按照一定的时间间隔将内存中的数据集备份,fork一个子进程,先将数据集写入一个临时文件,写入成功后替换之前的文件;

只追加文件(AOF):实时性好,记录每一条更改Redis中数据的命令。

Redis AOF文件过大:

使用AOF重写,产生一个新的AOF文件,新的AOF文件和原有的文件保存的数据库状态一样,体积更小。是通过读取数据库中的键值对来实现的,不用对AOF文件进行分析;

      1. 混合持久化

判断是否开启 AOF 持久化,开启继续执行后续流程,未开启执行加载 RDB 文件的流程;

判断 appendonly.aof 文件是否存在,文件存在则执行后续流程;

判断 AOF 文件开头是 RDB 的格式, 先加载 RDB 内容再加载剩余的 AOF 内容;

判断 AOF 文件开头不是 RDB 的格式,直接以 AOF 格式加载整个文件。

混合持久化优点:

混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。

混合持久化缺点:

AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;

兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。

    1. Java设计模式中的几种常用设计模式总结
      1. 单例模式

懒汉式单例

public class Person{

    //为了不让其他类直接访问该成员 懒汉式单例,在使用时创建对象

    //1、私有静态变量

    private static Person person=null;

    //2、将构造器私有化

    private Person(){

    

    }

    //3、提供一个静态方法,并返回该类的对象

    public static Person getInstance(){

        if(person==null){

            //第一次访问

            person=new Person();;

        }

        return person;

    }

    public void sayHello(){

        System.out.println("sayHello方法");

    }

}

饿汉式单例

public class Student {

    //1、 饿汉式单例模式,  在类加载时创建一个对象

    private static Student student = new Student();

    // 2 构造器私有化

    private Student(){

    }

    // 3 提供返回类对象的静态方法

    public static Student getInstance(){

        if(student !=null){

            return student;

        }

         return null;

    }

}

      1. 简单工厂模式静态工厂模式

简单工厂模式的工厂类一般是使用静态方法,通过接收的参数的不同来返回不同的对象实例。不修改代码的话,是无法扩展的

      1. 工厂方法模式

它的做法是在父类声明一个final方法用来真正被外部调用(在子类中被继承但是不允许覆盖)。在这个方法中调用一个抽象方法去具体实现新建对象,可是这个抽象方法本身由子类实现。

      1. 抽象工厂模式

★工厂模式中,重要的是工厂类,而不是产品类。产品类可以是多种形式,多层继承或者是单个类都是可以的。但要明确的,工厂模式的接口只会返回一种类型的实例,这是在设计产品类的时候需要注意的,最好是有父类或者共同实现的接口。

★使用工厂模式,返回的实例一定是工厂创建的,而不是从其他对象中获取的。

★工厂模式返回的实例可以不是新创建的,返回由工厂创建好的实例也是可以的。

      1. 区别

1、对于简单工厂,用于生产同一结构中的任意产品,对于新增产品不适用

2、对于工厂方法,在简单工厂的基础上,生产同一等级结构中笃定产品,可以支持新增产品

  1. 抽象工厂,用于生产不同种类(品牌)的相同类型(迷你,SUV),对于新增品牌可以,不支持新增类型
    1. Java 垃圾回收机制
      1. 如何确定某个对象是“垃圾”

在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法

但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。

为了解决这个问题,在Java中采取了可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

      1. 典型的垃圾收集算法

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

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。

标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

Copying(复制)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。

内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低

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

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

Generational Collection(分代收集)算法

核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法

部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)。

年轻代有被分为Eden区,From区与To区。

当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。

一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区。

这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区。

经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。

老年代经过这么几次折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除(Full GC),也就是全量回收,一起滚蛋吧。

全量回收呢,就好比我们刚才比作的大扫除,毕竟动做比较大,成本高,不能跟平时的小型值日(Young GC)相比,所以如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。

所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。

      1. 什么时候触发young gc和full gc:

Young GC:当eden区域满的时候触发,young gc有部分存活的会晋升到老年代;

Full GC:当出发young gc的时候,发现新生代数据平均晋升大小比当前老年代剩余的空间打,直接触发Full GC;或者system.gc()

      1. 老年代:

大对象(需要大量连续内存空间的对象,字符串、数组等)直接进入老年代,为了避免大对象分配内存时由于分配担保机制带来的复制降低效率。

分配担保机制:

发生新生代垃圾回收时,JVM会检查老年代最大的连续空间是否大于新生代所有对象的和,大于安全,小于的话判断是否允许分配担保失败,如果允许判断是否老年代最大的可用连续空间大于之前晋升到老年代对象的平均大小,如果大于就进行新生代垃圾回收,小于的话或者不允许分配担保的话可以进行老年代垃圾回收;

在新生代回收之后大部分对象仍然存活,就需要老年代进行分配担保把survior无法容纳的对象直接晋升到老年代;

判断对象死亡:

引用计数器:每当有地方引用它,计数器加1;当引用失效,计数器减1.计数器为0的对象就是不能被使用的。(效率较高,实现简单,但很难解决对象之间相互循环引用的问题)

      1. 典型的垃圾收集器

1.Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

2.ParNew收集器 是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

3.Parallel Scavenge收集器 是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

4.Parallel Old收集器 是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

5、CMS(Concurrent Mark Sweep)收集器 是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

垃圾回收器在工作的时候,垃圾回收的线程和工作线程同时进行,叫做:concurrent mark sweep;当内存比较小的情况下,清理垃圾的速度比较快没有问题,当内存越来越大较的时候,大到什么程度呢?大到你用多少线程进行清理的都需要很长一段时间,以前10G的内存的时候使用默认的ps+po大概需要的时间为11s。

下面来解释一下CMS常见的几个阶段

第一个阶段叫做CMS initial mark(初始标记阶段)这个阶段很简单,就是直接找到最根上的对象,然后进行标记,其他的对象不标记。

第二个阶段是CMS concurrent mark(并发标记)据统计百分之八十的时间都浪费在这里,因此它把这块耗时最长的阶段和我们的应用程序同时进行,对于客户来说感觉上可能慢了点,但是至少还有反应,这里的并发标记是一边产生垃圾一边进行跟着标记,这个过程很难完成;

第三阶段是CMS remark (重新标记)这里又是一个STW,在上一个并发标记的过程中产生的新垃圾在这个阶段进行标记,这个时候需要停顿,但是时间不会很长,毕竟只是上一步很短时间内产生的新垃圾;

在 "Stop the World" 阶段, 当前运行的所有程序将被暂停, 扫描内存的 root 节点和添加写屏障 (write barrier) 。简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW

可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿

停顿的原因:

分析工作必须在一个能确保一致性的快照中进行

一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上V

如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

特点描述:

被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 所以我们需要减少STW的发生。

STW事件和采用哪款GC无关,所有的GC都有这个事件。

哪怕是G1也不能完全避免Stop一the一world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。

开发中不要采用System.gc();会导致Stop一the一world的发生

第四阶段是concurrent sweep(并发清理)并发清理也有他的问题,在并发清理的过程中也会产生新的垃圾,这个垃圾叫做浮动垃圾,这个浮动垃圾就要等下一次CMS来清理了;

什么条件下会触发CMS呢?老年代分配不下了,这时会触发CMS,这里的初始标记是单线程,重新标记是多线程。

CMS的缺点:CMS出现问题后会调用Serial Old使用单线程进行标记压缩;

memory fragmentation 内存的碎片:这个是比比较严重的一个问题,如果内存超级大,产生了很多的碎片,这就会浪费很多的空间,其实CMS设计出来就是应付几百兆的内存至上G的内存,有的人拿它来应付很大内存就会出现问题,因为一旦老年代产生很多碎片,从新生代过来的对象就会出现找不到空间的情况,这叫做:PromotionFailed找不到空间。这时候它干了一件事情,把Serial Old 请出来,让它用一个线程在这里进行标记压缩。

floating garbage浮动垃圾:当出现Concurrent Mode Failure 和 PromotionFailed时,说明碎片较多,Old区内存分配不下,会调用Serial Old。并不是说使用了CMS之后这个问题没办法避免,可以降低触发CMS的阈值。

解决方法

降低CMS的阈值:  -XX:CMSInitiatingOccupancyFraction 92%  可以降低这个值,让CMS保持老年代有足够的空间。

Remark阶段的算法:

怎么才能进行并发标记呢 非常的复杂,CMS采用的是三色标记+Incremental Update 算法。

三色扫描算法:白灰黑

在并发标记时,引用可能发生变化,白色对象有可能会被错误回收

解决方法:SATB (snapshot at the beginning)     在起始的时候做一个快照    

Incremental Update  :  当一个白色对象被一个黑色对象引用时,将黑色对象重新标记为灰色,让垃圾回收器重新扫描;

      1. G1收集器 

是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

GC收集器的三个考量指标:

占用的内存(Capacity)

延迟(Latency)

吞吐量(Throughput)

随着硬件的成本越来越低,机器的内存也越来越大,GC收集器占用的内存基本上可以容忍。而吞吐量可以通过集群(增加机器)来解决。

随着JVM中内存的增大,STW的时间成为JVM急迫解决的问题,如果还是按照传统的分代模型,使用传统的垃圾收集器,那么STW的时间将会越来越长。

在传统的垃圾收集器中,STW的时间是无法预测的,有没有一种办法,能够首先定义一个停顿时间,然后反向推算收集内容呢?就像是领导在年初制定KPI一样,分配的任务多就多干些,分配的任务少就少干点。

G1的思路说起来也类似,它不要求每次都把垃圾清理的干干净净,它只是努力做它认为对的事情。

我们要求G1,在任意1秒的时间内,停顿不得超过10ms,这就是在给它制定KPI。G1会尽量达成这个目标,它能够反向推算出本次要收集的大体区域,以增量的方式完成收集。

这也是使用G1垃圾回收器(-XX:+UseG1GC)不得不设置的一个参数:-XX:MaxGCPauseMillis=10。

为了实现STW的时间可预测,首先要有一个思想上的改变。G1将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

G1在逻辑上还是划分Eden、Survivor、OLd,但是物理上他们不是连续的。

另外Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的进行回收大多数情况下都把Humongous Region作为老年代的一部分来进行看待。

      1. 可达性分析

可达性分析算法的主要思路是先找出一批根节点对象集合作为GC Roots(可称为根节点枚举),然后从这批根节点出发,查找其引用关系(类似于深度优先搜索),最终形成如下图这样的反映对象间依赖关系的图,若某些对象没有任何引用链与GC Roots相连(如下图中的Object5,Object6,Object7),则说明这些对象就是垃圾对象,是可以被垃圾收集器回收的。

如下这些对象就可以作为GC Roots对象:

虚拟机栈中引用的对象(参数,局部变量等)

方法区中类静态变量

​ 方法区中常量引用的对象

​ 本地方法栈中引用的对象

​ 被同步锁(synchronized)持有的对象

​ JMXBean等

从思路上来看,可达性分析算法很简单,就两步,第一步找出GC Roots,第二步从GC Roots开始向下遍历整个对象图。但在真正的实现中,还是有很多地方值得注意的。

​ 就拿根节点枚举来说。整个方法区那么多类,常量信息,若一个一个来检查那些可做GC Roots,耗费的时间肯定不少,所以在HotSpot虚拟机中就通过一组OopMap的数据结构来记录哪些位置是引用,在类加载完成后就将哪个对象内什么偏移量上是什么数据类型计算出来,这样收集器就可以直接得知这些信息,而不需要依次遍历整个方法区。

​ 而第二步从GC Roots向下遍历对象图则有很多优化措施,例如,如何让用户线程与垃圾收集线程并发运行?并发运行会产生什么问题?

      1. G1的运行过程

G1的运行过程与CMS大体一致,分为以下四个步骤:

初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。

并发标记( Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,并发时有引用变动的对象会产生漏标问题,G1中会使用SATB(snapshot-at-the-beginning)算法来解决,后面会详细介绍。

最终标记(Final Marking):对用户线程做一个短暂的暂停,用于处理并发标记阶段仍遗留下来的最后那少量的SATB记录(漏标对象)。

筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。

TAMS是什么?要达到GC与用户线程并发运行,必须要解决回收过程中新对象的分配,所以G1为每一个Region区域设计了两个名为TAMS(Top at Mark Start)的指针,从Region区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围。

三色标记

在三色标记法之前有一个算法叫Mark-And-Sweep(标记清除)。这个算法会设置一个标志位来记录对象是否被使用。最开始所有的标记位都是0,如果发现对象是可达的就会置为1,一步步下去就会呈现一个类似树状的结果。等标记的步骤完成后,会将未被标记的对象统一清理,再次把所有的标记位设置成0方便下次清理。

这个算法最大的问题是GC执行期间需要把整个程序完全暂停,不能实现用户线程和GC线程并发执行。因为在不同阶段标记清扫法的标志位0和1有不同的含义,那么新增的对象无论标记为什么都有可能意外删除这个对象。对实时性要求高的系统来说,这种需要长时间挂起的标记清扫法是不可接受的。所以就需要一个算法来解决GC运行时程序长时间挂起的问题,那就是三色标记法。

三色标记最大的好处是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个GC。

三色标记法很简单。首先将对象用三种颜色表示,分别是白色、灰色和黑色。

黑色:表示根对象,或者该对象与它引用的对象都已经被扫描过了。

灰色:该对象本身已经被标记,但是它引用的对象还没有扫描完。

白色:未被扫描的对象,如果扫描完所有对象之后,最终为白色的为不可达对象,也就是垃圾对象。

漏标问题在CMS和G1收集器中有着不同的解决方案。

CMS:采用IncrementalUpdate(增量更新)算法,在并发标记阶段时如果一个白色对象被一个黑色对象引用时,会将黑色对象重新标记为灰色,让垃圾收集器在重新标记阶段重新扫描。

G1:采用SATB(snapshot-at-the-beginning),在初始标记时做一个快照,当B和C之间的引用消失时要把这个引用推到GC的堆栈,保证C还能被GC扫描到,在最终标记阶段扫描STAB记录。

两种漏标解决方案的对比:

SATB算法关注的是引用的删除(B->C的引用)。

Incremental Update算法关注的是引用的增加(A->C 的引用),需要重新扫描,效率低。

并行并发概念补充:

并行(Parallel):多条垃圾收集器线程并行工作,用户线程处于等待。

并发(Concurrent):指用户线程与垃圾收集器线程同时执行(也可能交替执行)

    1. 类加载机制

JVM将class文件字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在(并不一定在堆中,HotSpot在方法区中)中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。

    1. 类加载过程

JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后

加载过程主要完成三件事情:

通过类的全限定名来获取定义此类的二进制字节流

将这个类字节流代表的静态存储结构转为方法区的运行时数据结构。

在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。

这个过程主要就是类加载器完成。

.java文件保存了业务的逻辑代码。Java编译器将.java文件编译成扩展名为.class的文件。.class文件中保存着java转换后虚拟机将要执行的指令,当需要某个类的时候,java虚拟机会加载.class文件,并创建对应的class对象,并将class文件加载到虚拟机的内存。

    1. 类加载机制-双亲委派

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器,每一层的类加载器都是如此,因此所有的累加器请求都会传给顶层的启动类加载器只有当父类加载器无法完成加载请求时,子类加载器才会尝试加载

      1. 类加载器:

BootstrapClassLoader(启动类加载器):最顶层的加载类,由C++实现,负载加载/lib目录下的jar包

ExtensionClassLoader(扩展类加载器):负载加载/lib/ext目录下的jar包和类

AppClassLoader(应用程序类加载器):面向用户的加载器,负载加载向前应用classpath下的所有jar包和类

为什么要实现自定义加载器:

类不一定存放在classpath(系统类加载器)对于自定义路径的class文件的加载,需要自己的类加载器;

需要做一些加密解密的操作,这就需要实现加载类的逻辑;

    1. volatile的作用和原理

当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。 当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

volatile的主要作用是在多处理器开发中保证共享变量对于多线程的可见性。 可见性的意思是,当一个线程修改一个共享变量时,另外一个线程能读取到修改以后的值。

volatile关键字的作用 - 茄子_2008 - 博客园

    1. 可达性分析

可达性分析算法的主要思路是先找出一批根节点对象集合作为GC Roots(可称为根节点枚举),然后从这批根节点出发,查找其引用关系(类似于深度优先搜索),最终形成如下图这样的反映对象间依赖关系的图,若某些对象没有任何引用链与GC Roots相连(如下图中的Object5,Object6,Object7),则说明这些对象就是垃圾对象,是可以被垃圾收集器回收的。

如下这些对象就可以作为GC Roots对象:

虚拟机栈中引用的对象(参数,局部变量等)

方法区中类静态变量

​ 方法区中常量引用的对象

​ 本地方法栈中引用的对象

​ 被同步锁(synchronized)持有的对象

​ JMXBean等

从思路上来看,可达性分析算法很简单,就两步,第一步找出GC Roots,第二步从GC Roots开始向下遍历整个对象图。但在真正的实现中,还是有很多地方值得注意的。

​ 就拿根节点枚举来说。整个方法区那么多类,常量信息,若一个一个来检查那些可做GC Roots,耗费的时间肯定不少,所以在HotSpot虚拟机中就通过一组OopMap的数据结构来记录哪些位置是引用,在类加载完成后就将哪个对象内什么偏移量上是什么数据类型计算出来,这样收集器就可以直接得知这些信息,而不需要依次遍历整个方法区。

​ 而第二步从GC Roots向下遍历对象图则有很多优化措施,例如,如何让用户线程与垃圾收集线程并发运行?并发运行会产生什么问题?有哪些解决方案?

    1. HashMap和HashTable的区别
      1. HashMap的put过程:

如果定位到的数组位置没有元素就直接插入;

有元素,判断和key是否相同,相同就覆盖,不同判断p是否是树节点,不是的话就遍历链表插入尾部;

插入后如果链表长度大于8并且数组长度超过64就将链表转为红黑树,不到64就扩容;

      1. HashMap的安全问题

可能会出现死循环,也可能出现put值被覆盖:

这个问题出现在扩容,并发对hashmap读取时,如果hashmap需要扩容,里面的元素需要重新经扰动函数计算新的下标,原来统一下标里面的值可能顺序发生了变化,这时候另一个线程并发访问可能会出现死循环问题;

      1. 区别

(1)HashMap 是线程不安全的,HashTable 是线程安全的;

(2)由于线程安全,所以 HashTable 的效率比不上 HashMap;

(3)HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable不允许;

(4)HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;

(5)HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode

(6)HaspMap当链表长度大于阈值默认为8,将链表转为红黑树;

    1. 为什么长度为8的时候转换、负载因子为什么是0.75

在负载因子0.75的情况下,单个hash槽内元素个数为8的概率小于百万分之一,到达8性能会受影响,采用红黑树提高性能;

负载因为若为1,某个hash对应的节点长度全部填充了才会发生扩容,意味着有大量的hash冲突,红黑树也变得复杂,查询效率低,保证了空间利用率;

负载因为位0.5,意味着数组填充到一半就进行扩容,填充的数据少hash冲突也少,查询效率高一些,但不能保证空间利用率;

使用0.75是时间和空间的权衡,避免了相当多的hash冲突,使得底层的链表或者红黑树高度较低,提升了空间利用率;

HashMap的put过程:

如果定位到的数组位置没有元素就直接插入;

有元素,判断和key是否相同,相同就覆盖,不同判断p是否是树节点,不是的话就遍历链表插入尾部;

插入后如果链表长度大于8并且数组长度超过64就将链表转为红黑树,不到64就扩容;

HashMap的安全问题,可能会出现死循环,也可能出现put值被覆盖:

这个问题出现在扩容,并发对hashmap读取时,如果hashmap需要扩容,里面的元素需要重新经扰动函数计算新的下标,原来统一下标里面的值可能顺序发生了变化,这时候另一个线程并发访问可能会出现死循环问题;

    1. 为什么HashMap使用红黑树而不使用AVL树

    1. ConcurrentHashMap为什么是线程安全的?

阿里面试题:ConcurrentHashMap为什么是线程安全的?_zycxnanwang的博客-CSDN博客_concurrenthashmap为什么线程安全

ConcurrentHashMap是由一个Segment数组和多个HashEntry数组组成

jdk1.8ConcurrentHashMap是数组+链表,或者数组+红黑树结构,并发控制使用Synchronized关键字和CAS操作。

其实就是将HashMap分为多个小HashMap,每个Segment元素维护一个小HashMap,目的是锁分离,本来实现同步,直接可以是对整个HashMap加锁,但是加锁粒度太大,影响并发性能,所以变换成此结构,仅仅对Segment元素加锁,降低锁粒度,提高并发性能。

你真的懂大厂面试题:HashMap吗?_zycxnanwang的博客-CSDN博客_hashmap大厂面试题

    1. jdk1.7 HashMap中的致命错误:循环链表
    2. 为什么 Zookeeper 可实现分布式锁

持久性节点;持久性顺序节点;临时性节点;临时性顺序节点

• 持久性节点表示只要你创建了这个节点,那不管你 ZooKeeper 的客户端是否断开连接,ZooKeeper 的服务端都会记录这个节点。

• 临时性节点刚好相反,一旦你 ZooKeeper 客户端断开了连接,那 ZooKeeper 服务端就不再保存这个节点。

• 顺便也说下顺序性节点,顺序性节点是指,在创建节点的时候,ZooKeeper 会自动给节点编号比如 0000001,0000002 这种的。

Zookeeper 有一个监听机制,客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)等,Zookeeper 会通知客户端。

    1. 集合
      1. ArrayList和LinkedList的区别

都不同步,不保证线程安全;

前者底层是Object数组,LinkedList底层是双向链表;

前者数组存储后者链表存储,前者支持随即访问,后者不行;

ArrayList会在列表结尾预留一定的容量空间,LinkedList每个元素都比ArrayList元素消耗的空间多;

ArrayList初始大小是10,每次1.5倍扩容;LinkedList是双向链表,没有初始大小,扩容在后面新增就好了;

线程安全的list:CopyOnWriteArrayList

当list被修改的时候,不直接修改原有的数组对象,将数组对象进行拷贝,修改的内容保存到副本中,再将修改完的副本替换为原来的数据,写入的时候加锁,避免多线程copy多多个副本;

缺点:拷贝数组会消耗大量时间,分配的时候会进入老年代,不一定能迅速的清除;适合多读少写;

      1. Array.sort底层排序

如果长度大于286判断是否具有结构(降序组小于67),有结构进入归并,没有结构进入快拍;

如果长度大于47小于286,快排;

如果长度小于47,插入排序;

    1. HashMap

Java7中ConcurrentHashMap使用的分段锁,在每一个segment上同时只有一个线程可以操作,每个segment都是一个类似hashmap的数据结构,个数一点确定不能改变;segment数组+HashEntry数组+链表。

Java8中concurrentHashMap使用synchronized锁加CAS的机制,结构是Node数组+链表/红黑树。

put的过程:

根据key计算hashcode,判断是否需要初始化,如果为空表示当前位置可以写,尝试CAS写入,失败则自旋保证成功;如果当前位置的hashcode == MOVED == -1,需要扩容;如果都不满足利用synchronized锁写入数据;如果数量大于阈值转为红黑树

      1. concurrentHashMap获取size的过程

1.7有两种方案,

第一种通过不加锁的方式多次计算size,最多三次,比较前后两次计算的值,一致就认为没有元素加入。

第二种是给每个segment加锁,然后计算size

    1. Synchronize

    1. 为什么重写equals需要重写hashcode

默认的hashcode方法是根据对象的内存地址经哈希算法得到的,重写了equals,不同对象也可能会相等,这样hashcode计算两个相等对象得到的hashcode不一致,所以需要重写hashcode;

    1. java中sdk和api

sdk:软件开发工具包,提供实现软件产品的工具包;

Api:已经实现的特定功能函数;

    1. Exception和Error

Java从Throwalbe派生出Exception和Error,其中Exception是可以恢复的异常,在java类库、方法以及运行时候的异常,Error表示系统错误,属于不可恢复的错误,会导致程序中止;

Exception又分为检查异常和运行异常

运行异常像:空指针、类型转换异常、越界、缓冲区溢出、非法参数等

检查异常像:IO异常、中断异常、SQL异常等

7、JVM调优:降低FGC的时间和频率

NewRatio:调整新生代和老年代的比例,默认1:4

SurvivorRatio:2个servivior区和eden区的比例大小

PretenureSizeThreshold:晋升到老年代的对象大小

    1. JVM调优:降低FGC的时间和频率

NewRatio:调整新生代和老年代的比例,默认1:4

SurvivorRatio:2个servivior区和eden区的比例大小

PretenureSizeThreshold:晋升到老年代的对象大小

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值