文章目录
1. Java的4种引用类型 / 内存泄漏问题:强软弱虚
- 强引用
Object o = new Object();
特点:
- 强引用可以直接访问目标对象
- 强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常,也不会回收强引用所指向的对象
- 强引用可能导致内存泄漏。
- 软引用
可以通过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());
}
}
- 弱引用
只要发生垃圾回收,就会被回收,使用: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
- 虚引用
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
说明:
- 拿到当前线程独有的ThreadLocalMap map
- set方法是将value放在map里
- 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区开辟很小的一个线程私有的空间用来分配对象
- 详细内容:浅析java中的TLAB
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字节