Java高频面试题

前言

疫情确诊的人数每天都在增加,离去的人也在增多,这个世界上有很多事我们无能为力也无从选择,日升日落,白昼黑夜,我们能看见白昼中的光芒,我们也能看见黑暗里的流氓。暮色四合,龌龊八开。鲜花还是塑料花,香或臭,当潮水散去,现在即历史,而历史通常是由后人说的。

所以还是上次跟乡亲们说的,我们不要传播未经证实或者不该传播的消息,舆论的力量是我们无法估计的,有些也是我们无法承担的,所以乡亲们也要重视起来,点到即止。无法控制别人,但可以做好自己,帮不了别人,但可以不祸害别人。

我不是个喜欢蹭热度的人,上面那段话乡亲们看看就好,现阶段最重要的就是老老实实待在家里,不聚集,也尽量不出门,自己和家人都要做好安全防护,老百姓经历了太多风风雨雨,相信这次一定也会安然无恙的渡过此劫。那待在家里的这段时间,如果能远程办公的,就做好公司交代的事,无法办公的乡亲们也不要停止学习,因为疫情过去之后,一定会有巨大的变动或者机会来临,而到那时,你准备好了吗?

这几天一直在想,码之初能做点什么?最终决定在这个期间就推出一个面试系列,都是经过我精心整理的,希望能给乡亲们一点帮助。下面进入正题。

高频面试题

1、说说对象的四中引用?

1)强引用只要引用存在,垃圾回收器永远不会回收。

 Object obj = new Object();
User user=new User();

可直接通过obj取得对应的对象 如 obj.equels(new Object()); 而这样 obj 对象对后面 new Object 的一个强 引用,只有当 obj 这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。 

2)软引用 非必须引用,内存溢出之前进行回收,可以通过以下代码实现

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj); 
obj = null;
sf.get();//有时候会返回null

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象 时,则返回null; 软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的 真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。

3)弱引用 第二次垃圾回收时回收,可以通过如下代码实现

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj); 
obj = null;
wf.get();//有时候会返回null 
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时, 将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued 方法返回对象是否被垃圾回收器标记。

ThreadLocal 中有使用到弱引用:

public class ThreadLocal<T> {
 static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
value = v; }
}
//....
}
//.....
}

4)虚引用垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现:

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj); 
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除

虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引 用。虚引用主要用于检测对象是否已经从内存中删除。

2、HashSet是如何保证不重复的?

向 HashSet 中 add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合 equles 方法比较。 

HashSet 中的 add ()方法会使用 HashMap 的 add ()方法。以下是 HashSet 部分源码: 

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
        map = new HashMap<>();
}
public boolean add(E e) {
}

HashMap 的 key 是唯一的,由上面的代码可以看出 HashSet 添加进去的值就是作为 HashMap 的key。所以不会 重复( HashMap 比较key是否相等是先比较 hashcode 在比较 equals )。

3、HashMap是线程安全的吗,为什么不是线程安全的(最好画图说明多线程 环境下不安全)?

不是线程安全的; 

如果有两个线程A和B,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位 置还没有其他的数据。所以这两个线程都会进入我在上面标记为1的代码中。假设一种情况,线程A通过if判断,该 位置没有哈希冲突,进入了if语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程B,线程A停在了if语句 里面,线程B判断该位置没有哈希冲突(线程A的数据还没插入),也进入了if语句,线程B执行完后,轮到线程A执 行,现在线程A直接在该位置插入而不用再判断。这时候,你会发现线程A把线程B插入的数据给覆盖了。发生了线 程不安全情况。本来在 HashMap 中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中,可能 就直接给覆盖了。 

上面所说的是一个图来解释可能更加直观。如下面所示,两个线程在同一个位置添加数据,后面添加的数据就覆盖住了前面添加的。

如果上述插入是插入到链表上,如两个线程都在遍历到最后一个节点,都要在最后添加一个数据,那么后面添加数据的线程就会把前面添加的数据给覆盖住。则

在扩容的时候也可能会导致数据不一致,因为扩容是从一个数组拷贝到另外一个数组。

5、HashMap的扩容过程

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值(知道这个阈字怎么念吗?不念 fa 值,念 yu 值四声)---即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。

扩容( resize )就是重新计算容量,向 HashMap 对象里不停的添加元素,而 HashMap 对象内部的数组无法装载更 多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然 Java 里的数组是无法自动扩容的,方法 是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

 HashMap hashMap=new HashMap(cap);
  • cap =3, hashMap 的容量为4;
  • cap =4, hashMap 的容量为4;
  • cap=5, 的容量为8;
  • cap =9, hashMap 的容量为16;
  • 如果 cap 是2的n次方,则容量为 cap ,否则为大于 cap 的第一个2的n次方的数。

6、Arrays.sort 和 Collections.sort 实现原理和区别?

