玩转JVM中的对象及引用

玩转JVM中的对象及引用

目录(数字表示优先级别)

1.JVM中对象的创建过程

JVM中对象的创建过程

虚拟机遇到一条 new 指令时,首先检查是否被类加载器加载,如果没有,那必须先执行相应的类加载过程。 类加载就是把 class 加载到 JVM 的运行时数据区的过程。

1.1 检查加载

首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用(符号引用 :符号引用以一组符号来描述所引用的目标),并且检查类是否已经被加载、 解析和初始化过。

1.2 分配内存(要点)

接下来虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来

1.2.1 划分内存

指针碰撞:
如果 Java 堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅 是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。
指针碰撞

空闲列表:
如果 Java 堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上 哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。
空闲列表

对于指针碰撞和空闲列表,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
(1)如果是 Serial、ParNew 等带有压缩的整理的垃圾回收器的话,系统采用的是指针碰撞,既简单又高效。
(2)如果是使用 CMS 这种不带压缩(整理)的垃圾回收器的话,理论上只能采用较复杂的空闲列表。

1.2.2 并发安全

除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情 况下也并不是线程安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。
CAS (Compare and swap)机制:
在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成。实际上虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性,防止多线程在分配内存时重复分配同一块区域导致线程不安全。
CAS机制

这个Compare的过程不太好了解,我再画个流程图来解释一下
CAS的Compare过程:CAS有三个操作数,内存值V,旧的预期值E,要修改的新值N。当且仅当预期值E和内存值V相同时,将内存值V修改为N,否则什么都不做。
CAS Compare流程

接下来我们用一个实例来测试一下
先用一个类来实现Runnable接口

package sandwich.test3;

/**
 * @author sandwich
 * @date 2021/4/24
 */
public class ThreadDemo implements Runnable {

    private int count = 0;

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            addCount();
        }
    }

    private void addCount() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

接下来创建线程池,提交10个线程执行,预期结果应该是1000

package sandwich.test3;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/24
 */
public class ThreadTest {

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        ThreadDemo threadDemo = new ThreadDemo();
        for (int i = 0; i < 10; i++) {
            threadPool.submit(threadDemo);
        }
        Thread.sleep(1000);
        threadPool.shutdown();
        System.out.println(threadDemo.getCount());
    }
}

打印结果

第一次:
959
第二次:
962

由此可见执行结果并不理想。由此可见java中i++或者++i并不是线程安全的。
接下来我们试着改变count,将值从高速缓冲区刷新到主内存后,让其他线程重新读取主内存中的值到自己的工作内存。
用volatile关键字修饰count。它的作用是保证对象在内存中的可见性。

private volatile int count = 0;

再执行几次试试

第一次:
956
第二次:
1000
第三次
1000
第四次
991

还是不太可靠,并不能稳定输出1000。
这是为什么呢?
线程安全主要体现在三个方面:
(1)原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作
(2)可见性:一个线程对主内存的修改可以及时的被其他线程观察到
(3)有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
目前可见性已经实现了,缺少原子性的操作,因为同一时刻,多个线程对其操作,会将改动后的最新值读取到自己的工作内存进行操作,最终只能得到后一个执行线程操作的结果,所以相当于少了一步操作,就会造成数据的不一致。
Atomic类来可以使用CAS+volatile来实现原子性与可见性的。
我们来试试吧
代码改造成如下

package sandwich.test3;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/24
 */
public class AtomicThreadDemo implements Runnable{

    private  AtomicInteger count = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            count.getAndIncrement();
        }
    }

    public int getCount() {
        return count.get();
    }
}
package sandwich.test3;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/24
 */
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
//        ThreadDemo threadDemo = new ThreadDemo();
        AtomicThreadDemo threadDemo = new AtomicThreadDemo();
        for (int i = 0; i < 10; i++) {
            threadPool.submit(threadDemo);
        }
        Thread.sleep(1000);
        threadPool.shutdown();
        System.out.println(threadDemo.getCount());
    }
}

再试几次执行结果

1000

总是得到稳定的期望值,证明AtomicInteger是线程安全的。
我来分析一下AtomicInteger的源码

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 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;

