JVM对象创建与内存分配机制深度剖析

1.JVM对象创建过程详解 

 

(1)类加载检查

虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过,如果没有,那必须的先执行相应的类加载过程。

new指令对应到语言层面上讲是,new 关键词、对象克隆、对象序列化等。

(2)分配内存

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

划分内存的方法

  • 指针碰撞(Bump the Pointer)(默认使用指针碰撞)

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

  • 空闲列表(Free List)

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

 ② 在并发情况下,正在给A对象分配内存,指针还未来得及修改,对象B又同时使用原来的指针来分配内存。解决并发问题的方法

  • CAS(compare and swap)

虚拟机采用CAS配上失败重试的方式保证更新操作的原子性,对分配内存空间的动作进行同步处理

  • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过-XX:+/-UseTLAB参数来设定虚拟机是否试用TLAB(JVM会默认开启-XX:+UseTLAB),-XX:+TLABSize指定TLAB大小(默认是Eden区的1%)。如果内存不够用使用CAS。

(3)初始化

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

(4)设置对象头

初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header中。

(5)执行<init>方法

执行<init>方法,即对象按照程序员的意愿进行初始化。对应到语言层面就是为属性赋值,和执行构造方法。

注意:这里面的赋值与(3)初始化不一样,(3)初始化是赋零值,此处赋值是程序中定义的值。例如int = 6;(3)初始化时为int=0;此处会重新赋值int=6;

2.对象头与指针压缩详解

(1)对象头

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头包括:

  • MarkWord标记字段(32位占4字节,64位占8字节),自身运行时数据,哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时时间戳
  • Klass Pointer类型指针(开启压缩占4字节,关闭压缩占8字节)类的元数据的指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 数组长度(4字节,只有数组对象才有)。

实例数据:定义的一些变量等信息。

对齐填充:JVM内部存取数据会按照8个字节的整数倍对齐,以保证最高的存取效率。

使用JOL看一下对象头信息,在pom中引用jol-core

<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   <version>0.9</version>
</dependency>

示例代码

import org.openjdk.jol.info.ClassLayout;
public class JOLSmple {
    public static void main(String[] args) {
        ClassLayout layout =ClassLayout.parseInstance(new Object());
        System.out.println(layout.toPrintable());
        System.out.println();

        ClassLayout layout1 =ClassLayout.parseInstance(new int[]{});
        System.out.println(layout1.toPrintable());
        System.out.println();

        ClassLayout layout2 =ClassLayout.parseInstance(new A());
        System.out.println(layout2.toPrintable());
        System.out.println();
    }

    public static class A{
        int id;
        String name;
        byte b;
        Object o;
    }
}

