历尽数月,终于整理出一份让你涨薪8K的Java基础面试题解析

即将到来金三银四人才招聘的高峰期,渴望跳槽的朋友肯定跟我一样四处找以往的面试题,但又感觉找的又不完整,在这里我将把我所见到的题目做一总结,并尽力将答案术语化、标准化。预祝大家面试顺利。

术语会让你的面试更有说服力,让你感觉更踏实,建议大家多记背点术语。

一、简单说下什么是跨平台

术语:操作系统指令集、屏蔽系统之间的差异

由于各种操作系统所支持的指令集不是完全一致,所以在操作系统之上加个虚拟机可以来提供统一接口,屏蔽系统之间的差异。

二、Java有几种基本数据类型

有八种基本数据类型。

各自占用几字节也记一下。

三、面向对象特征

面向对象的编程语言有封装、继承 、抽象、多态等4个主要的特征。

  1. 封装: 把描述一个对象的属性和行为的代码封装在一个模块中,也就是一个类中,属性用变量定义,行为用方法进行定义,方法可以直接访问同一个对象中的属性。

  2. 抽象: 把现实生活中的对象抽象为类。分为过程抽象和数据抽象

  • 数据抽象 -->鸟有翅膀,羽毛等(类的属性)
  • 过程抽象 -->鸟会飞,会叫(类的方法)
  1. 继承:子类继承父类的特征和行为。子类可以有父类的方法,属性(非private)。子类也可以对父类进行扩展,也可以重写父类的方法。缺点就是提高代码之间的耦合性。

  2. 多态: 多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定(比如:向上转型,只有运行才能确定其对象属性)。方法覆盖和重载体现了多态性。

四、为什么要有包装类型

术语:让基本类型也具有对象的特征

为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型)因为容器都是装object的,这是就需要这些基本类型的包装器类了。

自动装箱:new Integer(6);,底层调用:Integer.valueOf(6)

自动拆箱: int i = new Integer(6);,底层调用i.intValue();方法实现。

Integer i  = 6;
Integer j = 6;
System.out.println(i==j);

答案在下面这段代码中找:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

二者的区别

  1. 声明方式不同:基本类型不使用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
  2. 存储方式及位置不同:基本类型是直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后通过引用来使用;
  3. 初始值不同:基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null;
  4. 使用方式不同:基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到。

五、==和equals区别

  • ==较的是两个引用在内存中指向的是不是同一对象(即同一内存空间),也就是说在内存空间中的存储位置是否一致。如果两个对象的引用相同时(指向同一对象时),“==”操作符返回true,否则返回flase。
  • equals用来比较某些特征是否一样。我们平时用的String类等的equals方法都是重写后的,实现比较两个对象的内容是否相等。

我们来看看String重写的equals方法:

它不止判断了内存地址,还增加了字符串是否相同的比较。

public boolean equals(Object anObject) {
    //判断内存地址是否相同
    if (this == anObject) {
        return true;
    }
    // 判断参数类型是否是String类型
    if (anObject instanceof String) {
        // 强转
        String anotherString = (String)anObject;
        int n = value.length;
        // 判断两个字符串长度是否相等
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // 一一比较 字符是否相同
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

六、String、StringBuffer和StringBuilder区别

java中String、StringBuffer、StringBuilder是编程中经常使用的字符串类,他们之间的区别也是经常在面试中会问到的问题。现在总结一下,看看他们的不同与相同。

1. 数据可变和不可变

  • String底层使用一个不可变的字符数组private final char value[];所以它内容不可变。
  • StringBuffer和StringBuilder都继承了AbstractStringBuilder底层使用的是可变字符数组:char[] value;

2. 线程安全

  • StringBuilder是线程不安全的,效率较高;而StringBuffer是线程安全的,效率较低。

通过他们的append()方法来看,StringBuffer是有同步锁,而StringBuilder没有:

@Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
}
@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

3. 相同点

StringBuilderStringBuffer有公共父类AbstractStringBuilder

最后,操作可变字符串速度:StringBuilder > StringBuffer > String,这个答案就显得不足为奇了。

七、讲一下Java中的集合

  • Collection下:List系(有序、元素允许重复)和Set系(无序、元素不重复)

set根据equals和hashcode判断,一个对象要存储在Set中,必须重写equals和hashCode方法

  • Map下:HashMap线程不同步;ConcurrentMap线程同步
  • Collection系列和Map系列:Map是对Collection的补充,两个没什么关系

八、ArrayList和LinkedList区别?

之前专门有写过ArrayList和LinkedList源码的文章。

  • ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
  • 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
  • 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

九、ConcurrentModificationException异常出现的原因

public class Test {
    public static void main(String[] args)  {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==2)
                list.remove(integer);
        }
    }
}

