Java基础面试题大总结(4)

简介: Java基础面试题大总结(4)

62、GC Root有哪些?


在Java语言中,可作为GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象。

方法区中类静态属性引用的对象。

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

本地方法栈中JNI(即一般说的Native方法)引用的对象。


63、垃圾收集有哪些算法,各自的特点?



标记 - 清除算法


首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。


复制算法


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


标记 - 整理算法


复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。


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


分代收集算法


当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。


一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。


在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。


在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。


64、为什么使用Executor框架?


1、 每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。


2、 调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。


3、 接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。


65、你能保证 GC 执行吗?


不能,虽然你可以调用 System.gc() 或者 Runtime.gc(),但是没有办法保证 GC的执行。


66、volatile关键字的原理是什么?干什么用的?


使用了volatile关键字的变量,每当变量的值有变动的时候,都会将更改立即同步到主内存中;而如果某个线程想要使用这个变量,就先要从主存中刷新到工作内存,这样就确保了变量的可见性。

一般使用一个volatile修饰的bool变量,来控制线程的运行状态。


volatile boolean stop = false; 
void stop(){ 
  this.stop = true; 
} 
void start(){ 
  new Thread(()->{ while (!stop){ sth } }).start(); 
}


67、java中有几种方法可以实现一个线程?


继承 Thread 类

实现 Runnable 接口

实现 Callable 接口,需要实现的是 call() 方法


68、Java 中的 HashSet,内部是如何工作的?


HashSet 的内部采用 HashMap来实现。由于 Map 需要 key 和 value,所以所有 key 的都有一个默认 value。类似于 HashMap,HashSet 不允许重复的 key,只允许有一个null key,意思就是 HashSet 中只允许存储一个 null 对象。


69、redux的工作流程?


首先,我们看下几个核心概念:


1、 Store:保存数据的地方,你可以把它看成一个容器,整个应用只能有一个Store。


2、 State:Store对象包含所有数据,如果想得到某个时点的数据,就要对Store生成快照,这种时点的数据集合,就叫做State。


3、 Action:State的变化,会导致View的变化。但是,用户接触不到State,只能接触到View。所以,State的变化必须是View导致的。Action就是View发出的通知,表示State应该要发生变化了。


4、 Action Creator:View要发送多少种消息,就会有多少种Action。如果都手写,会很麻烦,所以我们定义一个函数来生成Action,这个函数就叫Action Creator。


5、 Reducer:Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer。Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。


6、 dispatch:是View发出Action的唯一方法。


然后我们过下整个工作流程:


1、 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。


2、 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State


3、 State一旦有变化,Store就会调用监听函数,来更新View。


到这儿为止,一次用户交互流程结束。可以看到,在整个流程中数据都是单向流动的,这种方式保证了流程的清晰。


image.png


70、String类的常用方法有那些?


1、 charAt:返回指定索引处的字符


2、 indexOf():返回指定字符的索引


3、 replace():字符串替换


4、 trim():去除字符串两端空白


5、 split():分割字符串,返回一个分割后的字符串数组


6、 getBytes():返回字符串的byte类型数组


7、 length():返回字符串长度


8、 toLowerCase():将字符串转成小写字母


9、 toUpperCase():将字符串转成大写字符


10、 substring():截取字符串


11、 format():格式化字符串


12、 equals():字符串比较


13、intern(): 返回的值在常量池中


71、当父类引用指向子类对象的时候,子类重写了父类方法和属性,那么当访问属性的时候,访问是谁的属性?调用方法时,调用的是谁的方法?


子类重写了父类方法和属性,访问的是父类的属性,调用的是子类的方法


72、JVM 监控与分析工具你用过哪些?介绍一下。


1、 jps,显示系统所有虚拟机进程信息的命令行工具


2、 jstat,监视分析虚拟机运行状态的命令行工具


3、 jinfo,查看和调整虚拟机参数的命令行工具


4、 jmap,生成虚拟机堆内存转储快照的命令行工具


5、 jhat,显示和分析虚拟机的转储快照文件的命令行工具


6、 jstack,生成虚拟机的线程快照的命令行工具


7、 jcmd,虚拟机诊断工具,JDK 7 提供


8、 jhsdb,基于服务性代理实现的进程外可视化调试工具,JDK 9 提供


9、 JConsole,基于JMX的可视化监视和管理工具


10、 jvisualvm,图形化虚拟机使用情况的分析工具