运行结果

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     0    int [I.<elements>                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


com.wing.ring.jvm.JOLSmple$A object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           61 cc 00 f8 (01100001 11001100 00000000 11111000) (-134165407)
     12     4                int A.id                                      0
     16     1               byte A.b                                       0
     17     3                    (alignment/padding gap)                  
     20     4   java.lang.String A.name                                    null
     24     4   java.lang.Object A.o                                       null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

 Object对象内部结构(无成员变量)

 数组内部结构

 自定义A对象(有成员变量)

 (2)指针压缩

  • jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
  • JVM配置参数:UseCompressedOops,Compressed--压缩、oop(ordinary object pointer)--对象指针
  • 启用指针压缩:-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops
  • -XX:+UseCompressedClassPointers 默认开启只压缩对象头里的类型指针Klass Pointer

配置禁止压缩后上述例子执行结果

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           00 1c a0 1c (00000000 00011100 10100000 00011100) (480254976)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           68 0b a0 1c (01101000 00001011 10100000 00011100) (480250728)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     20     4        (alignment/padding gap)                  
     24     0    int [I.<elements>                             N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total


com.wing.ring.jvm.JOLSmple$A object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           f8 51 f9 1c (11111000 01010001 11111001 00011100) (486101496)
     12     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4                int A.id                                      0
     20     1               byte A.b                                       0
     21     3                    (alignment/padding gap)                  
     24     8   java.lang.String A.name                                    null
     32     8   java.lang.Object A.o                                       null
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

Object对象 KlassPointer占了两个4字节=8字节

数组的KlassPointer也占用了8字节

 自定义含有局不变量的对象中,String对象和Object都占用了8个字节

为什么要进行指针压缩

1.在64位平台的HotSpot中使用32位指针(实际存储用64位),内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大带宽,同时GC也回承受较大压力。

2.为了减少64位平台下内存的消耗,启用指针压缩功能。

3.在JVM中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的存入堆内存时压缩编码、取出到CPU寄存器后解码的方式进行优化(对象指针在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)

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

5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)对对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好。

3.JVM内存分配&逃逸分析&栈上分析&标量替换详解

对象内存分配流程图

 (1)栈上分配

通过JVM内存分配Java中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大的压力,也间接的影响了应用的性能,为了减少临时对象在堆内存分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

对象逃逸分析:分析对象动态作用域,当一个对象在方法中被定义后,他可能被外部方法所引用,例如作为调用参数传递到其他地方中。

// 会逃逸对象User
public User test(){
    User u = new User();
    u.setId(1);
    //TODO  保存到数据库
    return u;
}

// 不会逃逸对象User
public void test1(){
    User u = new User();
    u.setId(1);
    //TODO  保存到数据库
}

test方法中User对象被返回了,这个对象的作用域范围不确定,test1中User对象可以确定当前方法结束这个对象就可以认为是无效的对象,对于这样的对象我们可以将其分配到栈内存,让其在方法结束时跟随栈内存一起被回收掉。

JVM可以通过(-XX:+DoEscapeAnalysis)开启逃逸分析来优化对象内存分配位置,使其通过标量替换 优先栈上分配JDK7之后默认开启逃逸分析,关闭逃逸分析使用参数(-XX:-DoEscapeAnalysis);

标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或者寄存器上分配空间,这样就不会因为没有一大块连续的空间导致对象内存不够分配,开启标量替换(-XX:+EliminateAllocations),JDK7之后默认开启。

标量与聚合量:标量即不可被进一步分解的量,而Java的基本数据类型就是标量(int,long等基本数据类型以及reference类型等)。可以被进一步分解的量称之为聚合量。Java中的对象就是可以被进一步分解的聚合量。

栈上分配示例:

/**
  * @date 2022/9/6 22:00
  * @param args
  * 使用3种参数进行测试
  * 1.-Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
  * 2.-Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
  * 3.-Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
  */
public class AllotOnStack {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end-start) + "ms");
    }

    private static void alloc(){
        User user = new User();
        user.setId(1);
        user.setName("M");
    }
}

参数解释:

-Xmx15m -Xms15m   上一篇文章中提到过,分配堆内存

-XX:+DoEscapeAnalysis 开启逃逸分析

-XX:+PrintGC  发生GC打印GC日志

-XX:+EliminateAllocations 开启标量替换

1.-Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations

 

 用时8ms发生两次GC,是因为把堆内存调小了

堆内存调整大了之后没有发生GC

2.-Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations

关闭逃逸分析,开启标量替换(此时开不开无所谓,标量替换是基于开启逃逸分析而言的)

耗时495ms,发生大量GC 

3.-Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations

开启逃逸分析,关闭标量替换

 耗时433ms,发生大量GC

结论:栈上分配依赖于逃逸分析和标量替换

(2)对象在Eden区分配

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

  • Minor GC/Young GC:指发生新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
  • Major GC/Full GC:一般会回收老年代,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的速度慢10倍以上。

Eden与Survivor区默认8:1:1

大量的对象被分配到Eden区,Eden区满了后会触发Minor GC,可能会有99%以上的对象成为垃圾被回收掉,剩余的对象会被挪到为空的那块Survivor区,下一次Eden区满了后再次触发Minor GC,把Eden和Survivor区垃圾对象回收,把剩余的对象一次性挪动到另外一块Survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1很合适,让Eden区尽量很大,Survivor区够用即可。