Unsafe类是不安全的类,它提供了一些底层的方法,我们是不能直接使用Unsafe类的。AtomicInteger的值保存在value中,而valueOffset是value在内存中的偏移量,利用静态代码块使其类一加载的时候就赋值。value值使用volatile,保证其可见性。这里被声明为volatile,就是为了保证在更新操作时,当前线程可以拿到value最新的值(并发环境下,value可能已经被其他线程更新了)。
getAndIncrement是我们的测试代码用到的,用来实现自增的

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
 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;
    }

var1表示当前对象,var2表示value在内存中的偏移量,var4为增加的值。var5为调用底层方法获取value的值
compareAndSwapInt方法通过var1和var2获取当前内存中的value值,并与var5进行比对,如果一致,就将var5+var4的值赋给value,并返回true,否则返回false
由do while语句可知,如果这次没有设置进去值,就重复执行此过程。这一过程称为自旋。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

compareAndSwapInt是JNI(Java Native Interface)提供的方法,可以是其他语言写的。
synchronized与CAS比较
synchronized也可以保证线程安全,我们先看用synchronized实现的实例

package sandwich.test3;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/24
 */
public class SynchronizedThreadDemo implements Runnable{
    private int count = 0;

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (ThreadDemo.class) {
                count++;
            }
        }
    }

    public int getCount() {
        return count;
    }
}
package sandwich.test3;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/24
 */
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
//        ThreadDemo threadDemo = new ThreadDemo();
//        AtomicThreadDemo threadDemo = new AtomicThreadDemo();
        SynchronizedThreadDemo threadDemo = new SynchronizedThreadDemo();
        for (int i = 0; i < 10; i++) {
            threadPool.submit(threadDemo);
        }
        Thread.sleep(1000);
        threadPool.shutdown();
        System.out.println(threadDemo.getCount());
    }
}

执行结果

1000

结果也是满足预期
使用synchronized和AtomicInteger都能使线程安全,但是他们之间各有什么劣势呢
(1)synchronized是重量级锁,是悲观锁,就是无论你线程之间发不发生竞争关系,它都认为会发生竞争,从而每次执行都会加锁。
在并发量大的情况下,如果锁的时间较长,那将会严重影响系统性能。
(2)CAS操作中我们可以看到getAndAddInt方法的自旋操作,如果长时间自旋,那么肯定会对系统造成压力。而且如果value值从A->B->A,那么CAS就会认为这个值没有被操作过,这个称为CAS操作的"ABA"问题。
由此可见,这两种方式为了线程安全,都牺牲了性能。

分配缓冲(Thread Local Allocation Buffer,TLAB):
另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块私有内存,也就是本地线程分配缓冲,JVM 在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个 Buffer,如果 需要分配内存,就在自己的 Buffer 上分配,这样就不存在竞争的情况,可以大大提升分配效率,当 Buffer 容量不够的时候,再重新从 Eden 区域申请一块 继续使用。
TLAB 的目的是在为新对象分配内存空间时,让每个 Java 应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。
TLAB 只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个 TLAB 用满(分 配指针 top 撞上分配极限 end 了),就新申请一个 TLAB。
TLAP分配

由于TLAB是线程私有的,所以天生有具备线程安全的特性。
可以用以下参数决定是否开启TLAB:
XX:+UseTLAB
允许在年轻代空间中使用线程本地分配块(TLAB)。默认情况下启用此选项。要禁用 TLAB,请指定-XX:-UseTLAB。
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

-XX:+UseTLAB
Enables the use of thread-local allocation blocks (TLABs) in the young generation space. This option is enabled by default. To disable the use of TLABs, specify -XX:-UseTLAB.

1.3 内存空间初始化

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

1.4 设置

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(Java classes 在 Java hotspot VM 内部表示为类 元数据)、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象的对象头之中。

1.5 对象初始化

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

2.对象的内存布局

对象的内存布局
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程
ID、偏向时间戳等。
对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 如果对象是一个 java 数组,那么在对象头中还有一块用于记录数组长度的数据。
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于 HotSpot VM 的自动内存管理系统要求对对象的大小必须 是 8 字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。

3. 对象的访问定位

建立对象是为了使用对象,我们的 Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。

3.1 句柄

如果使用句柄访问的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类 型数据各自的具体地址信息。
句柄方式访问对象

3.2 直接指针

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

直接指针方式访问对象

