Java基础常见面试题整理

1、Java模式

具体见代码

2、集合

Collection(单列集合)

1、List(有序,可重复)
ArrayList
底层数据结构是数组,查询快,增删慢
线程不安全,效率高
Vector
底层数据结构是数组,查询快,增删慢
线程安全,效率低
LinkedList
底层数据结构是链表,查询慢,增删快
线程不安全,效率高
2、Set(无序,唯一)
HashSet
底层数据结构是哈希表。
哈希表依赖两个方法:hashCode()和equals()
执行顺序:
首先判断hashCode()值是否相同
是:继续执行equals(),看其返回值
是true:说明元素重复,不添加
是false:就直接添加到集合
否:就直接添加到集合
最终:
自动生成hashCode()和equals()即可
LinkedHashSet
底层数据结构由链表和哈希表组成。
由链表保证元素有序。
由哈希表保证元素唯一。
TreeSet
底层数据结构是红黑树。(是一种自平衡的二叉树)
如何保证元素唯一性呢?
根据比较的返回值是否是0来决定
如何保证元素的排序呢?
两种方式
自然排序(元素具备比较性)
让元素所属的类实现Comparable接口
比较器排序(集合具备比较性)
让集合接收一个Comparator的实现类对象
SortedSet

3、Queue

Map(双列集合)

A:Map集合的数据结构仅仅针对键有效,与值无关。
B:存储的是键值对形式的元素,键唯一,值可重复。

HashMap
底层数据结构是哈希表。线程不安全,效率高
(1)数据结构和实现原理
由数组和链表实现,数组⾥⾯每个地⽅都存了Key-Value这样的实例,在Java7叫Entry在Java8中叫Node。每⼀个节点都会保存⾃身的hash、key、value、以及下个节点,Java7采用头插法在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引⽤关系。Java8采用尾插法在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引⽤关系。
默认初始16(实现均匀分布)
(2)设哈希函数为Hash(key) = key%11,地址冲突处理算法:
链地址法:通常情况HashMap,HashSet,LinkedHashSet,LinkedHashMap,ConcurrentHashMap,HashTable,IdentityHashMap和WeakHashMap均采用这种方法处理冲突。
二次探测法:Hash2(key) = (Hash(key)%11 + d)%11
在这里插入图片描述

ConcurrentHashMap
底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。
我先说⼀下他在1.7中的数据结构吧
在这里插入图片描述
如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap ⼀样,仍然是数组加链表。Segment 是 ConcurrentHashMap 的⼀个内部类,HashEntry跟HashMap差不多的,但是不同点是,他使⽤volatile去修饰了他的数据Value还有下⼀个节点next。
ConcurrentHashMap 采⽤了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap ⽀持CurrencyLevel (Segment 数组数量)的线程并发。每当⼀个线程占⽤锁访问⼀个 Segment 时,不会影响到其他的 Segment。就是说如果容量⼤⼩是16他的并发度就是16,可以同时允许16个线程操作16个Segment⽽且还是线程安全的。是的,因为基本上还是数组加链表的⽅式,我们去查询的时候,还得遍历链表,会导致效率很低,这个跟jdk1.7的HashMap是存在的⼀样问题,所以他在jdk1.8完全优化了。

jdk1.8的优化
其中抛弃了原有的 Segment 分段锁,⽽采⽤了 CAS + synchronized 来保证并发安全性。跟HashMap很像,也把之前的HashEntry改成了Node,但是作⽤不变,把值和next采⽤了volatile去修饰,保证了可⻅性,并且也引⼊了红⿊树,在链表⼤于⼀定值的时候会转换(默认是8)。1.8 在 1.7 的数据结构上做了⼤的改动,采⽤红⿊树之后可以保证查询效率( O(logn) ),甚⾄
取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

LinkedHashMap
底层数据结构由链表和哈希表组成。
由链表保证元素有序。
由哈希表保证元素唯一。

Hashtable
底层数据结构是哈希表。线程安全,效率低。
Hashtable 是不允许键或值为 null 的
默认初始11