执行上段代码是有问题的,会抛出ConcurrentModificationException异常。

原因:调用list.remove()方法导致modCount和expectedModCount的值不一致。

final void checkForComodification() {
    if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
}

解决办法:在迭代器中如果要删除元素的话,需要调用Iterator类的remove方法。

public class Test {
    public static void main(String[] args)  {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==2)
                iterator.remove();   //注意这个地方
        }
    }
}

十、HashMap和HashTable、ConcurrentHashMap区别?

相同点

  • HashMap和Hashtable都实现了Map接口
  • 都可以存储key-value数据

不同点

  • HashMap可以把null作为key或value,HashTable不可以
  • HashMap线程不安全,效率高。HashTable线程安全,效率低。
  • HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。

什么是fail-fast? 就是最快的时间能把错误抛出而不是让程序执行。

1. 如何保证线程安全又效率高?

Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。

ConcurrentHashMap将整个Map分为N个segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认N为16。

2. 我们能否让HashMap同步?

HashMap可以通过下面的语句进行同步: Map m = Collections.synchronizeMap(hashMap);

十一、拷贝文件的工具类使用字节流还是字符流

答案:字节流

1. 什么是字节流,什么是字符流?

  • 字节流:传递的是字节(二进制),
  • 字符流:传递的是字符

2. 答案

我们并不支持下载的文件有没有包含字节流(图片、影像、音源),所以考虑到通用性,我们会用字节流。

十二、线程创建方式

这个之前自己做过总结,也算比较全面。

1. 方法一:继承Thread类,作为线程对象存在(继承Thread对象)

public class CreatThreadDemo1 extends Thread{
    /**
     * 构造方法: 继承父类方法的Thread(String name);方法
     * @param name
     */
    public CreatThreadDemo1(String name){
        super(name);
    }

    @Override
    public void run() {
        while (!interrupted()){
            System.out.println(getName()+"线程执行了...");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        CreatThreadDemo1 d1 = new CreatThreadDemo1("first");
        CreatThreadDemo1 d2 = new CreatThreadDemo1("second");

        d1.start();
        d2.start();

        d1.interrupt();  //中断第一个线程
    }
}

常规方法,不多做介绍了,interrupted方法,是来判断该线程是否被中断。(终止线程不允许用stop方法,该方法不会施放占用的资源。所以我们在设计程序的时候,要按照中断线程的思维去设计,就像上面的代码一样)。

让线程等待的方法

  • Thread.sleep(200); //线程休息2ms
  • Object.wait(); //让线程进入等待,直到调用Object的notify或者notifyAll时,线程停止休眠

2. 方法二:实现runnable接口,作为线程任务存在

public class CreatThreadDemo2 implements Runnable {
    @Override
    public void run() {
        while (true){
            System.out.println("线程执行了...");
        }
    }

    public static void main(String[] args) {
        //将线程任务传给线程对象
        Thread thread = new Thread(new CreatThreadDemo2());
        //启动线程
        thread.start();
    }
}

Runnable 只是来修饰线程所执行的任务,它不是一个线程对象。想要启动Runnable对象,必须将它放到一个线程对象里。

3. 方法三:匿名内部类创建线程对象

public class CreatThreadDemo3 extends Thread{
    public static void main(String[] args) {
        //创建无参线程对象
        new Thread(){
            @Override
            public void run() {
                System.out.println("线程执行了...");
            }
        }.start();
       //创建带线程任务的线程对象
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程执行了...");
            }
        }).start();
        //创建带线程任务并且重写run方法的线程对象
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("runnable run 线程执行了...");
            }
        }){
            @Override
            public void run() {
                System.out.println("override run 线程执行了...");
            }
        }.start();
    }

}