Collection和Collections区别

java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。

java.util.Collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、 线程安全等操作。然后还有混排(Shuffling)、反转(Reverse)、替换所有的元素(fill)、拷贝(copy)、返 回Collections中最小元素(min)、返回Collections中最大元素(max)、返回指定源列表中最后一次出现指定目 标列表的起始位置( lastIndexOfSubList )、返回指定源列表中第一次出现指定目标列表的起始位置

( IndexOfSubList )、根据指定的距离循环移动指定列表中的元素(Rotate);

事实上Collections.sort方法底层就是调用的array.sort方法,

public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
//void java.util.ComparableTimSort.sort()
static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen)
{
        assert a != null && lo >= 0 && lo <= hi && hi <= a.length;
        int nRemaining  = hi - lo;
        if (nRemaining < 2)
return;  // Arrays of size 0 and 1 are always sorted
   // If array is small, do a "mini-TimSort" with no merges
        if (nRemaining < MIN_MERGE) {
            int initRunLen = countRunAndMakeAscending(a, lo, hi);
            binarySort(a, lo, hi, lo + initRunLen);
            return;
  }
}

legacyMergeSort (a):归并排序 ComparableTimSort.sort() : Timsort 排序

Timsort 排序是结合了合并排序(merge sort)和插入排序(insertion sort)而得出的排序算法。

Timsort的核心过程:

TimSort 算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分 区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这 些 run 序列,每次拿一个 run 出来按规则进行合并。每次合并会将两个 run合并成一个 run。合并的结果保 存到栈中。合并直到消耗掉所有的 run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的run 便是排好序的结果。

综上述过程,Timsort算法的过程包括:

  1. 如何数组长度小于某个值,直接用二分插入排序算法。
  2. 找到各个run,并入栈。
  3. 按规则合并run。

7、Cloneable接口实现原理?

Cloneable接口是Java开发中常用的一个接口, 它的作用是使一个类的实例能够将自身拷贝到另一个新的实例中, 注意,这里所说的“拷贝”拷的是对象实例,而不是类的定义,进一步说,拷贝的是一个类的实例中各字段的值。 

在开发过程中,拷贝实例是常见的一种操作,如果一个类中的字段较多,而我们又采用在客户端中逐字段复制的方 法进行拷贝操作的话,将不可避免的造成客户端代码繁杂冗长,而且也无法对类中的私有成员进行复制,而如果让需要 具备拷贝功能的类实现Cloneable接口,并重写clone()方法,就可以通过调用clone()方法的方式简洁地实现实例 拷贝功

深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在C++语言中,若不弄懂,则会在delete的时候出问 题,但是我们在这幸好用的是Java。虽然Java自动管理对象的回收,但对于深拷贝(深复制)和浅拷贝(浅复制),我们 还是要给予足够的重视,因为有时这两个概念往往会给我们带来不小的困惑。 

浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。深拷 贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。举例来说更加清楚:对象 A1 中包含对 B1 的引 用, B1 中包含对 C1 的引用。浅拷贝 A1 得到 A2 , A2 中依然包含对 B1 的引用, B1 中依然包含对 C1 的引 用。深拷贝则是对浅拷贝的递归,深拷贝 A1 得到 A2 , A2 中包含对 B2 ( B1 的 copy )的引用, B2 中包含 对 C2 ( C1 的 copy )的引用。 

若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝 。

8、简单讲一下异常分类以及处理机制?

Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。Throwable又派生出Error类和Exception类。

错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。

异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用, 是异常处理的核心。

总体上我们根据 Javac 对异常的处理要求,将异常类分为二类。

  • 非检查异常( unckecked exception ): Error 和 RuntimeException 以及他们的子类。javac 在编译时, 不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用catch...finally )这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处 理 。这样的异常发生的原因多半是代码写的有问题。如除0错误 ArithmeticException ,错误的强制类型转换错 误 ClassCastException ,数组索引越界 ArrayIndexOutOfBoundsException ,使用了空对象NullPointerException 等等。
  • 检查异常( checked exception ):除了 Error 和 RuntimeException 的其它异常。javac 强制要求程序员 为这样的异常做预备处理工作(使用 try...catch...finally 或者 throws )。在方法中要么用 try-catch 语句捕 获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因 为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样 的异常时刻准备着。如 SQLException , ClassNotFoundException 等。

需要明确的是:检查和非检查是对于 javac 来说的,这样就很好理解和区分了。

9、wait和sleep的区别?

源码如下:

public class Thread implements Runnable {
public static native void sleep(long millis) throws InterruptedException;
     public static void sleep(long millis, int nanos) throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
        "nanosecond timeout value out of range");
        }
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++; }
        sleep(millis);
    }
//...
}
public class Object {
     public final native void wait(long timeout) throws InterruptedException;
     public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
        if (nanos > 0) {
timeout++; }
        wait(timeout);
    }