TreeMap
底层数据结构是红黑树。(是一种自平衡的二叉树)
如何保证元素唯一性呢?
根据比较的返回值是否是0来决定
如何保证元素的排序呢?
两种方式
自然排序(元素具备比较性)
让元素所属的类实现Comparable接口
比较器排序(集合具备比较性)
让集合接收一个Comparator的实现类对象

红黑树

关于红黑树

3、多线程(原子性、可见性、有序性)

(1)并行和并发

并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)
并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行

(2)线程的五种状态

在Java当中,线程通常都有五种状态,创建、就绪、运行、阻塞和死亡:
  第一是创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
  第二是就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
  第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个事件的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
  第五是死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。

(3)什么是线程安全?

所以线程安全指的是,在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。

即堆内存空间在没有保护机制的情况下,对多线程来说是不安全的地方,因为你放进去的数据,可能被别的线程“破坏”。
  在这里插入图片描述

(4)线程池

作用:线程启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互,而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象使用。
应用场景:假设一个服务完成一项任务所需时间为:T1创建线程时间,T2在线程中执行任务的时间,T3销毁线程的时间。如果T1+T3远大于T2,则可以采用线程池。
线程池是关注如何缩短或调整T1,T3时间的技术,从而提高服务器的性能。它把T1,T3分别安排在服务器启动和结束的时间段,或者一些空闲的时间段,这样在服务器处理客户请求时,不会有T1,T3的开销。

常见线程池有以下几种:

CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。

SingleThreadPool:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

常见参数:

corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
unit : keepAliveTime 参数的时间单位。
threadFactory :executor 创建新线程的时候会用到。
handler :饱和策略。关于饱和策略下面单独介绍一下。

ThreadPoolExecutor 饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:

1、ThreadPoolExecutor.AbortPolicy: 抛出 RejectedExecutionException来拒绝新任务的处理。
2、ThreadPoolExecutor.CallerRunsPolicy: 调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
3、ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
4、ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

举个例子:Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)

实例代码见本地例子

(5)同步的实现

概念

同步锁:同一时刻,一个同步锁只能被一个线程访问。以对象为依据,通过synchronized关键字来进行同步,实现对竞争资源的互斥访问。

独占锁(可重入的互斥锁):互斥,即在同一时间点,只能被一个线程持有;可重入,即可以被单个线程多次获取。什

公平锁:是按照通过CLH等待线程按照先来先得的规则,线程依次排队,公平的获取锁,是独占锁的一种。Java中,ReetrantLock中有一个Sync类型的成员变量sync,它的实例为FairSync类型的时候,ReetrantLock为公平锁。设置sync为FairSync类型,只需——Lock lock = new ReetrantLock(true)。

非公平锁:是当线程要获取锁时,它会无视CLH等待队列而直接获取锁。ReetrantLock默认为非公平锁,或——Lock lock = new ReetrantLock(false)。

实现方式

Synchronized

synchronized的原理及其新版本优化
随着 Java SE 1.6 对 synchronized 进⾏了各种优化之后,有些情况下它就并不那么重,Java SE1.6 中为了减少获得锁和释放锁带来的性能消耗⽽引⼊的偏向锁和轻量级锁。针对 synchronized 获取锁的⽅式,JVM 使⽤了锁升级的优化⽅式,就是先使⽤偏向锁优先同⼀线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂⾃旋,防⽌线程被系统挂起。最后如果以上都失败就升级为重量级锁。
在这里插入图片描述

ReentrantLock

ReentrantLock在介绍这玩意之前,我觉得我有必要先介绍
AQS(AbstractQueuedSynchronizer)。
AQS:也就是队列同步器,这是实现 ReentrantLock 的基础。
AQS 有⼀个 state 标记位,值为1 时表示有线程占⽤,其他线程需要进⼊到同步队列等待,同步队列是⼀个双向链表。
在这里插入图片描述