4. 方法四:创建带返回值的线程

public class CreatThreadDemo4 implements Callable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CreatThreadDemo4 demo4 = new CreatThreadDemo4();

        FutureTask<Integer> task = new FutureTask<Integer>(demo4); //FutureTask最终实现的是runnable接口

        Thread thread = new Thread(task);

        thread.start();

        System.out.println("我可以在这里做点别的业务逻辑...因为FutureTask是提前完成任务");
        //拿出线程执行的返回值
        Integer result = task.get();
        System.out.println("线程中运算的结果为:"+result);
    }

    //重写Callable接口的call方法
    @Override
    public Object call() throws Exception {
        int result = 1;
        System.out.println("业务逻辑计算中...");
        Thread.sleep(3000);
        return result;
    }
}

Callable接口介绍:

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

返回指定泛型的call方法。然后调用FutureTask对象的get方法得道call方法的返回值。

5. 方法五:定时器Timer

public class CreatThreadDemo5 {

    public static void main(String[] args) {
        Timer timer = new Timer();

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时器线程执行了...");
            }
        },0,1000);   //延迟0,周期1s

    }
}

6. 方法六:线程池创建线程

public class CreatThreadDemo6 {
    public static void main(String[] args) {
        //创建一个具有10个线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        long threadpoolUseTime = System.currentTimeMillis();
        for (int i = 0;i<10;i++){
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"线程执行了...");
                }
            });
        }
        long threadpoolUseTime1 = System.currentTimeMillis();
        System.out.println("多线程用时"+(threadpoolUseTime1-threadpoolUseTime));
        //销毁线程池
        threadPool.shutdown();
        threadpoolUseTime = System.currentTimeMillis();
    }

}

7. 方法七:利用java8新特性 stream 实现并发

public class CreatThreadDemo7 {
    public static void main(String[] args) {
        List<Integer> values = Arrays.asList(10,20,30,40);
        //parallel 平行的,并行的
        int result = values.parallelStream().mapToInt(p -> p*2).sum();
        System.out.println(result);
        //怎么证明它是并发处理呢
        values.parallelStream().forEach(p-> System.out.println(p));
    }
}
200
40
10
20
30

怎么证明它是并发处理呢,他们并不是按照顺序输出的 。

十三、两个对象的hashCode相同,则equals也一定为true,对吗?

不对,答案见下面的代码:

@Override
public int hashCode() {
    return 1;
}

两个对象equals为true,则hashCode也一定相同,对吗?

这块肯定是有争议的。面试的时候这样答:如果按照官方设计要求来打代码的话,hashcode一定相等。但是如果不按官方照设计要求、不重写hashcode方法,就会出现不相等的情况。

十四、Java线程池用过没有?

Executors提供了四种方法来创建线程池。

  • newFixedThreadPool() :创建固定大小的线程池。
  • newCachedThreadPool(): 创建无限大小的线程池,线程池中线程数量不固定,可根据需求自动更改。
  • newSingleThreadPool() : 创建单个线程池,线程池中只有一个线程。
  • newScheduledThreadPool() 创建固定大小的线程池,可以延迟或定时的执行任务。

手写一个:

public static void main(String[] args) {

    ExecutorService threadPool = Executors.newCachedThreadPool();
    threadPool.execute(() -> {
        for (int i = 0; i< 20;i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    });
    threadPool.shutdown();
}

线程池作用

  • 限制线程个数,避免线程过多导致系统运行缓慢或崩溃。
  • 不需要频繁的创建和销毁,节约资源、响应更快。

十五、Math.round(-2.5)等于多少?

不要认为它是四舍五入!不要认为它是四舍五入!不要认为它是四舍五入!

口诀:+0.5后向下取整。所以结果是-2。

留个题,Math.round(-2.6)结果和Math.round(2.6)结果

十六、面向对象六大原则

  1. 单一职责原则——SRP:让每个类只专心处理自己的方法。
  2. 开闭原则——OCP:软件中的对象(类,模块,函数等)应该对于扩展是开放的,但是对于修改是关闭的。
  3. 里式替换原则——LSP:子类可以去扩展父类,但是不能改变父类原有的功能。
  4. 依赖倒置原则——DIP:应该通过调用接口或抽象类(比较高层),而不是调用实现类(细节)。
  5. 接口隔离原则——ISP:把接口分成满足依赖关系的最小接口,实现类中不能有不需要的方法。
  6. 迪米特原则——LOD:高内聚,低耦合。

十七、Static和Final区别

十八、String s = "hello"和String s = new String("hello");区别

String s = new String("hello");可能创建两个对象也可能创建一个对象。如果常量池中有hello字符串常量的话,则仅仅在堆中创建一个对象。如果常量池中没有hello对象,则堆上和常量池都需要创建。

String s = "hello"这样创建的对象,JVM会直接检查字符串常量池是否已有"hello"字符串对象,如没有,就分配一个内存存放"hello",如有了,则直接将字符串常量池中的地址返回给栈。(没有new,没有堆的操作)

十九、引用类型是占用几个字节?

hotspot在64位平台上,占8个字节,在32位平台上占4个字节。

二十、(1<3)?"a":"b")+3+4(1<3)?"a":"b")+(3+4)区别