JVM默认有这个参数-XX:UseAdaptiveSizePolicy(默认开启),会导致8:1:1比例自动变化,如果不想这个比例变化可以设置参数-XX:-UseAdaptiveSizePolicy

代码示例:

运行生成50M的字节数组,通过GC日志看一下内存分配 

public class GCTest {
    /**
     * @param args
     * a1 = new byte[50000 * 1024];
     *
     * a2 = new byte[20000 * 1024];
     *
     * a3 = new byte[1000 * 1024];
     * a4 = new byte[1000 * 1024];
     * a5 = new byte[1000 * 1024];
     * a6 = new byte[1000 * 1024];
     *
     * 添加JVM运行参数:-XX:+PrintGCDetails
     */
    public static void main(String[] args) {
        byte[] a1;
        a1 = new byte[50000 * 1024];
    }
}

由以上例子可以得到以下结论:

 Eden区没有满不进行GC

 生成50M的字节数组后再次生成一个20M的数组,会是什么样,看以下代码

public class GCTest {
    /**
     * @param args
     * a1 = new byte[50000 * 1024];
     *
     * a2 = new byte[20000 * 1024];
     *
     * a3 = new byte[1000 * 1024];
     * a4 = new byte[1000 * 1024];
     * a5 = new byte[1000 * 1024];
     * a6 = new byte[1000 * 1024];
     *
     * 添加JVM运行参数:-XX:+PrintGCDetails
     */
    public static void main(String[] args) {
        byte[] a1,a2;
        a1 = new byte[50000 * 1024];

        a2 = new byte[20000 * 1024];
    }
}

 由以上运行结果可得出结论:

Eden区满了之后会触发GC,将非垃圾对象保存到Survivor区,Survivor区内存不够的放到老年代

进行了一次Minor GC后在生成几个1M的字节数组,会是什么效果:

    public static void main(String[] args) {
        byte[] a1, a2, a3, a4, a5, a6;
        a1 = new byte[50000 * 1024];

        a2 = new byte[20000 * 1024];

        a3 = new byte[1000 * 1024];
        a4 = new byte[1000 * 1024];
        a5 = new byte[1000 * 1024];
        a6 = new byte[1000 * 1024];
    }

由以上例子可以得到以下结论:

 Eden区没有满不进行GC

(3)大对象直接进入老年代

大对象就是需要大量连续的空间对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在Serial和ParNew两个收集器下有效。

例如:-XX:PretenureSizeThreshol=10000(单位是字节) -XX:+UseSerialGC

    public static void main(String[] args) {
        byte[] a1;
        a1 = new byte[2000 * 1024];
    }

为什么要这样做呢?

为了避免大对象分配内存时的复制操作而将低效率

(4) 长期存活的对象将进入老年代

虚拟机采用了分代收集思想来管理内存,内存回收时需要识别哪些对象应该放在新生代,哪些对象应放在老年代。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象的年龄设为1。对象在Survivor中每经过一次Minor GC年龄就会加1,当年龄增加到一定程度时(默认15岁,CMS收集器默认6岁,不同的收集器会有差异),就会被移动到老年代中。可以通过-XX:MaxTenuringThreshold设置移动到老年代的阈值。

(5)对象动态年龄判断

当前放对象的Survivor区域里(其中一块区域,S0或S1),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了。

例如:年龄1  +  年龄2  +   年龄n 的多个年龄对象综合超过了Survivor区域的50%,此时就会把年龄n(包含n)以上的对象都放入到老年代。

这个规则其实是希望那些可能长期存活的对象,尽早的进入老年代。对象动态年龄判断机制一般是在Minor GC之后触发。

(6)老年代空间分配担保机制

年轻代每次Minor GC之前JVM都会计算下老年代剩余可用空间