当获得锁的线程需要等待某个条件时,会进⼊ condition 的等待队列,等待队列可以有多个。当 condition 条件满⾜时,线程会从等待队列重新进⼊同步队列进⾏获取锁的竞争。

常见面试题

1.1. 说说 synchronized 关键字和 volatile 关键字的区别

volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

1.1. 说一说自己对于 synchronized 关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
synchronized关键字最主要的三种使用方式:

修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

1.3. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

1.3. 谈谈 synchronized和ReentrantLock 的区别

① 两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

③ ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。

sleep()wait()区别
在这里插入图片描述在这里插入图片描述

(6)死锁

顺便复习下死锁产生的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:进程已获得的资源在未使用完之前,不能强行剥夺
循环等待条件:若干进程之间形成头尾相接的循环等待资源关系
相应的预防措施:
采用静态资源分配策略,破坏部分分配条件
允许进程剥夺其它进程的资源,破坏不可剥夺的条件
采用资源有序分配,破坏环路条件
注意:互斥条件是无法被破坏的

Java实现线程死锁

public class Demo13_DeadLock {

    private static String s1 = "筷子左";
    private static String s2 = "筷子右";

    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                while (true){
                    synchronized (s1){
                        System.out.println(getName() + "获取"+ s1 + "等待" + s2);
                        synchronized (s2){
                            System.out.println(getName() +"拿到" + s2 + "开吃");
                        }
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                while (true){
                    synchronized (s2){
                        System.out.println(getName() +"获取"+ s2 + "等待" + s1);
                        synchronized (s1){
                            System.out.println(getName() +"拿到" + s1 + "开吃");
                        }
                    }
                }
            }
        }.start();
    }
}

(7)volatile

变量在多个线程之间可见
volatile关键字的作用和原理

(8)执行execute()方法和submit()方法的区别是什么呢?

1、execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

2、submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

(9)ThreadLocal

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。(Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复)

应用场景

之前我们上线后发现部分用户的日期居然不对了会重复,排查下来是SimpleDataFormat的锅,当时我们使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
其实要解决这个问题很简单,让每个线程都new 一个自己的SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat?所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

原理

主要就是ThreadLocalMap我们需要关注一下,而ThreadLocalMap呢是当前线程Thread一个叫threadLocals的变量中获取的。每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。

4、JVM

(1)常用命令

jmap 生成打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节
jstack 生成线程快照
jstat 查看堆内存各部分的使用量,以及加载类的数量
jhat 分析java堆,可以将堆中的对象以html的形式显示出来

(2)垃圾回收机制

c的垃圾回收是人工的,工作量大,但是可控性高。
java是自动化的,但是可控性很差,甚至有时会出现内存溢出的情况,
内存溢出也就是jvm分配的内存中对象过多,超出了最大可分配内存的大小。
System.gc()用于调用垃圾收集器,在调用时,垃圾收集器将运行以回收未使用的内存空间。它将尝试释放被丢弃对象占用的内存。
 然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。
 所以System.gc()并不能说是完美主动进行了垃圾回收。
 作为java程序员还是很有必要了解一下gc,这也是面试过程中经常出现的一道题目。
 我们从三个角度来理解gc。
 (1)jvm怎么确定哪些对象应该进行回收
 (2)jvm会在什么时候进行垃圾回收的动作
 (3)jvm到底是怎么清楚垃圾对象的

1、jvm怎么确定哪些对象应该进行回收

对象是否会被回收的两个经典算法:引用计数法,和可达性分析算法。
引用计数法

简单的来说就是判断对象的引用数量。实现方式:给对象共添加一个引用计数器,每当有引用对他进行引用时,计数器的值就加1,当引用失效,也就是不在执行此对象是,他的计数器的值就减1,若某一个对象的计数器的值为0,那么表示这个对象没有人对他进行引用,也就是意味着是一个失效的垃圾对象,就会被gc进行回收。
 但是这种简单的算法在当前的jvm中并没有采用,原因是他并不能解决对象之间循环引用的问题。
 假设有A和B两个对象之间互相引用,也就是说A对象中的一个属性是B,B中的一个属性时A,这种情况下由于他们的相互引用,从而是垃圾回收机制无法识别。