System.out.println(((1<3)?"a":"b")+3+4);
System.out.println(((1<3)?"a":"b")+(3+4));

控制台:

a34
a7

1. 什么情况下,加号会变成字符串连接符

依据上面的例子来思考。

二十一、Java中的switch选择结构可以使用数据类型的数据(JDK1.8)

  • char
  • byte
  • short
  • int
  • Character
  • Byte
  • Short
  • Integer
  • String
  • enum

更好的记忆方法:

基本类型中,没有boolean和浮点类型+长类型long.相应的包装类型也没有。

外加String和enum。

二十二、4&5``4^5``4&10>>1各等于多少

// 0100 & 0101 = 0100 = 4
System.out.println(4&5);
// 0100 ^ 0101 = 0001 = 1
System.out.println(4^5);
System.out.println(10>>1);
 // 有疑问参考下面的运算符优先级
System.out.println(4&10>>1);
4
1
5
4

4|5等于多少呢

答案:5

运算符优先级

二十三、某些java类为什么要实现Serializable接口

为了网络进行传输或者持久化

什么是序列化

将对象的状态信息转换为可以存储或传输的形式的过程

除了实现Serializable接口还有什么序列化方式

  • Json序列化
  • FastJson序列化
  • ProtoBuff序列化 ...

二十四、JVM垃圾处理方法

标记-清除算法(老年代)

该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象.

该算法会有两个问题:

  • 效率问题:标记和清除效率不高。
  • 空间问题:标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。

所以它一般用于"垃圾不太多的区域,比如老年代"。

复制算法(新生代)

该算法的核心是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象(非垃圾)复制到另外一块上面, 然后把已使用过的内存空间一次清理掉.

优点:不用考虑碎片问题,方法简单高效。 缺点:内存浪费严重。

现代商用VM的新生代均采用复制算法, 但由于新生代中的98%的对象都是生存周期极短的, 因此并不需完全按照1∶1的比例划分新生代空间, 而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor. 当发生MinorGC时, 将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor上, 最后清理掉Eden和刚才用过的Survivor的空间. 当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖老年代进行空间分配担保机制, 这部分内存直接进入老年代。

复制算法的空间分配担保: 在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间). 然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间).

标记-整理算法(老年代)

标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有了标记整理算法. 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存.

二十五、新生代、老年代、持久代都存储哪些东西

新生代

  • 方法中new一个对象,就会先进入新生代。

老年代

  • 新生代中经历了N次垃圾回收仍然存活的对象就会被放到老年代中。
  • 大对象一般直接放入老年代。
  • 当Survivor空间不足。需要老年代担保一些空间,也会将对象放入老年代。

永久代

  • 指的就是方法区。

二十六、可达性算法中,哪些对象可作为GC Roots对象。

  • 虚拟机栈中引用的对象
  • 方法区静态成员引用的对象
  • 方法区常量引用对象
  • 本地方法栈JNI引用的对象

二十七、什么时候进行MinGC和FullGC

MinGC

  • 当Eden区满时,触发Minor GC.

FullGC

  • 调用System.gc时,系统建议执行Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的剩余空间
  • 堆中分配很大的对象,而老年代没有足够的空间