//...
}

1、 sleep 来自 Thread 类,和 wait 来自 Object 类。

2、最主要是sleep方法没有释放锁,而wait方法释放了 锁,使得其他线程可以使用同步控制块或者方法。
3、wait,notify和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用(使 用范围)

4、 sleep 必须捕获异常,而 wait , notify 和 notifyAll 不需要捕获异常

  • sleep 方法属于 Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可 运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了 sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在 sleep 的过程中过 程中有可能被其他对象调用它的 interrupt() ,产生 InterruptedException 异常,如果你的程序不捕获这个异 常,线程就会异常终止,进入 TERMINATED 状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语 句块(可能还有 finally 语句块)以及以后的代码。注意 sleep() 方法是一个静态方法,也就是说他只对当前对象有效,通过 t.sleep() 让t对象进入 sleep ,这样 的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程。
  • wait 属于 Object 的成员方法,一旦一个对象调用了wait方法,必须要采用 notify() 和 notifyAll() 方法 唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了 wait() 后,这个线程就会释放它持有的所有 同步资源,而不限于这个被调用了 wait() 方法的对象。wait() 方法也同样会在 wait 的过程中有可能被其他对 象调用 interrupt() 方法而产生 。

10、数组在内存中如何分配?

对于 Java 数组的初始化,有以下两种方式,这也是面试中经常考到的经典题目。

静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度,如:

 //只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为4 
 String[] computers = {"Dell", "Lenovo", "Apple", "Acer"}; //1 
//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为3 
 String[] names = new String[]{"多啦A梦", "大雄", "静香"}; //2

动态初始化:初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配

初始值,如:

 //只是指定了数组的长度,并没有显示的为数组指定初始值,但是系统会默认给数组数组元素分配初始值为null 
 String[] cars = new String[4]; //3

因为 Java 数组变量是引用类型的变量,所以上述几行初始化语句执行后,三个数组在内存中的分配情况如下图所 示:

由上图可知,静态初始化方式,程序员虽然没有指定数组长度,但是系统已经自动帮我们给分配了,而动态初始化 方式,程序员虽然没有显示的指定初始化值,但是因为 Java 数组是引用类型的变量,所以系统也为每个元素分配 了初始化值 null ,当然不同类型的初始化值也是不一样的,假设是基本类型int类型,那么为系统分配的初始化值 也是对应的默认值0。

11、说说Java反射机制?

Java 反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法,对于任意一个对象都能够 调用它的任意一个属性和方法。这种在运行时动态的获取信息以及动态调用对象的方法的功能称为 Java 的反射机 制。 

Class 类与 java.lang.reflect 类库一起对反射的概念进行了支持,该类库包含了 Field,Method,Constructor 类 (每 个类都实现了 Member 接口)。这些类型的对象时由 JVM 在运行时创建的,用以表示未知类里对应的成员。 

这样你就可以使用 Constructor 创建新的对象,用 get() 和 set() 方法读取和修改与 Field 对象关联的字段,用 invoke() 方法调用与 Method 对象关联的方法。另外,还可以调用 getFields() getMethods() 和 getConstructors() 等很便利的方法,以返回表示字段,方法,以及构造器的对象的数组。这样匿名对象的信息 就能在运行时被完全确定下来,而在编译时不需要知道任何事情。 

import java.lang.reflect.Constructor;
public class ReflectTest {
    public static void main(String[] args) throws Exception {
      Class clazz = null;
      clazz = Class.forName("com.jas.reflect.Fruit");
      Constructor<Fruit> constructor1 = clazz.getConstructor();
      Constructor<Fruit> constructor2 = clazz.getConstructor(String.class);
      Fruit fruit1 = constructor1.newInstance();
      Fruit fruit2 = constructor2.newInstance("Apple");
  } 
}
class Fruit{
    public Fruit(){
      System.out.println("无参构造器 Run..........."); }
      public Fruit(String type){
      System.out.println("有参构造器 Run..........." + type);
  } 
}
运行结果: 无参构造器 Run........... 有参构造器 Run...........Apple

12、Java获取反射的三种方法?

  1. 通过new对象实现反射机制。
  2. 通过路径实现反射机制。
  3. 通过类名实现反射机制。

代码示例:

public class Student {
    private int id;
    String name;
    protected boolean sex;
    public float score;
}
public class Get {
  //获取反射机制三种方式
  public static void main(String[] args) throws ClassNotFoundException {
    //方式一(通过建立对象)
    Student stu = new Student();
    Class classobj1 = stu.getClass(); 
    System.out.println(classobj1.getName()); 
    //方式二(所在通过路径-相对路径)
    Class classobj2 = Class.forName("fanshe.Student"); 
    System.out.println(classobj2.getName()); 
    //方式三(通过类名)
    Class classobj3 = Student.class; 
    System.out.println(classobj3.getName());
  }
}

