JVM——JVM面试题笔记

1. Java的4种引用类型 / 内存泄漏问题:强软弱虚

  1. 强引用
Object o = new Object();

特点:

  • 强引用可以直接访问目标对象
  • 强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常,也不会回收强引用所指向的对象
  • 强引用可能导致内存泄漏。
  1. 软引用

可以通过java.lang.ref.SoftReference使用软引用。一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。当堆使用率临近阈值时,才会去回收软引用的对象

适用于缓存

此时是可以通过get()方法拿到m,打印结果:[B@15db9742

public class T02_SoftReference {
	public static void main(String[] args) throws IOException {
		//在堆内存中有10m的字节数组
		SoftReference<byte[]> m=new SoftReference<>(new byte[1024*1024*10]);		
		System.out.println(m.get());		
		System.gc();
	}
}

假定sleep 500ms,m会被垃圾回收吗?打印结果:
[B@15db9742
[B@15db9742

public class T02_SoftReference {
	public static void main(String[] args) throws IOException {
		//在堆内存中有10m的字节数组
		SoftReference<byte[]> m=new SoftReference<>(new byte[1024*1024*10]);
		System.out.println(m.get());//[B@15db9742  [B代表字节数组,[是数组,B是字节
		System.gc();
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(m.get());
	}
}

修改JVM参数:-Xmx20M(最大堆内存20M),打印结果:
[B@15db9742
[B@15db9742
null

public class T02_SoftReference {
	public static void main(String[] args) throws IOException {
		//在堆内存中有10m的字节数组
		SoftReference<byte[]> m=new SoftReference<>(new byte[1024*1024*10]);
		System.out.println(m.get());//[B@15db9742  [B代表字节数组,[是数组,B是字节
		System.gc();
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(m.get());
		
		//修改堆空间大小为20m
		//此时heap装不下,系统会先垃圾回收一次,如果不够,会回收软引用
		byte[] b=new byte[1024*1024*15];//15m
		System.out.println(m.get());
	}
}
  1. 弱引用

只要发生垃圾回收,就会被回收,使用:WeakHashMap

一文搞懂WeakHashMap

WeakReference<String> m = new WeakReference<String>(new String("I'm here"));
System.out.println(m.get());//输出为String对象:I'm here
System.gc();
System.out.println(m.get());//输出为null
  1. 虚引用

get()不到虚引用指向的对象,在任何时候都可能被垃圾回收

  • 作用:管理堆外内存

有对象指向的是堆外内存,为了让JVM管理它,使用虚引用指向这个对象。当它被回收,首先回收虚引用,再回收对象指向的堆外内存

  • 虚引用必须和引用队列 (ReferenceQueue)联合使用,弱引用和软引用可以使用引用队列
public class T04_PhantomReference {	
	private static final List<Object> LIST=new LinkedList<>();	
	//引用队列,当检测到对象的可到达性更改时,垃圾回收器将已注册的引用对象添加到队列中
	//队列相当于信号灯的作用,告诉你是否有虚引用对象要被垃圾回收了
	private static final ReferenceQueue<M> QUEUE=new ReferenceQueue<M>();	
	public static void main(String[] args) {
		//创建引用对象,并关联引用队列,被回收就会被添加到队列中
		PhantomReference<M> phantomReference = new PhantomReference<>(new M(),QUEUE);
		System.out.println(phantomReference.get());		
		new Thread(()-> {
			while(true) {
				LIST.add(new byte[1024*1024]);//设定堆内存大小10M,空间满了,就会产生垃圾回收
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
					Thread.currentThread().interrupt();
				}		
				System.out.println(phantomReference.get());//get不到虚引用的对象				
			}
		}).start();		
		//怎么知道虚引用被垃圾回收?
		//需要自己不断观察回收队列,如果不为空,对虚引用的对象进行特殊处理
		//——回收这个引用指向的直接内存
		new Thread(()-> {
			while(true) {
				Reference<? extends M> poll=QUEUE.poll();//看引用队列是否有东西
				if(poll!=null) {
					System.out.println("虚引用对象被回收了..."+poll);
				}
			}
		}).start();		
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

2. ThreadLocal

场景:
原始方法:有一局部变量X,执行了set(X)的方法
原始方法经过好几层传递,调用其他方法,最后一个调用的方法想拿到X的值

方法1:把X层层传递,传到最后一个方法里
问题:如果多个方法中包含已经封装好的类库,传递X失败

方法2:将X设置成全局变量,static
问题:多线程访问不安全

方法3:将X放在ThreadLocal
说明:

  1. 拿到当前线程独有的ThreadLocalMap map
  2. set方法是将value放在map里
  3. map的key是this,this是在方法中new ThreadLocal<>()对象

详细说明:都说ThreadLocal被面试官问烂了,可为什么面试官还是喜欢继续问

  • set方法
public class ThreadLocalTest{
	private static ThreadLocal<Person> local=new ThreadLocal<>();
	
	private static class Person{
		String name="abc";
	}
	
	//结果 线程1get结果为null --->  线程私有的
	public static void main(String[] args) {
		new Thread(()-> {
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			local.set(new Person());
		}).start();
	}
}

源码分析:ThreadLocal.set()

//ThreadLocal.class
public void set(T value) {
    Thread t = Thread.currentThread();//注意!拿到当前线程
    ThreadLocalMap map = getMap(t);//拿到当前线程的ThreadLocalMap 
    if (map != null)         //存在
        map.set(this, value);//this:当前ThreadLocal实例,弱引用
    else					 //不存在
        createMap(t, value); //创建map
}

getMap()

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// Thread类中声明的threadLocals变量
ThreadLocal.ThreadLocalMap threadLocals = null;

map.set()方法如何解决哈希冲突

private void set(ThreadLocal<?> key, Object value) {
  	Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);//确定插入位置

    for (Entry e = tab[i];
        e != null;
        //否则,找到下一个位置:nextIndex
        e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

		//当前位置存在Entry对象,刷新Entry对象中的value
        if (k == key) {
            e.value = value;
            return;
        }
		
		//如果当前位置为空,初始化一个Entry对象
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocalMap结构:Entry[],使用数组而没有使用链表

//ThreadLocal$ThreadLocalMap.class
static class ThreadLocalMap {
	static class Entry extends WeakReference<ThreadLocal<?>> {
	    Object value;
	    Entry(ThreadLocal<?> k, Object v) {
	        super(k);
	        value = v;
	    }
	}
	private static final int INITIAL_CAPACITY = 16;
	private Entry[] table;
	...
}
  • 父子线程间数据共享问题:inheritableThreadLocals
public class Thread implements Runnable{
	//
	ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
	//线程初始化
	private void init(...) {
		...
		//如果当前线程inheritThreadLocals不为空,父线程的inheritThreadLocals也不为空
		//就把父线程的inheritThreadLocals传给子线程
		//在子线程中就可以通过get方法获取到主线程set方法设置的值了
		if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);     
	}
}
  • 内存泄露问题
    • 原因:如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。
    • 解决:
      • 每次使用完ThreadLocal都调用它的remove()方法清除数据
      • 按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

3. 解释一下对象的创建过程(半初始化)

//T:方法区找类信息
//t:局部变量存放在栈上
//new T():对象存放在堆中
//t-->new T():默认是强引用
T t = new T();
  • new:申请内存空间,分配内存,此时m=0(赋值int的默认值0)
  • invokespecial,调用T的init初始化方法(构造方法),m=8(赋初始值)
  • astore_1:将t与new 出来对象建立构造关联
  • 详细信息,查看:java对象的创建过程

在这里插入图片描述

4. DCL单例到底需不需要volatile?(指令重排)

一开始就new出来对象,浪费内存

class T01{
	private static final T01 INSTANCE=new T01();
	private T01(){ }
	public static T01 getInstance() { return INSTANCE;}
}

线程不安全

class T02{
	private static T02 INSTANCE;
	private T02(){ }
	public static T02 getInstance() {
		if(INSTANCE==null){
			try {
				TimeUnit.MILLISECONDS.sleep(10);
			} catch (InterruptedException e) {		
				e.printStackTrace();
			}
			INSTANCE=new T02();
		} 
		return INSTANCE;
	}
}

锁的粒度太大,有的方法可能不需要加锁

class T03{
	private static T03 INSTANCE;
	private T03(){ }
	public static synchronized T03 getInstance() {
		if(INSTANCE==null){
			try {
				TimeUnit.MILLISECONDS.sleep(10);
			} catch (InterruptedException e) {		
				e.printStackTrace();
			}
			INSTANCE=new T03();
		} 
		return INSTANCE;
	}
}

不能保证线程安全,if(INSTANCE==null)不能保证

class T04{
	private static T04 INSTANCE;
	private T04(){ }
	public static T04 getInstance() {
		if(INSTANCE==null){
			synchronized (T04.class){
				try {
					TimeUnit.MILLISECONDS.sleep(10);
				} catch (InterruptedException e) {		
					e.printStackTrace();
				}
				INSTANCE=new T04();
			}
		} 
		return INSTANCE;
	}
}

双重锁定检查(Double Check Lock),看似保证线程安全

class T05{
	private static T05 INSTANCE;
	private T05(){ }
	public static T05 getInstance() {
		if(INSTANCE==null){
			synchronized (T05.class){
				if(INSTANCE==null){
					try {
						TimeUnit.MILLISECONDS.sleep(10);
					} catch (InterruptedException e) {		
						e.printStackTrace();
					}
					INSTANCE=new T05();
				}
			}
		} 
		return INSTANCE;
	}
}

原有指令顺序:

  • invokespecial #3 <T.>调用构造方法赋初始值
  • astore_1 建立构造关联

但是,如果发生指令重排,变成

  • astore_1 建立构造关联
  • invokespecial #3 <T.>调用构造方法赋初始值

此时就指向了半初始化的对象,尽管DCL会先判断对象是否为null,但另一个线程此时会使用的是半初始化状态的对象(m=0)

在这里插入图片描述

5. 对象在内存中的存储布局(对象与数组的存储不同)

普通对象:new XX()

  • 对象头:Header
    • Mark Word:用于存储对象自身的运行时数据
    • 类型指针:Class Pointer
  • 实例数据:Instance Data
  • 对齐填充:Padding

Mark Word 存储哈希码、锁状态标志、线程持有的锁、偏向线程ID等信息,在64位系统中默认(不考虑压缩)占64bit(64位)=8字节,32位占4字节

Class Pointer默认占4个字节(压缩后)

Instance Data取决于对象中实例变量的类型

对齐区域的大小取决于前三项大小。64位JVM虚拟机要求对象占内存中的空间为8的倍数,比如前3项占12个字节,对齐区域应为4字节

//new T对象占多少字节?
//Mark Word=8 + Class Pointer=4 + int=4 + String=4 = 20 不是8倍数
//20 + Padding=4 = 24 实现对齐
public class T{
	volatile int i;
	String s;
}
T t=new T();

6. 对象头具体包括什么(markword、 classpointer、synchronized锁信息)

markword主要包含synchronized锁信息、hashcode信息、GC信息

锁升级:

  • 无锁状态
  • 偏向锁

为什么引入偏向锁:大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价

原理:当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,比较当前线程threadID和Java对象头中的threadID结果一致,该线程可以直接进入同步块

升级:一旦有第二条线程需要竞争锁,那么偏向模式立即结束,进入轻量级锁的状态

  • 自旋锁 / 无锁 / 轻量级锁 / lock free

轻量级锁是一种乐观锁,它认为锁存在竞争的概率比较小,所以它不使用互斥同步,而是使用CAS操作来获得锁,这样能减少互斥同步所使用的『互斥量』带来的性能开销。自旋发生在cpu里。

当线程请求锁时,若该锁对象的Mark Word中标志位为01(未锁定状态),则在该线程的栈帧中创建一块名为『锁记录』的空间,然后将锁对象的Mark Word拷贝至该空间;最后通过CAS操作将锁对象的Mark Word指向该锁记录

若CAS操作成功,则轻量级锁的上锁过程成功,并且对象Mark Word的锁标志位设置为00,即表示此对象处于轻量级锁定状态

若CAS操作失败,再判断当前线程是否已经持有了该轻量级锁;若已经持有,则直接进入同步块;若尚未持有,则表示该锁已经被其他线程占用,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,此时轻量级锁就要膨胀成重量级锁。

  • 重量级锁

重量级锁也就是通常说synchronized的对象锁,锁标识位为10,此时是向操作系统申请锁

在这里插入图片描述

7. 对象怎么定位(直接、间接)

  • 使用句柄访问

Java堆中划分出一块内存来当做句柄池,句柄中包含了对象实例数据与类型数据各自的具体地址信息。

  • 使用直接指针访问:reference中存储的直接就是对象地址。

在这里插入图片描述

8. 对象怎么分配

  • 栈上分配:new 对象需要满足2个条件:逃逸分析、标量替换

  • TLAB:Thread Local Allocation Buffer 即线程本地分配缓存

  • 为什么需要TLAB

对象过大,在栈上放不下

我们知道,对象分配在堆上,而堆是一个全局共享的区域,当多个线程同一时刻操作堆内存分配对象空间时,就需要进行同步,而同步带来的效果就是对象分配效率变差(尽管JVM采用了CAS的形式处理分配失败的情况),但是对于存在竞争激烈的分配场合仍然会导致效率变差。

  • 什么是TLAB

那么能不能构造一种线程私有的堆空间,哪怕这块堆空间特别小,但是只要有,就可以每个线程在分配对象到堆空间时,先分配到自己所属的那一块堆空间中(实际上是Eden区中划出的),避免同步带来的效率问题,从而提高分配效率

  • TLAB失败

如果TLAB失败,对象也是存储在堆上,只不过堆上所有区域都被共享。而TLAB会在Eden区开辟很小的一个线程私有的空间用来分配对象

在这里插入图片描述

9. Object o = new Object() 在内存中占用多少字节

  • 什么时候开启压缩

为了减少64位平台下内存的消耗,启用指针压缩功能。4字节寻址的最大空间:指向的最大数字24*8-1=32g。

堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间;

在4G-32G堆内存范围内,可以通过编码、解码方式进行优化,使得jvm可以支持更大的内存配置

堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力,所以堆内存不要大于32G为好。就算你自己扩展内存为48g,也不起作用

  • 压缩

-XX:+UseCompressedClassPointers:压缩类指针
-XX:+UseCompressedOops:压缩普通对象指针(OOP)

C:\Users\cc>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=132175488 -XX:MaxHeapSize=2114807808 
-XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers 
-XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation 
-XX:+UseParallelGC
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)

对于64位JVM,Object o 默认占64位=8字节,但是默认开启压缩类指针,压缩成4字节
如果有一成员变量,里面有String s,s指向String类型对象,默认8字节,开启压缩OOP,占4字节

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值