二十八、如何判定对象为垃圾对象

在堆里面存放着Java世界中几乎所有的对象实例, 垃圾收集器在对堆进行回收前, 第一件事就是判断哪些对象已死(可回收).

引用计数法

在JDK1.2之前,使用的是引用计数器算法。 在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,计数器的值就-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!

问题:如果在A类中调用B类的方法,B类中调用A类的方法,这样当其他所有的引用都消失了之后,A和B还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。但是该算法并不会计算出该类型的垃圾。

可达性分析法

在主流商用语言(如Java、C#)的主流实现中, 都是通过可达性分析算法来判定对象是否存活的: 通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图:虽然E和F相互关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。

: 即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相连接的引用链, 第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过).

二十九、你能说出来几个垃圾收集器

1. Serial

Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它在进行垃圾收集时,会暂停所有的工作进程,用一个线程去完成GC工作

特点:简单高效,适合jvm管理内存不大的情况(十兆到百兆)。

2. Parnew

ParNew收集器其实是Serial的多线程版本,回收策略完全一样,但是他们又有着不同。

我们说了Parnew是多线程gc收集,所以它配合多核心的cpu效果更好,如果是一个cpu,他俩效果就差不多。(可用-XX:ParallelGCThreads参数控制GC线程数)

3. Cms

CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上的并发收集器, 虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如Taobao),又称多并发低暂停的收集器。

由他的英文组成可以看出,它是基于标记-清除算法实现的。整个过程分4个步骤:

  • 初始标记(CMS initial mark):仅只标记一下GC Roots能直接关联到的对象, 速度很快
  • 并发标记(CMS concurrent mark: GC Roots Tracing过程)
  • 重新标记(CMS remark):修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
  • 并发清除(CMS concurrent sweep: 已死对象将会就地释放)

可以看到,初始标记、重新标记需要STW(stop the world 即:挂起用户线程)操作。因为最耗时的操作是并发标记和并发清除。所以总体上我们认为CMS的GC与用户线程是并发运行的。

优点:并发收集、低停顿

缺点

  • CMS默认启动的回收线程数=(CPU数目+3)*4 当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低.
  • 无法清除浮动垃圾(GC运行到并发清除阶段时用户线程产生的垃圾),因为用户线程是需要内存的,如果浮动垃圾施放不及时,很可能就造成内存溢出,所以CMS不能像别的垃圾收集器那样等老年代几乎满了才触发,CMS提供了参数-XX:CMSInitiatingOccupancyFraction来设置GC触发百分比(1.6后默认92%),当然我们还得设置启用该策略-XX:+UseCMSInitiatingOccupancyOnly
  • 因为CMS采用标记-清除算法,所以可能会带来很多的碎片,如果碎片太多没有清理,jvm会因为无法分配大对象内存而触发GC,因此CMS提供了-XX:+UseCMSCompactAtFullCollection参数,它会在GC执行完后接着进行碎片整理,但是又会有个问题,碎片整理不能并发,所以必须单线程去处理,所以如果每次GC完都整理用户线程stop的时间累积会很长,所以XX:CMSFullGCsBeforeCompaction参数设置隔几次GC进行一次碎片整理(默认为0)。

4. G1

同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。

因为每个区都有E、S、O代,所以在G1中,不需要对整个Eden等代进行回收,而是寻找可回收对象比较多的区,然后进行回收(虽然也需要STW操作,但是花费的时间是很少的),保证高效率。

新生代收集

G1的新生代收集跟ParNew类似,如果存活时间超过某个阈值,就会被转移到S/O区。

年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域的大小

老年代收集

分为以下几个阶段:

  • 初始标记 (Initial Mark: Stop the World Event) 在G1中, 该操作附着一次年轻代GC, 以标记Survivor中有可能引用到老年代对象的Regions.
  • 扫描根区域 (Root Region Scanning: 与应用程序并发执行) 扫描Survivor中能够引用到老年代的references. 但必须在Minor GC触发前执行完
  • 并发标记 (Concurrent Marking : 与应用程序并发执行) 在整个堆中查找存活对象, 但该阶段可能会被Minor GC中断
  • 重新标记 (Remark : Stop the World Event) 完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除并回收, 并计算所有区域的活跃度).
  • 清理 (Cleanup : Stop the World Event and Concurrent) 在含有存活对象和完全空闲的区域上进行统计(STW)、擦除Remembered Sets(使用Remembered Set来避免扫描全堆,每个区都有对应一个Set用来记录引用信息、读写操作记录)(STW)、重置空regions并将他们返还给空闲列表(free list)(Concurrent)

