面试准备:Java常见面试题汇总(三)

面试准备:Java常见面试题汇总(一)

面试准备:Java常见面试题汇总(二)

面试准备:Java常见面试题汇总(三)

文章目录

83. Java泛型了解么?什么是类型擦除?介绍一下常用的通配符?

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。 Java编译时期生成的字节码的时候会去掉泛型,写入一个特定类型参数。这个过程成为类型擦除。

通配符没啥区别,只不过是编码时的一种约定俗成的东西。
常用的通配符为: T,E,K,V,?

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个java类型
  • K V (key value) 分别代表java键值中的Key Value
  • E (element) 代表Element

其他知识点,参考:Java泛型类型擦除以及类型擦除带来的问题

84. 包装类的常量池技术有了解过吗?

常量池(constant_pool)是在编译期被确定,并被保存在已编译的class文件中的一些数据。除了包含代码中所定义的各种基本类型(如 int、long等)和对象型(如 String 及数组)的常量值外,还包含一些以文本形式出现的符号引用(Class中的常量池中数据会在加载的方式放进方法区中的运行时常量池中)。

Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围会去创建新的对象,否则直接在常量池中拿到对象

比如下面的代码:

	public static void main(String[] args) {
        Integer a = 10;
        Integer b = 10;
        Integer c = 200;
        Integer d = 200;
        System.out.println(a==b);//true
        System.out.println(c==d);//false
		//new 肯定是创建一个新的对象
		Integer i1=new Integer(1);  
	    Integer i2=new Integer(1);  
		//i1,i2分别位于堆中不同的内存空间  
	   System.out.println(i1==i2);//输出false  
    }

会被编译成为:

	public static void main(String[] args) {
        Integer a = Integer.valueOf(10);
        Integer b = Integer.valueOf(10);
        Integer c = Integer.valueOf(200);
        Integer d = Integer.valueOf(200);
        System.out.println(a == b);
        System.out.println(c == d);
    }

其中valueOf()方法的实现如下:

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

85. 在 Java 中定义一个不做事且没有参数的构造方法的作用?*

Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。

86. 成员变量与局部变量的区别有哪些?

  1. 成员变量能由public private protect修饰,而局部变量不行。
  2. 成员变量依附于对象,跟随对象的生命周期,它也存储在堆内存中;
    局部变量依附于方法,跟随方法栈帧的生命周期,存在于栈内存的局部变量表中。
  3. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值,而局部变量则不会自动赋值。

87. 构造方法作用?有哪些特性?*

主要作用是完成对类对象的初始化工作。

特性:

  1. 名字与类名相同。
  2. 没有返回值,但不能用 void 声明构造函数。
  3. 生成类的对象时自动执行,无需调用。

88. 在调用子类构造方法之前会先调用父类无参构造方法,其目的是?*

子类继承了父类的一些属性,调用父类构造方法帮助子类做初始化工作。

89. Object类提供了哪些方法?

public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。

public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。

public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。

public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。

public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念

protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作

90. 获取用键盘输入常用的两种方法?*

方法 1:通过 Scanner

Scanner input = new Scanner(System.in);
String s  = input.nextLine();
input.close();

方法 2:通过 BufferedReader

BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();

91. 使用 try-with-resources?*

需要继承closeable,重写close方法。

public final class Scanner implements Iterator<String>, Closeable {...}
try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

92. 既然有了字节流,为什么还要有字符流?

不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,字符流也是基于字节流封装的,主要作用就是提高开发效率。并且,如果我们不知道编码类型就很容易出现乱码问题(因为字节流在处理时是逐个字节读取,在读取汉字时会出现乱码问题)。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

93. 浮点数怎么比较?*

浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。

float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false

可以使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);// 0.1
BigDecimal y = b.subtract(c);// 0.1
System.out.println(x.equals(y));// true 

参考:Java-BigDecimal

94. 说一说自己对于 synchronized 关键字的了解?*

synchronized关键字解决的是多个线程之间访问资源的互斥和同步。

在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。

JDK1.6对synchronized的实现引入了大量的优化,比如锁升级的方式。

95. 讲一下volatile关键字?

归根结底就是volatile关键字可以让线程的修改立刻通知其他的线程,从而达到数据一致的作用。

参考:Java并发编程实战——彻底理解volatile

96. 简单介绍一下 AtomicInteger 类的原理?

AtomicInteger 线程安全原理简单分析