总结
这两种对象访问方式各有优势

  • 使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实 例数据指针,而 reference 本身不需要修改.
  • 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频 繁,因此这类开销积少成多后也是一项非常可观的执行成本。
    对 Sun HotSpot 而言,它是使用直接指针访问方式进行对象访问的。

4 判断对象的存活

在堆里面存放着几乎所有的对象实例,垃圾回收器在对对进行回收前,要做的事情就是确定这些对象中哪些还是“存活”着,哪些已经“死去”(死去 代表着不可能再被任何途径使用的对象了)

4.1 引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1,当引用失效时,计数器减 1.
引用计数

Python 在用,但主流虚拟机没有使用,因为存在对象相互引用的情况,这个时候需要引入额外的机制来处理,这样做影响效率。
下面用一个实例来证明java并没有用引用计数法

package sandwich.test3;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/24
 * VM Args: -XX:+PrintGC
 */
public class ObjectAliveValidation {

    public Object instance = null;
    /**
     * 给对象分配内存,便于判断分析GC
     */
    private final byte[] bigSize = new byte[10*1024*1024];

    public static void main(String[] args) {
        ObjectAliveValidation objectA = new ObjectAliveValidation();
        ObjectAliveValidation objectB = new ObjectAliveValidation();
        //互相引用
        objectA.instance = objectB;
        objectB.instance = objectA;
        //切断可达
        objectA = null;
        objectB = null;
        //强制垃圾回收
        System.gc();
    }
}

输出
新生代数据被回收
由上图可以看出,只保留互相引用的对象还是被回收了,说明JVM中采用的不是引用计数法。

4.2 可达性分析(要点)

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

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个现场被调用方法堆栈中使用到的参数、局部变量、临时变量等。
  • 方法区中类静态属性引用的对象;java 类的引用类型静态变量。
  • 方法区中常量引用的对象;比如:字符串常量池里的引用。
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
  • JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器)。(非重点)
  • 所有被同步锁(synchronized 关键)持有的对象。(非重点)
  • JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等(非重点)
  • JVM 实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代的对象,这个后续会细讲,先大致了解概念)(非重点)
    以上的回收都是对象,类的回收条件:
    注意 Class 要被回收,条件比较苛刻,必须同时满足以下的条件(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):
    1、 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
    2、 加载该类的 ClassLoader 已经被回收。
    3、 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    4、 参数控制:
-Xnoclassgc
禁用类的垃圾收集(GC)。这样可以节省一些GC时间,从而缩短了应用程序运行期间的中断时间
当您-Xnoclassgc在启动时指定,应用程序中的类对象在GC期间将保持不变,并始终被认为是活动的。这可能会导致更多内存被永久占用,如果不慎使用,将抛出内存不足异常。

废弃的常量和静态变量的回收其实和Class回收的条件差不多。
可达性分析,非根可达可回收

4.3 Finalize 方法

即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是 没有找到与 GCRoots 的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了 finalize),我们可以在 finalize 中去拯救。
看实例

package sandwich.test3;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/25
 */
public class FinalizeGc {

    public static FinalizeGc instance = null;
    public void isAlive() {
        System.out.println("I am still alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeGc.instance = this;
    }

    public static void main(String[] args) throws InterruptedException {
        instance = new FinalizeGc();
        instance = null;
        //对象进行第1次GC
        System.gc();
        //Finalize方法优先级很低,需要等待
        Thread.sleep(1000);
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead1!");
        }
        instance = null;
        //对象进行第二次GC
        System.gc();
        Thread.sleep(1000);
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead2!");
        }
    }
}

执行结果

finalize method executed
I am still alive
I am dead2!

可以看到,对象可以被拯救一次(finalize 执行第一次,但是不会执行第二次)
修改成以下代码,再来测试一次

package sandwich.test3;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/25
 */
public class FinalizeGc {

    public static FinalizeGc instance = null;
    public void isAlive() {
        System.out.println("I am still alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeGc.instance = this;
    }

    public static void main(String[] args) throws InterruptedException {
        instance = new FinalizeGc();
        instance = null;
        //对象进行第1次GC
        System.gc();
        //Finalize方法优先级很低,需要等待
//        Thread.sleep(1000);
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead1!");
        }
        instance = null;
        //对象进行第二次GC
        System.gc();
//        Thread.sleep(1000);
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead2!");
        }
    }
}