11、 Java Mission Control,监控和管理 Java 应用程序的工具


1、 MAT,Memory Analyzer Tool,虚拟机内存分析工具


2、 vjtools,唯品会的包含核心类库与问题分析工具


3、 arthas,阿里开源的 Java 诊断工具


4、 greys,JVM进程执行过程中的异常诊断工具


5、 GCHisto,GC 分析工具


6、 GCViewer,GC 日志文件分析工具


7、 GCeasy,在线版 GC 日志文件分析工具


8、 JProfiler,检查、监控、追踪 Java 性能的工具


9、 BTrace,基于动态字节码修改技术(Hotswap)实现的Java程序追踪与分析工具


下面可以重点体验下:


JDK 自带的命令行工具方便快捷,不是特别复杂的问题可以快速定位;


阿里的 arthas 命令行也不错;


可视化工具 MAT、JProfiler 比较强大。


73、Java序列化中如果有些字段不想进行序列化,怎么办?


对于不想进行序列化的变量,使用transient关键字修饰。


transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。


74、进程与线程的区别,进程间如何通讯,线程间如何通讯?


在并发编程领域,有进程和线程两个概念,在Java语言中说起并发编程,常常是指多线程,但是了解进程的概念也非常重要: – 进程是操作系统的资源调度实体,有自己的内存地址空间和运行环境; – 线程一般被称为轻量级的进程,线程和进程一样,也有自己的运行环境,但是创建一个线程要需要的资源比创建一个进程要少。线程存在于进程之中——每个进程至少有一个线程。一个进程下的多个线程之间可以共享进程的资源,包括内存空间和打开的文件。 – 进程跟程序(programs)、应用(applications)具备相同的含义,进程间通讯依靠IPC资源,例如管道(pipes)、套接字(sockets)等; – 线程间通讯依靠JVM提供的API,例如wait方法、notify方法和notifyAll方法,线程间还可以通过共享的主内存来进行值的传递。


75、说明一下public static void main(String args[])这段声明里每个关键字的作用。


public: main方法是Java程序运行时调用的第一个方法,因此它必须对Java环境可见。所以可见性设置为pulic.


static: Java平台调用这个方法时不会创建这个类的一个实例,因此这个方法必须声明为static。


void: main方法没有返回值。


String是命令行传进参数的类型,args是指命令行传进的字符串数组。


76、如果去掉了main方法的static修饰符会怎样?


程序能正常编译。运行时会抛NoSuchMethodError异常。


77、System.out.println()的含义?


System是系统提供的final类;out是PrintStream对象;println是out里一个重载方法。


78、ArrayList如何删除指定元素?


这是一个很麻烦的面试题,很考验对源码的认识和理解,我们来慢慢的剖析一下。

如果使用常规的思想:

public class TestListMain {
    public static void main(String[] args) {
        List<String> result = new ArrayList<>();
        result.add("a");
        result.add("b");
        result.add("c");
        result.add("d");
        for (String s : result) {
            if ("b".equals(s)) {
                result.remove("b");
            }
        }
    }
}


image.png


既然从源代码分析不出来,我们就看下源代码编译后的class文件中的内容是怎样的吧,毕竟class文件才是JVM真正执行的代码,不看不知道,一看吓一跳,JDK原来是这么玩的。原来如此,我们原始代码中的for-each语句,编译后的实际是以迭代器来代替执行的。


public class TestListMain {
    public TestListMain() {
    }
    public static void main(String[] args) {
        List<String> result = new ArrayList();
        result.add("a");
        result.add("b");
        result.add("c");
        result.add("d");
        //创建迭代器
        Iterator var2 = result.iterator();
        while(var2.hasNext()) {
            String s = (String)var2.next();
            if ("b".equals(s)) {
                result.remove("b");
            }
        }
    }
}


通过ArrayList创建的Itr这个内部类迭代器,于是for-each循环就转化成了迭代器加while循环的方式,原来看上去的for-each循环被挂羊头卖狗肉了。


 public Iterator<E> iterator() {

       return new Itr();

   }


Itr这个内部类迭代器,通过判断hasNext()来判断迭代器是否有内容,而next()方法则获取迭代器中的内容。


 private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
        Itr() {}
        public boolean hasNext() {
            return cursor != size;
        }
        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
     ...
 }


image.png