在这里插入图片描述
可达性分析算法
因为引用计数法的缺点有引入了可达性分析算法,通过判断对象的引用链是否可达来决定对象是否可以被回收。可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连(就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。
 因为引用计数法的缺点有引入了可达性分析算法,通过判断对象的引用链是否可达来决定对象是否可以被回收。可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连(就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。

2、在确定了哪些对象可以被回收之后,jvm会在什么时候进行回收

(1)会在cpu空闲的时候自动进行回收
(2)在堆内存存储满了之后
(3)主动调用System.gc()后尝试进行回收

3、如何回收

如何回收说的也就是垃圾收集的算法。
 算法又有四个:标记-清除算法,复制算法,标记-整理算法,分代收集算法.

1 标记-清除算法
 这是最基础的一种算法,分为两个步骤,第一个步骤就是标记,也就是标记处所有需要回收的对象,标记完成后就进行统一的回收掉哪些带有标记的对象。这种算法优点是简单,缺点是效率问题,还有一个最大的缺点是空间问题,标记清除之后会产生大量不连续的内存碎片,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而造成内存空间浪费。
 在这里插入图片描述
 
2 复制算法
复制将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。只是这种算法的代价是将内存缩小为原来的一半。
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,浪费了一半的空间。

3、标记-整理算法
标记整理算法与标记清除算法很相似,但最显著的区别是:标记清除算法仅对不存活的对象进行处理,剩余存活对象不做任何处理,造成内存碎片;而标记整理算法不仅对不存活对象进行处理清除,还对剩余的存活对象进行整理,重新整理,因此其不会产生内存碎片。
在这里插入图片描述
4、分代收集算法

分代收集算法是一种比较智能的算法,也是现在jvm使用最多的一种算法,他本身其实不是一个新的算法,而是他会在具体的场景自动选择以上三种算法进行垃圾对象回收。

那么现在的重点就是分代收集算法中说的自动根据具体场景进行选择。这个具体场景到底是什么场景。
场景其实指的是针对jvm的哪一个区域,1.7之前jvm把内存分为三个区域:新生代,老年代,永久代。

新生代

主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。

新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。

Eden区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
ServivorTo:保留了一次MinorGC过程中的幸存者。
ServivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者。
当JVM无法为新建对象分配内存空间的时候(Eden满了),Minor GC被触发。因此新生代空间占用率越高,Minor GC越频繁。

MinorGC的过程:采用复制算法。

1、首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,一般是15,则赋值到老年代区)
2、同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区)
3、然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。

老年代

老年代的对象比较稳定,所以MajorGC不会频繁执行。

在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

MajorGC采用标记—清除算法:

首先扫描一次所有老年代,标记出存活的对象
然后回收没有标记的对象。
MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。

当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

永久代

指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。

Class在被加载的时候被放入永久区域。它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。

在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

了解过场景之后再结合分代收集算法得出结论:
1、在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
2、老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。
了解过场景之后再结合分代收集算法得出结论:
1、在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
2、老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。
在这里插入图片描述

注意:
在jdk8的时候java废弃了永久代,但是并不意味着我们以上的结论失效,因为java提供了与永久代类似的叫做“元空间”的技术。
废弃永久代的原因:由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryErroy。元空间的本质和永久代类似。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。也就是不局限与jvm可以使用系统的内存。理论上取决于32位/64位系统可虚拟的内存大小。

5、动态代理

动态代理其实是一种方便运行时候动态的处理代理方法的调用机制。

通过动态代理可以给我们带来什么呢?通过代理可以让调用者和实现者之间解耦,例如RPC调用,对于我们调用者来说我就是想对用远程的那个方法,对于内部寻址啊,序列化反序列化等等这些交给代理来就行了,这样就能解放我们双手!

或者我们平日开发中需要监控一些方法的执行性能等,这时候就很时候用到代理了。