执行结果

I am dead1!
finalize method executed
I am dead2!

对象没有被拯救,这个就是 finalize 方法执行缓慢,还没有完成拯救,垃圾回收器就已经回收掉了。
所以建议大家尽量不要使用 finalize,因为这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序,建议大家忘了 finalize 方法!因为在 finalize 方法能做的工作,java 中有更好的,比如 try-finally, try-resource或者其他方式可以做得更好

5. 各种引用

5.1 强引用

一般的 Object obj = new Object() ,就属于强引用。在任何情况下,只有有强引用关联(与根可达)还在,垃圾回收器就永远不会回收掉被引用的对象。

5.2 软引用(Soft Reference)

一些有用但是并非必需,用软引用关联的对象,系统将要发生内存溢出(OuyOfMemory)之前,这些对象就会被回收(如果这次回收后还是没有足够的 空间,才会抛出内存溢出)
测试实例

package sandwich.test3;

import java.lang.ref.SoftReference;
import java.util.LinkedList;
import java.util.List;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/25
 * VM Args: -Xms10m -Xmx10m -XX:+PrintGC
 */
public class SoftRef {

    public static class User{
        public int id = 0;
        public String name = "";
        public User(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User [id=" + id + ", name=" + name + "]";
        }

    }
    public static void main(String[] args) {
        //new是强引用
        User user = new User(1,"ITSandwich");
        //软引用
        SoftReference<User> userSoft = new SoftReference<>(user);
        //干掉强引用,确保这个实例只有userSoft的软引用
        user = null;
        //看一下这个对象是否还在
        System.out.println(userSoft.get());
        //进行一次GC垃圾回收  千万不要写在业务代码中。
        System.gc();
        System.out.println("After gc");
        System.out.println(userSoft.get());
        //往堆中填充数据,导致OOM
        List<byte[]> list = new LinkedList<>();
        try {
            for(int i=0;i<100;i++) {
                System.out.println("*************"+userSoft.get());
                //1M的对象 100m
                list.add(new byte[1024*1024*1]);
            }
        } catch (Throwable e) {
            //抛出了OOM异常时打印软引用对象
            System.out.println("Exception*************"+userSoft.get());
            e.printStackTrace();
        }

    }
}

运行结果

User [id=1, name=ITSandwich]
[GC (System.gc())  1603K->704K(9728K), 0.0012287 secs]
[Full GC (System.gc())  704K->601K(9728K), 0.0060100 secs]
After gc
User [id=1, name=ITSandwich]
*************User [id=1, name=ITSandwich]
*************User [id=1, name=ITSandwich]
*************User [id=1, name=ITSandwich]
*************User [id=1, name=ITSandwich]
*************User [id=1, name=ITSandwich]
*************User [id=1, name=ITSandwich]
*************User [id=1, name=ITSandwich]
*************User [id=1, name=ITSandwich]
[GC (Allocation Failure) -- 7942K->7942K(9728K), 0.0008156 secs]
[Full GC (Ergonomics)  7942K->7774K(9728K), 0.0065742 secs]
[GC (Allocation Failure) -- 7774K->7774K(9728K), 0.0005664 secs]
[Full GC (Allocation Failure)  7774K->7755K(9728K), 0.0066793 secs]
Exception*************null
java.lang.OutOfMemoryError: Java heap space
	at sandwich.test3.SoftRef.main(SoftRef.java:47)

由此可见,主动GC还是无法回收软引用,系统将要OOM之前才回收。

5.3 弱引用(Weak Reference)

一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC 发生时,不管内存够不够,都会被回收。
测试实例:

package sandwich.test3;

import java.lang.ref.WeakReference;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/25
 */
public class WeakRef {
    public static class User{
        public int id = 0;
        public String name = "";
        public User(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User [id=" + id + ", name=" + name + "]";
        }

    }
    public static void main(String[] args) {
        //new是强引用
        User user = new User(1,"ITSandwich");
        //软引用
        WeakReference<User> weakUser = new WeakReference<User>(user);
        //干掉强引用,确保这个实例只有userSoft的软引用
        user = null;
        //看一下这个对象是否还在
        System.out.println(weakUser.get());
        //进行一次GC垃圾回收 
        System.gc();
        System.out.println("After gc");
        System.out.println(weakUser.get());
    }
}