① 如果这个可用空间小于年轻代里面现有的所有对象大小之和(包括垃圾对象

② 看是否设置担保机制即-XX:-HandlePromotionFailure(JDK8默认设置了)

③ 如果有这个参数,会比较老年代可用的空间大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。

如果没有这个参数直接进入Full GC。

④如果上一步空间比较结果是,老年代可用空间小于平均值,触发一次FullGC,对老年代和年轻代内存一起垃圾回收一次,如果回收完还是没有足够的空间,对象就会发生OOM。

如果老年代可用空间大于平均值,直接进行Minor GC。

4.对象内存回收机制详解

堆中几乎放着所有的对象实例,进行垃圾回收前第一步要判断哪些对象已经死亡(即不能再被任何途径使用的对象)

(1)引用计数法

给对象中添加一个引用计数器,每当有一个地方引用计数器就加1;当引用失效计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。

这个方法简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

public class ReferenceCountingGC {
    Object instance = null;

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC(); // 1. objA 引用当前对象(A) countA+1 = 1
        ReferenceCountingGC objB = new ReferenceCountingGC(); // 2. objB 引用当前另一个对象(B) countB+1 = 1
        objA.instance = objB; // 3. objA的instance 又引用了 B 对象,countB+1 = 2
        objB.instance = objA; // 4. objB的instance 又引用了 A 对象,countA+1 = 2
        objA = null; // 5. objA赋值为null,1步骤里面的引用失效,A对象的 countA-1 = 1
        objB = null; // 6. objB赋值为null,2步骤里面的引用失效,B对象的 countB-1 = 1
        // 此时A对象和B对象都为垃圾对象,但是引用计数器不为0,不能被回收
    }
}

(2)可达性分析算法

将“GC Roots”对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。

GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等。

 (3)常见的引用类型

Java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用

强引用:普通的变量引用

public static User user = new User();

软引用:将对象用SoftReference软引用类型的对象包裹,正常情况下不会被回收,但是GC做完后发现释放不出空间存新的对象,则会把这些软引用的对象回收掉。软引用可实现内存敏感的高速缓存。

public static SoftReference<User> user = new SoftReference<User> new User();

软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是要进行请求还是从缓存中取呢?要看就提的实现策略。

① 如果一个网页在浏览结束时就要进行内容的回收,则按后退查看前面浏览过的网页,需要重新构建。

② 如果将浏览过的网页存储到内存中会造成内存大量的浪费,甚至会内存溢出。

弱引用:将对象用WeakReference弱引用类型的对象包裹。GC会直接回收掉。

public static WeakReference<User> user = new WeakReference<User> new User();

虚引用:虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用,几乎不用。GC会直接回收掉。

(4)finalize()方法最终判定对象是否存活

即使在可达性分析算法中不可达对象,也并非是“非死不可”的,这时候他们暂时处于“缓刑”状态,要真正宣告一个对象死亡,至少要经历再次标记过程。

标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链

1.第一次标记并进行一次筛选

筛选的条件是此对象是否有必要执行finalize()方法。

当对象没有覆盖finalize()方法,对象将直接被回收。

2.第二次标记

如果这对象覆盖了finalize()方法,finalize方法是对象脱逃死亡的最后一次机会,只要重新与引用链上的任何一个对象建立关系即可,譬如把自己赋值给某个类的成员变量。,在第二次标记的时候它将移除“即将回收”的集合。

注意:一个对象的finalize方法只会被执行一次。

public class OOMTest {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        int i = 0;
        int j = 0;
        while(true){
            list.add(new User(i++, UUID.randomUUID().toString()));
            new User(j--, UUID.randomUUID().toString());
        }
    }
}
@Data
public class User {

    public int id;

    public String name;

    public User() {
    }

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("userId="+id+"即将被回收!");
    }
}

运行结果:id为负数的执行类finalize方法

 (5)如何判断一个类是无用的类

方法区主要是回收无用的类,如何判断一个类是无用的类

  • 该类所有的对象实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收(由于这一条限制,在方法区做类回收时,只有自定义类加载器的加载的类才会被回收,其余的不会被回收)
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Seventeen117

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

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

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

打赏作者

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

抵扣说明:

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

余额充值