三十、JVM中对象的创建过程

1. 拿到内存创建指令

当虚拟机遇到内存创建的指令的时候(new 类名),来到了方法区,找 根据new的参数在常量池中定位一个类的符号引用。

2. 检查符号引用

检查该符号引用有没有被加载、解析和初始化过,如果没有则执行类加载过程,否则直接准备为新的对象分配内存

3. 分配内存

虚拟机为对象分配内存(堆)分配内存分为指针碰撞和空闲列表两种方式;分配内存还要要保证并发安全,有两种方式。

3.1. 指针碰撞

所有的存储空间分为两部分,一部分是空闲,一部分是占用,需要分配空间的时候,只需要计算指针移动的长度即可。

3.2. 空闲列表

虚拟机维护了一个空闲列表,需要分配空间的时候去查该空闲列表进行分配并对空闲列表做更新。

可以看出,内存分配方式是由java堆是否规整决定的,java堆的规整是由垃圾回收机制来决定的

3.2.1 安全性问题的思考

假如分配内存策略是指针碰撞,如果在高并发情况下,多个对象需要分配内存,如果不做处理,肯定会出现线程安全问题,导致一些对象分配不到空间等。

3.3 线程同步策略

也就是每个线程都进行同步,防止出现线程安全。

3.4. 本地线程分配缓冲

也称TLAB(Thread Local Allocation Buffer),在堆中为每一个线程分配一小块独立的内存,这样以来就不存并发问题了,Java 层面与之对应的是 ThreadLocal 类的实现

4. 初始化

分配完内存后要对对象的头(Object Header)进行初始化,这新信息包括:该对象对应类的元数据、该对象的GC代、对象的哈希码。
抽象数据类型默认初始化为null,基本数据类型为0,布尔为false。。。

5. 调用对象的初始化方法

也就是执行构造方法。

三十一、谈谈对象的访问定位

对象创建起来之后,就会在虚拟机栈中维护一个本地变量表,用于存储基础类型和基础类型的值,引用类型与引用类型的值。 其中引用类型的值就是堆中对象地址。如何引用堆中地址有两种方式:

  • 句柄:在堆中维护一个句柄池,句柄中包含了对象地址,当对象改变的时候,只需改变句柄,不需要改变栈中本地变量表的引用
  • 直接指针:对象的地址直接存储在栈中,这样做的好处就是访问速度变快(Hotspot采用该方式)

三十二、JVM将内存主要划分为哪五部分

方法区、虚拟机栈、本地方法栈、堆、程序计数器。

三十三、String的intern()函数作用

这个要分版本来回答:

  • 如果是JDK6,如果字符串产量池先前已经创建该对象,则返回引用;否则将其添加到字符串常量池并返回引用。
  • 如果是JDK6+,若字符串常量池有则返回引用,如果池中没有堆中有,则将堆中的引用添加到池中(注意是引用),然后返回引用;若池中也没有,则在池中创建并返回引用。

三十四、本地方法栈和虚拟机栈区别

本地方法栈与虚拟机栈所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。

三十五、分配堆内存指令

-Xms -Xmx

前者是堆的初始值,后者是堆能达到的最大值。

三十六、程序计数器作用

记录当前线程锁执行的字节码的行号。

  • 程序计数器是一块较小的内存空间。
  • 处于线程独占区。
  • 执行java方法时,它记录正在执行的虚拟机字节码指令地址。执行native方法,它的值为undefined
  • 该区域是唯一一个没有规定任何OutOfMemoryError的区域

三十七、如何将字符串反转?

  • 通过 charAt(int index)返回char值进行字符串拼接
  • 调用StringBuffer中的reverse方法

三十八、Collection 和 Collections 有什么区别?

Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。

Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