输出结果

User [id=1, name=ITSandwich]
After gc
null

注意:软引用 SoftReference 和弱引用 WeakReference,可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存。当系统内存不足的时候,缓存 中的内容是可以被释放的。
实际运用(WeakHashMap、ThreadLocal)

5.4 虚引用(Phantom Reference)

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

6. 对象的分配策略

6.1 栈上分配

逃逸分析的原理:分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用。
比如:调用参数传递到其他方法中,这种称之为方法逃逸。甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸。 从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度。
如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高 JVM 的效率。
总结:如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。 采用了逃逸分析后,满足逃逸的对象在栈上分配。
栈上分配

我们来看一个实例

package sandwich.test3;

/**
 * @author 公众号:IT三明治
 * @date 2021/4/25
 * 逃逸分析 - 栈上分配
 * -XX:-DoEscapeAnalysis -xx:+PrintGCDetails
 * 逃逸分析默认是打开的,这个配置是把它关了
 */
public class EscapeAnalysis {

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i< 50000000; i++) {
            allocate();
        }
        System.out.println(System.currentTimeMillis() - start + "ms");
        Thread.sleep(1000000);
    }

    /**
     * 逃逸分析(不会逃逸出方法)
     * 这里面的对象只会在方法内临时使用,占用内存也不大,适合在栈上分配, 这样在方法出栈的时候,对象就会被销毁了。
     * 可以大大提高内存使用效率
     */
    private static void allocate() {
        //这个TestObject引用没有出去,也没有其他方法使用
        TestObject object = new TestObject(1000, 2000);
    }

    static class TestObject {
        int a;
        int b;

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

输出结果

7ms

5000万次创建对象,只花了7ms,效率还是很高的
接下来我们人为把逃逸分析关了
关闭逃逸分析
重新执行一次,看输出结果

[GC (Allocation Failure) [PSYoungGen: 65024K->760K(75776K)] 65024K->768K(249344K), 0.0015729 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 65784K->840K(75776K)] 65792K->848K(249344K), 0.0010619 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 65864K->808K(75776K)] 65872K->816K(249344K), 0.0008698 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 65832K->776K(140800K)] 65840K->784K(314368K), 0.0008858 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 130824K->824K(140800K)] 130832K->840K(314368K), 0.0018528 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 130872K->808K(261120K)] 130888K->824K(434688K), 0.0012050 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 260904K->0K(261120K)] 260920K->672K(434688K), 0.0045038 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 260096K->0K(520704K)] 260768K->672K(694272K), 0.0007780 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
435ms

可以看出来,关闭了逃逸分析,JVM 在频繁的进行垃圾回收(GC),正是这一块的操作导致性能有较大的差别。垃圾回收会影响系统性能。

6.2 对象优先在 Eden 区分配

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

6.3 大对象直接进入老年代

大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。
大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到- -群“朝生夕灭”的“短命大对象”,我们写程序 的时候应注意避免。
在 Java 虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好 它们。
而当复制对象时,大对象就意味着高额的内存复制开销。
HotSpot 虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor 区之间来回复制,产生大量的内存复制操作。
这样做的目的:

  • 避免大量内存复制
  • 避免提前进行垃圾回收,明明内存有空间进行分配。
    PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器有效。-XX:PretenureSizeThreshold=4m

6.4 长期存活对象进入老年区

HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。 为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1,当它的年龄增加到一定程度(并发的垃圾回收器默认为 15),CMS 是 6 时,就会被晋升到老年代中。
-XX:MaxTenuringThreshold 调整

6.5 对象年龄动态判定

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

6.5 空间分配担保

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

6.6 本地线程分配缓冲(TLAB)

前面线程安全已经介绍了,不再多做介绍

6.7 对象分配策略总结

对象的分配原则

  • 对象优先在Eden分配
  • 空间分配担保
  • 大对象直接进入老年代
  • 长期存活对象进入老年代
  • 动态对象年龄判定

虚拟机的优化技术

  • 逃逸分析(栈上分配)
  • 本地线程分配缓冲

对象分配流程

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT三明治

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值