AtomicInteger 类的部分源码:

    // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;//保存值

	public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

AtomicInteger 类主要利用 CAS 来保障原子性操作,避免 synchronized 的高开销,执行效率大为提升。
具体是使用Unsafe的类来完成的,Unsafe下的方法都是native方法,AtomicInteger类在初始化的时候会使用static静态代码块来获取value值的valueOffset偏移量。这个偏移量就是CAS里的内存值M。
incrementAndGet()方法为例,内部调用了的Unsafe类的getAndAddInt()方法,而Unsafe的getAndAddInt()方法最终是调用了compareAndSwapInt()方法,这个方法就是CAS操作,从内存偏移量取值,比较数据与期望值一致,并修改新值。另外,这个被修改的value值是用volatile关键字修饰的,保证了值的可见性。

	public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
* 
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

97. 简单介绍一下AQS ?

参考:Java并发编程实战——理解AbstractQueuedSynchronizer(AQS)的模版方法模式

AQS是一个用来构建锁和同步器的框架,它的核心思想是判断一个共享资源有没有被占用,如果没有被占用,则将其设置为占用状态;否则实现一套阻塞等待、排队、唤醒的机制。

具体来说,就是通过一个被volatile修饰过的int类型的值和CAS机制来控制同步状态。然后通过模版方法模式来重写AQS提供的方法,比如说共享或者独占的同步方法,也可以去重写等待队列。

一般来说类似ReentrantLock或者CountDownLatch都是实现了一个Sync(继承自AQS)来使用AQS的模版方法的。
在这里插入图片描述

98. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同?

HashSet 是 Set 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;

LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历;内部多了一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于这个双向链表中。

TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。

三个的实现都是用的XXXMap。
LinkedHashMap是HashMap的子类。

99. HashSet 如何检查重复?*

当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

100. 什么是快速失败(fail-fast)?

快速失败(fail-fast) 是 Java 集合的一种错误检测机制。在使用迭代器对集合进行遍历的时候,我们在多线程下操作非安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 ConcurrentModificationException 异常。 另外,在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。

101. 什么是安全失败(fail-safe)呢?

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以,在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常。
参考:面试官:说说快速失败和安全失败是什么

102. 线程池大小如何确定?

如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。

但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

有一个简单并且适用面比较广的公式:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

103. 乐观锁的缺点?

1 ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2 循环时间长开销大
自旋CAS会给CPU带来非常大的执行开销。
如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

AtomicReference中也是使用的CAS来完成的,只不过CAS比较的是对象地址。

104. 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

  1. 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制
  2. 永久代回收的条件比较苛刻,所以容易发生溢出。虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
  3. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
  4. 永久带本身只是HotSpot实现的方法区,在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

105. 描述Java 对象的创建过程?

  • Step1:类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。


  • Step2:分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来


  • Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。


  • Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。


  • Step5:执行 init 方法
    在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

106. Java分配内存的方式?

在创建对象的第二步就是分配对象的内存:

分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。
在这里插入图片描述
在这里插入图片描述

内存分配并发问题
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  1. CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  2. TLAB: 为每一个线程预先在 Eden 区分配一块内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。

107. 简单说明 强/软/弱/虚引用 及其应用?

JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。

JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

  1. 强引用(StrongReference

以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

  1. 软引用(SoftReference)

如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

应用: 软引用可用来实现内存敏感的高速缓存,另外可以用来加速内存回收

3.弱引用(WeakReference)

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象

应用:ThreadLocal使用了弱引用,尽量防止内存泄漏

  1. 虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

应用:虚引用主要用来跟踪对象被垃圾回收的活动。
对于虚引用来说,当其不存在其它引用时,会被gc标记上。有一个高优先级的线程“java.ref.Reference.ReferenceHandler”会处理这些被标记的虚引用(实际上可能不止虚引用,但是虚引用会被这个线程处理),将其加入设定好的引用队列中,也就是所述的“得到一个通知”

DirectBuffer对象本身存储在堆内存中,但是会关联一大片堆外内存,由于堆外内存不会被GC自动回收,因此DirectBuffer对象创建时会关联一个Cleaner对象Cleaner继承了虚引用,在ReferenceHandler中会对Cleaner对象执行短路逻辑,直接执行Cleaner接口的clean()方法(执行绑定的清理函数)而不会入队。当DirectBuffer不再使用,它的虚引用,也就是Cleaner对象,会被ThreadHandler处理,它的clean()方法会回收直接内存。

108. 为什么ThreadLocal要使用弱引用?

ref: Java并发编程实战——并发容器之ThreadLocal及其内存泄漏问题

既然上面的问题我们可以得出,弱引用只要被GC扫描到,那么就会被回收。那么ThreadLocal是如何工作的呢?

我们注意下面这一段代码:

public void ceateThreadLocal(){
	//此时local对创建的对象是强引用
	ThreadLocal<String> local=new ThreadLocal<>();
	//以一个键值对的形式<local,"123">//线程的成员属性存入map
	local.set("123");
	...
}

这一段代码,此时创建的对象ThreadLocal被两个地方引用:

  1. local的强引用
  2. 键值对的弱引用(ThreadLocalMap的键就是ThreadLocal)

由于强引用的存在,那么在栈帧还在Java虚拟机栈里面的时候,ThreadLocal对象是不会被回收的,内存不够了只会抛出OOM错误

而当栈帧一旦出栈,方法就结束了,强引用也消失了,只剩一个弱饮用,那么下一次GC扫描的时候,就会将这个弱引用回收。虽然GC扫描线程优先级低,还是有一定的辅助避免内存泄漏的作用;但是正由于它的优先级低,所以还是存在内存泄漏的风险的。

如果是强引用,那么即使其他地方没有对ThreadLocal对象的引用,ThreadLocalMap中的ThreadLocal对象还是不会被回收,而如果是弱引用则这时候ThreadLocal引用是会被回收掉的,虽然对于的value还是不能被回收,这时候ThreadLocalMap里面就会存在key为null但是value不为null的entry项,即使ThreadLocalMap提供了set,get,方法在一些时机下会对这些Entry项进行清理,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。所以在使用完毕后即使调用remove方法才是解决内存泄露的王道

109. 如何判断一个常量是废弃常量?

假如在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池

110. 如何判断一个类是无用的类?

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

111. 一个类的生命周期?

加载->验证->准备->解析->初始化->使用->卸载。

其中前五个步骤就是类加载的过程,在Java常见面试题汇总(一)说过:问题23:描述Java类加载机制?
最后一个卸载,就是问题110. 如何判断一个类是无用的类?

112. BlockingQueue的实现方式?

参考:BlockingQueue

113. 线程池的实现方式?

Java并发编程实战——线程池ThreadPoolExecutor实现原理

114. 内存泄漏和内存溢出有什么不同?内存泄漏的直接原因是什么?

内存溢出:(out of memory)通俗理解就是内存不够,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。

内存泄漏:(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

  1. 被声明为static的时候,它们的生命周期和应用程序的生命周期一样。
  2. 集合容器中的内存泄露
    我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
  3. JDK6 String substring()存在内存泄漏的原因

对象A引用对象B。A的生命周期(t1 - t4) 比B的生命周期(t2 - t3)长。当在应用程序中B不再被使用,A仍然持有它的引用。在这种情况下,垃圾回收器不能够把B从内存中移除。这就可能造成内存溢出问题。此时,如果B持有大量的其它引用对象也可能发生内存溢出问题。那些被B引用的对象也不会被垃圾回收器回收,所有不再被使用的对象将消耗宝贵的内存空间。

在这里插入图片描述

115. Java进程挂掉的原因可能有哪些?怎么去排查?

  1. linux的OOM killer杀死
    原因:
    Linux 内核有个机制叫OOM killer(Out-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉。
    排查方式:
    去系统报错日志里翻/var/log/messages或者内核日志头里去翻。
    比如egrep -i 'killed process' /var/log/messages或者dmesg | grep java
  2. JVM自身故障
    原因:
    当JVM发生致命错误导致崩溃时,会生成一个hs_err_pid_xxx.log这样的文件,该文件包含了导致 JVM crash 的重要信息,我们可以通过分析该文件定位到导致 JVM Crash 的原因,从而修复保证系统稳定。
    排查方式:
    hs_err_pid_xxx.log文件。
  3. JVM的OOM导致进程退出
    原因:
    一般情况下,出现内存不足的时候,JVM的GC会进行回收,是不会导致JVM进程退出的。一般此时可能发生内存泄漏
    排查方式:
    注意两个个参数-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=*/java.hprof;。然后去对应目录找dump快照文件,接下来借助VisualVM这种可视化工具分析就行。很容易定位问题。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值