13、String 和 StringBuilder、StringBuffer 的区别?

Java 平台提供了两种类型的字符串:String 和StringBuffer/StringBuilder,它们可以储存和操作字符串。其中 String 是只 读字符串,也就意味着 String 引用的字符串内容是不能被改变的。而StringBuffer/StringBuilder 类表示的字符串对象可以直接进行修改。StringBuilder 是 Java 5 中引入的,它和 StringBuffer 的方法完全相同,区 别在于它是在单线程环境下使用的,因为它的所有方面都没有被 synchronized修饰,因此它的效率也比 StringBuffer 要高。

14、描述一下 JVM 加载 class 文件的原理机制?

JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的 Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。

由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是 一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被 加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件 中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生 与所加载类对应的 Class 对象。加载完成后,Class 对象还不完整,所以此时 的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引 用)三个步骤。最后 JVM 对类进行初始化,包括:1)如果类存在直接的父类并 且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语 句,就依次执行这些初始化语句。

类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展 加载器(Extension)、系统加载器(System)和用户自定义类加载器 (java.lang.ClassLoader 的子类)。从 Java 2(JDK 1.2)开始,类加载过程 采取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机 制中,JVM 自带的 Bootstrap 是根加载器,其他的加载器都有且仅有一个父类 加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子 类加载器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。下面是关 于几个类加载器的说明:

  • Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库 (rt.jar); 
  • Extension:从 java.ext.dirs 系统属性所指定的目录中加载类 库,它的父加载器是 Bootstrap; 
  • System:又叫应用类加载器,其父类是 Extension。它是应用最 广泛的类加载器。它从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中记载类,是用户自定义加载器 的默认父加载器。 

15、阐述 ArrayList、Vector、LinkedList 的存储性能和特性。

ArrayList 和 Vector 都是使用数组方式存储数据,此数组元素数大于实际 存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入 元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector中的方法由于添加了 synchronized 修饰,因此 Vector 是线程安全的容器,但 性能上较 ArrayList 差,因此已经是 Java 中的遗留容器。

LinkedList 使用双 向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一 个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相 比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插 入数据时只需要记录本项的前后项即可,所以插入速度较快。

Vector 属于遗留 容器(Java 早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties 都是遗留容器),已经不推荐使用,但是由于ArrayList 和 LinkedListed 都是非线程安全的,如果遇到多个线程操作同一个 容器的场景,则可以通过工具类 Collections 中的 synchronizedList 方法将其 转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另 一个类的构造器中创建新的对象来增强实现)。

16、 内存模型以及分区,需要详细到每个区放什么?

JVM 分为堆区和栈区,还有方法区,初始化的对象放在堆里面,引用放在栈里面,class 类信息常量池(static 常量和 static 变量)等放在方法区new:

  •   方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字 节码)等数据。
  •   堆:初始化的对象,成员变量 (那种非 static 的变量),所有的对象实例和数组都要 在堆上分配。
  •   栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操 作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所 以还是一个指向地址的指针。
  •   本地方法栈:主要为 Native 方法服务。  程序计数器:记录当前线程执行的行号。

17、java 中垃圾收集的方法有哪些?

1. 标记-清除:这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被 回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不 高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在 分配较大的对象时,由于没有充足的连续内存而提前触发一次 GC 动作。

2. 复制算法:为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只 使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然 后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为8:1:1 三部分,较大那份内存交 Eden 区,其余是两块较小的内存区叫 Survior 区。每次都会优先使用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,然 后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对 象通过分配担保机制复制到老年代中。(java 堆又分为新生代和老年代)

3. 标记-整理该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高 时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回 收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。

4. 分代收集现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生 代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那 么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担 保,所以可以使用标记-整理 或者 标记-清除。

18、简述 java 类加载机制?

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,解析和初始化,最 终形成可以被虚拟机直接使用的 java 类型。

19、类加载器双亲委派模型机制?

当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

20、什么是类加载器,类加载器有哪些?

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。主要有一下四种类加载器:

  • 启动类加载器(Bootstrap ClassLoader)用来加载 java 核心类库,无法被 java 程序直接 引用。
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的 实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH) 来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
  • 用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现。

21、简述 java 内存分配与回收策率以及 Minor GC 和Major GC

1. 对象优先在堆的 Eden 区分配。

2. 大对象直接进入老年代.

3. 长期存活的对象将直接进入老年代。

当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC.Minor Gc 通 常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 Gc 的频率较高, 回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代 GC的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 Minor GC 这样可以加快老年代的回收速度。

分享不易,如果感兴趣的话,可以关注我的公众号“码之初”或者“ma_zhichu”,阅读更多精品技术文章。

猜你喜欢

转载自www.cnblogs.com/mazhichu/p/12294426.html