真正抛异常的地方是这个检测方法, 当modCount与expectedModCount不相等的时候直接抛出异常了。那我们要看下modCount以及expectedModCount分别是什么。这里的modCount代表ArrayList的修改次数,而expectedModCount代表的是迭代器的修改次数,在创建Itr迭代器的时候,将modCount赋值给了expectedModCount,因此在本例中一开始modCount和expectedModCount都是4(添加了四次String元素)。但是在获取到b元素之后,ArrayList进行了remove操作,因此modCount就累加为5了。因此在进行检查的时候就出现了不一致,最终导致了异常的产生。到此我们找到了抛异常的原因,循环使用迭代器进行循环,但是操作元素却是使用的ArrayList操作,因此迭代器在循环的时候发现元素被修改了所以抛出异常。


image.png


我们再来思考下,为什么要有这个检测呢?这个异常到底起到什么作用呢?我们先来开下ConcurrentModificationException的注释是怎么描述的。简单理解就是不允许一个线程在修改集合,另一个线程在集合基础之上进行迭代。一旦检测到了这种情况就会通过fast-fail机制,抛出异常,防止后面的不可知状况。


/**
 ***
 * For example, it is not generally permissible for one thread to modify a Collection
 * while another thread is iterating over it.  In general, the results of the
 * iteration are undefined under these circumstances.  Some Iterator
 * implementations (including those of all the general purpose collection implementations
 * provided by the JRE) may choose to throw this exception if this behavior is
 * detected.  Iterators that do this are known as <i>fail-fast</i> iterators,
 * as they fail quickly and cleanly, rather that risking arbitrary,
 * non-deterministic behavior at an undetermined time in the future.
 ***
**/
public class ConcurrentModificationException extends RuntimeException {
    ...
}


回顾整个过程微信图片_20220520133635.png


如何正确的删除

既然抛异常的原因是循环使用了迭代器,而删除使用ArrayList导致检测不通过。那么我们就循环使用迭代器,删除也是用迭代器,这样就可以保证一致了。

public class TestListMain {
    public static void main(String[] args) {
        List<String> result = new ArrayList<>();
        result.add("a");
        result.add("b");
        result.add("c");
        result.add("d");
       Iterator<String> iterator = list.iterator();
    while (iterator .hasNext()) {
      String str = iterator.next();
      if ("b".equals(str)) {
        iterator.remove();
      }
    }
}


79、为啥线程不安全还使用ArrayList呢?


因为在我们正常得使用场景中,都是用来查询的,不会涉及太频繁的增删,如果涉及到频繁的增删,可以使用LinkedList,如果需要线程安全的就是可以使用Vector,CopyOrWriteArrayList


80、1.7和1.8版本初始化的区别


1.7的时候是初始化就创建一个容量为10的数组,1.8后是初始化先创建一个空数组,第一次add时才扩容为10



相关文章
|
3天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
12 0
|
3天前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
6 0
|
5天前
|
Java
三个可能的Java面试题
Java垃圾回收机制自动管理内存,回收无引用对象的内存,确保内存有效利用。多态性允许父类引用操作不同子类对象,如Animal引用可调用Dog的方法。异常处理机制通过try-catch块捕获和处理程序异常,例如尝试执行可能导致ArithmeticException的代码,catch块则负责处理异常。
26 9
|
15天前
|
Java
【JAVA面试题】static的作用是什么?详细介绍
【JAVA面试题】static的作用是什么?详细介绍
|
15天前
|
Java
【JAVA面试题】final关键字的作用有哪些
【JAVA面试题】final关键字的作用有哪些
|
15天前
|
JavaScript 前端开发 Java
【JAVA面试题】什么是引用传递?什么是值传递?
【JAVA面试题】什么是引用传递?什么是值传递?
|
15天前
|
安全 Java
【JAVA面试题】什么是对象锁?什么是类锁?
【JAVA面试题】什么是对象锁?什么是类锁?
|
15天前
|
存储 自然语言处理 Java
【JAVA面试题】什么是代码单元?什么是码点?
【JAVA面试题】什么是代码单元?什么是码点?
|
15天前
|
Java 程序员
【JAVA面试题】基本类型的强制类型转换是否会丢失精度?引用类型的强制类型转换需要注意什么?
【JAVA面试题】基本类型的强制类型转换是否会丢失精度?引用类型的强制类型转换需要注意什么?
|
15天前
|
Java
【JAVA面试题】什么是深拷贝?什么是浅拷贝?
【JAVA面试题】什么是深拷贝?什么是浅拷贝?