程序员

这些JVM中的对象及引用你一定得知道,阿里,美团这些大厂都喜欢问

2020-07-24  本文已影响0人  程序员匡胤

一、JVM中对象的创建过程

类加载

将.class文件加载到JVM运行时数据区的过程(后面在讲)

检查加载

检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查类是否已经被加载、解析、初始化

内存分配

JVM给新生对象分配内存,分配内存的方式有2种:

指针碰撞

如果java的内存是规整的,所有用过的内存放在一边,没有用过的内存放在另一边,使用一个指针将其分开,那所分配内存就仅仅是将指针往没有用过的内存的方向挪动新生对象所需要内存大小相等的距离,这就是"指针碰撞"。


空闲列表

如果java的内存是不规整的,已使用的内存和空闲内存相互交错,虚拟机必须维护一个列表,列表记录哪块内存是已使用的,哪块内存是空闲的,分配对象的时候将足够大的空闲内存分配给新生对象,在列表上更新记录,这就是"空闲列表"


选择哪种分配是由java堆是否规整决定,而堆是否规整取决于垃圾回收器是否带有内存整理功能

并发安全

面试时候重点,牢记
创建对象在JVM中式非常频繁的行为,那么在多线程的环境下就会出现对象A正在分配内存,对象B又同时使用原来的指针分配内存,解决的手段有以下2种

CAS

在分配对象的时候,先读取当前内存的值进行预处理,然后在实际分配的时候进行CAS比较,如果比较的结果是相同的,那么就将这块内存分配给这个对象,如果不相同,继续比较,直到找到相同的值为止

分配缓冲(ThreadLocal Allocation Buffer,TLAB)

把内存分配按照线程的不同划分在不同的空间中运行,即每个线程在 Java 堆中预先分配一小块私有内存,也就是本地线程分配缓冲,JVM 在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个 Buffer,如果 需要分配内存,就在自己的 Buffer 上分配,这样就不存在竞争的情况,可以大大提升分配效率,当 Buffer 容量不够的时候,再重新从 Eden 区域申请一块 继续使用。
TLAB 的目的是在为新对象分配内存空间时,让每个 Java 应用线程能在使用自己专属的分配指针来分配空间,减少同步开销
参数:-XX:+UseTLAB,默认是开启的

内存空间初始化

将分配的内存空间初始化为零值(int = 0,boolean = false),这是为了保证java的对象在使用的时候不赋初始值也能够正常使用

设置

虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息

对象初始化

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

二、对象的内存布局

说白了就是对象由哪几部分组成

三、对象的访问定位

创建对象是为了使用,通过虚拟机栈中Reference操作堆中的对象,方式有2种,句柄和直接指针

句柄

使用句柄,在java堆种就会创建一个句柄池,虚拟机栈种Reference存储的就是句柄的地址,句柄池中存储对象实例的地址和对象类型的地址。使用句柄的好处就是虚拟机栈中Reference存储的句柄地址比较稳定,当对象改变时,只需要改变句柄池内的存储对象实例的地址即可

直接指针

如果使用直接指针访问, 虚拟机栈Reference 中存储的直接就是对象地址。
使用直接指针的好处就是访问对象的速度比句柄方式更快,因为节省了一个指针的开销


四、判断对象是否存活

判断对象是否存活就是查看对象是否还有没有被引用,通常有2种方法:计数器法和引用可达法

计数器法

对象创建好之后,之后不管谁引用对象都要给计数器加1,不引用对象就给计数器减1,如果计数器等于0,代表对象没有任何引用,那就可以当作垃圾处理,GC的时候对象的内存就会被释放掉。
这里有一个问题,就是对象互相引用的时候会造成计数器不会为0,就会产生内存泄漏的现象,java中采用的是根可达法来判断对象是否存活

根可达法

面试时候重点,牢记
来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为 引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。 作为 GC Roots 的对象包括下面几种:

五、各种引用

强引用

=,new关键字就是强引用,只要与根可达,GC的时候也不会被回收

软引用

一些有用但非必须的对象,系统将要发生OutOfMemory之前,GC时候这些对象会被回收

弱引用

只要发生GC,就一定会被回收
ThreadLocal中就使用了弱引用

虚引用

幽灵引用,最弱(随时会被回收掉) 垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作。

对象的分配策略

虚拟机中几乎所有的对象是在堆中分配,但是有的对象可以在栈上分配,想要在栈上分配对象,就要满足一个点,就是逃逸分析
逃逸分析:如下代码,MyObject对象,在方法allocate中new出来,Myobject对象的引用也在方法内,它逃不出这个方法,JVM会做一次优化,对象在栈上分配会好一点,因为这个方法也不会被其它线程使用到。

package ex3;
/**
 * @author King老师
 * 逃逸分析-栈上分配
 * -XX:-DoEscapeAnalysis -XX:+PrintGC
 */
public class EscapeAnalysisTest {
    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 50000000; i++) {//5000万次---5000万个对象
            allocate();
        }
        System.out.println((System.currentTimeMillis() - start) + " ms");
        Thread.sleep(600000);
    }

    static void allocate() {//逃逸分析(不会逃逸出方法)
        //这个myObject引用没有出去,也没有其他方法使用
        MyObject myObject = new MyObject(2020, 2020.6);
    }

    static class MyObject {
        int a;
        double b;

        MyObject(int a, double b) {
            this.a = a;
            this.b = b;
        }
    }
}

逃逸分析默认是开启的,运行上面代码可以看到,5000万次循环也只是用了5ms,PrintGC并没有打印出任何关于GC的日志,可以知道对象在栈上分配就不会有垃圾回收。将逃逸分析关闭,对象分配就会在堆中,就会打印出GC消息,同时,执行5000万次循环的速度也大大降低

逃逸分析是JVM的优化技术,上面的例子中,如果不是触发5000万次,只有一个对象生成,那么还是会在堆中产生对象

对象优先在Eden区分配

大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间分配时,虚拟机将发起一次 Minor GC。

大对象直接进入老年代

大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。
在 Java 虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好 它们。
HotSpot 虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor 区之间来回复制,产生大量的内存复制操作
这样做的目的:1.避免大量内存复制,2.避免提前进行垃圾回收,明明内存有空间进行分配。 PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器有效。-XX:PretenureSizeThreshold=4m

长期存活对象进入老年区

HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。 为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1,当它的年龄增加到一定程度(并发的垃圾回收器默认为 15),CMS 是 6 时,就会被晋升到老年代中。 -XX:MaxTenuringThreshold 调整

对象年龄动态判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中 相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的 年龄

空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全 的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历 次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的,如果担保失败则会进行一次 Full GC;如果小 于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

上一篇 下一篇

猜你喜欢

热点阅读