如我们需要监控每一个方法调用的开始时间和结束时间。
在这里插入图片描述
这样的话就把那些非业务逻辑的代码嵌入到我们的业务中,破坏了业务代码的纯粹性,所以我们希望把这个非业务的代码从业务代码中剥离出来,通过动态代理,我们可以将这些代码移除在动态运行的时候再织入它!

我们常见的动态代理有:JDK动态代理、Cglib(基于ASM)等。

JDK动态代理是基于Java的反射机制实现的,主要涉及到java.lang.reflect包中的Proxy和InvocationHandler。

InvocationHandler是一个接口,通过实现这个接口定义一个横切的逻辑!然后通过反射机制调用目标类的方法,这样就能动态的把非业务逻辑和业务逻辑动态的拼接在一起!

proxy则利用InvocationHandler创建代理实例,来间接的调用代理的方法!在这里插入图片描述

这样的话非业务代码就被剥离开了!但是注意JDK的反射是基于接口的!也就是你的service一定是有接口的不然是不行的!这时候就有个Cglib可以顶上了!

Cglib采用了底层的字节码技术,为代理类创建了一个子类来代理它!

在这里插入图片描述
但是要注意一点,Cglib因为是用子类代理所以对目标类中的final或者private方法进行代理。

再来对比一下JDK动态代理和Cglib,JDK动态代理在java1.3之前性能很差,但是随着java版本的提升jdk代理的性能也有所提升。

但是据有研究表明,Cglib的性能是JDK动态代理的大概十倍,但是Cglib创建动态代理所花费的时间是JDK的大概八倍。因此对于无需频繁创建代理对象的例如单例或者是有实力池的代理,比较适合用Cglib。反正它们俩的性能差别不是数量级上面的差别!

但是以上的代码我们可以看出,我们每次调用service都需要new 代理类!而且基本上等于强制service的每个方法都被监控了!有时候我们就需要其中的重点的一两个!

所以如果我们想用代理的话而且我们用Spring的话,推荐使用Spring AOP。AOP的底层就是通过使用JDK或者Cglib的动态代理实现的!

6、Java内存结构和内存模型

Java内存结构
Java内存模型

7、Error 和 Exception的区别

Error:表示由 JVM 所侦测到的无法预期的错误,由于这是属于 JVM 层次的严重错误,导致 JVM 无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。

Exception:表示可恢复的例外/异常,这是可捕捉到的。

Java 提供了两类主要的异常:RuntimeException和checked exception。

checked异常也就是我们经常遇到的 IO 异常,以及SQL 异常等。对于这种异常,JAVA 编译器强制要求我们必需对出现的这些异常进行处理。

但是RuntimeException,也称运行时异常,我们可以不处理。当出现这样的异常时,总是由虚拟机接管。比如:我们从来没有人去处理过NullPointerException 异常,它就是运行时异常,并且这种异常还是最常见的异常之一。
出现运行时异常后,系统会把异常一直往上层抛,一直遇到处理代码。如果没有处理块,到最上层,如果是多线程就由 Thread.run() 抛出 ,如果是单线程就被 main() 抛出 。抛出之后,如果是线程,这个线程也就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了。运行时异常是 Exception 的子类,也有一般异常的特点,也是可以被 Catch 块处理的。只不过往往我们不对他处理罢了。也就是说,你如果不对运行时异常进行处理,那么出现运行时异常之后,要么是线程中止,要么是主程序终止。
如果不想终止,则必须捕捉所有的运行时异常,决不让这个处理线程退出。队列里面出现异常数据了,正常的处理应该是把异常数据舍弃,然后记录日志。不应该由于异常数据而影响下面对正常数据的处理。在这个场景这样处理可能是一个比较好的应用,但并不代表在所有的场景都应该如此。如果在其它场景,遇到了一些错误,如果退出程序比较好,这时你就可以不理会运行时异常 。
异常处理的目标之一就是为了把程序从异常中恢复出来 。

8、类加载机制

Java类加载器和双亲委派机制

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值