三十九、在 Queue 中 poll()和 remove()有什么区别?

  • queue的增加元素方法add和offer的区别在于,add方法在队列满的情况下将选择抛异常的方法来表示队列已经满了,而offer方法通过返回false表示队列已经满了;在有限队列的情况,使用offer方法优于add方法;
  • remove方法和poll方法都是删除队列的头元素,remove方法在队列为空的情况下将抛异常,而poll方法将返回null;
  • element和peek方法都是返回队列的头元素,但是不删除头元素,区别在与element方法在队列为空的情况下,将抛异常,而peek方法将返回null.

四十、什么是迭代器

Iterator接口提供了很多对集合元素进行迭代的方法。每一个集合类都包括了可以返回迭代器实例的迭代方法。迭代器可以在迭代过程中删除底层集合的元素,但是不可以直接调用集合的remove(Object obj)删除,可以通过迭代器的remove()方法删除

四十一、迭代器的优点

如果用的是for循环,就用集合自带的remove(),而这样就改变了集合的Size()循环的时候会出错。但如果把集合放入迭代器,既iterator迭代可以遍历并选择集合中的每个对象而不改变集合的结构,而把集合放入迭代器,用迭代器的remove()就不会出现问题

四十二、Java集合类中的Iterator和ListIterator的区别

对List来说,你也可以通过listIterator()取得其迭代器,两种迭代器在有些时候是不能通用的,Iterator和ListIterator主要区别在以下方面:

  • iterator()方法在set和list接口中都有定义,但是ListIterator()仅存在于list接口中(或实现类中);
  • ListIterator有add()方法,可以向List中添加对象,而Iterator不能
  • ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
  • ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
  • 都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。

四十三、怎么确保一个集合不能被修改?

  • Java中提供final关键字,对基本类型进行修饰,当第一次初始化后,该变量就不可被修改
  • Collections工具类中的UnmodifiableList(不可修改的List、Map、Set等)

四十四、并行和并发区别

并发的关键是你有处理多个任务的能力,不一定要同时。

四十五、说一下你对Daemon线程(守护线程)的理解?它有什么意义?一般应用于什么样的场景?

所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

守护线程和用户线程的没啥本质的区别:唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

四十六、sleep() 和 wait() 有什么区别?

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

四十七、notify 和 notifyAll 区别

notify 仅仅通知一个线程,并且我们不知道哪个线程会收到通知,然而 notifyAll 会通知所有等待中的线程。换言之,如果只有一个线程在等待一个信号灯,notify和notifyAll都会通知到这个线程。但如果多个线程在等待这个信号灯,那么notify只会通知到其中一个,而其它线程并不会收到任何通知,而notifyAll会唤醒所有等待中的线程。

四十八、线程中start()和run()的区别

  • 每个线程都有要执行的任务。线程的任务处理逻辑可以在Tread类的run实例方法中直接实现或通过该方法进行调用,因此run()相当于线程的任务处理逻辑的入口方法,它由Java虚拟机在运行相应线程时直接调用,而不是由应用代码进行调用。
  • 而start()的作用是启动相应的线程。启动一个线程实际是请求Java虚拟机运行相应的线程,而这个线程何时能够运行是由线程调度器决定的。start()调用结束并不表示相应线程已经开始运行,这个线程可能稍后运行,也可能永远也不会运行。

四十九、线程池的五种状态

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

五十、线程池中 submit()和 execute()方法有什么区别?

  • execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。——实现Runnable接口
  • submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。——实现Callable接口

# 链接 Java程序员福利"常用资料分享"

推荐阅读更多精彩内容

  • 《深入理解Java虚拟机》笔记_第一遍 先取看完这本书(JVM)后必须掌握的部分。 第一部分 走近 Java 从传...
    xiaogmail阅读 2,246评论 1 29
  • JVM架构 当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的...
    cocohaifang阅读 851评论 0 7
  • 介绍JVM中7个区域,然后把每个区域可能造成内存的溢出的情况说明 程序计数器:看做当前线程所执行的字节码行号指示器...
    jemmm阅读 1,321评论 0 9
  • HostSpot虚拟机运行时内存 程序计数器——当前线程执行字节码的行号指示器,如果执行Native方法,则计数器...
    Mars_M阅读 397评论 0 3
  • Java和C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进来,墙里面的人想出来。 对象...
    胡二囧阅读 244评论 0 4