JVM性能专题三:JVM内存分配机制

一、对象创建过程

  P1:对象创建的主要流程:

    1)类加载检查:

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

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

    2)分配内存:

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

      这个步骤有两个问题:

        1.如何划分内存

        2.在并发情况下,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用原来的指针分配内存的情况

      划分内存的方法:

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

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

      *  "空闲列表"(Free List)

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

     解决并发问题的方法:

     *  CAS(compare and swap)

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

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

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

    3)初始化:

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

    4)设置对象头(Object Header):

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

    5)执行<init>方法:

      执行<init>方法,即对象按照程序员的意愿进行初始化,对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法

二、对象头与指针压缩

    P1:对象内存布局:

      1)Mark Word:包含一系列的标志位,比如对象的哈希码、GC分代年龄、锁标志位信息等等,在64位系统中占8字节

      2)类型指针(Klass Pointer):用来指向对象在元空间对应的元数据信息,64位系统开启指针压缩占4字节,未开启指针压缩占8字节

      3)数组长度:如果该对象是数组对象,那么会保存一个数组长度的空间,占4字节,如果该对象不是数组对象,则不占任何空间

      4)实例数据(Instance Data):非静态的属性才能是实例数据,boolean占1字节、byte类型占1字节、short类型占2字节、char类型占2字节、int类型占4字节、float类型占4字节、long类型占8字节、double类型占8字节,string类型开启指针压缩4字节,未开启指针压缩8字节、reference类型开启指针压缩4字节,未开启指针压缩8字节

      5)对齐填充(padding):保证对象大小是8字节的整数倍

    P2:对象头区域:

      1)MarkWord:用于存储对象自身的运行时数据,这块区域是动态定义的数据结构

       代码清单:

1 Bit‐format of an object header (most significant first, big endian layout below):
2 //
3 // 32 bits:
4 // ‐‐‐‐‐‐‐‐
5 // hash:25 ‐‐‐‐‐‐‐‐‐‐‐‐>| age:4 biased_lock:1 lock:2 (normal object)
6 // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
7 // size:32 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (CMS free block)
8 // PromotedObject*:29 ‐‐‐‐‐‐‐‐‐‐>| promo_bits:3 ‐‐‐‐‐>| (CMS promoted object)
9 //
10 // 64 bits:
11 // ‐‐‐‐‐‐‐‐
12 // unused:25 hash:31 ‐‐>| unused:1 age:4 biased_lock:1 lock:2 (normal object)
13 // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
14 // PromotedObject*:61 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| promo_bits:3 ‐‐‐‐‐>| (CMS promoted object)
15 // size:64 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (CMS free block)
16 //
17 // unused:25 hash:31 ‐‐>| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
18 // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
19 // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ‐‐‐‐‐>| (COOPs && CMS promoted object)
20 // unused:21 size:35 ‐‐>| cms_free:1 unused:7 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (COOPs && CMS free block)

      2)类型指针(Klass Pointer):对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。查找对象的元数据信息并不一定要经过对象本身

     P3:指针压缩技术(https://blog.csdn.net/liujianyangbj/article/details/108049482):

      1)什么是指针压缩?

         1.jdk1.6 update14开始,在64位操作系统中,JVM支持指针压缩

         2.jvm配置参数:UseCompressedOops,compressed--压缩、oop(ordinary object pointer)--对象指针

           3.启用指针压缩:-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops或者-XX:+UseCompressedClassPointers

       2)为什么要进行指针压缩?

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

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

           3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G),在CPU寄存器中的实际使用会将地址变成35位

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

           5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对Java对象寻址,所以堆内存不要大于32G为好   

    P4:对象大小与指针压缩:

      对象大小可以用jol-core包查看,引入依赖

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

      代码清单:

 1 public class JolTest {
 2 
 3     public static void main(String[] args) {
 4         ClassLayout layout1 = ClassLayout.parseInstance(new Object());
 5         System.out.println(layout1.toPrintable());
 6 
 7 
 8         System.out.println();
 9         ClassLayout layout2 = ClassLayout.parseInstance(new Object[]{});
10         System.out.println(layout2.toPrintable());
11 
12         
13         System.out.println();
14         ClassLayout layout3 = ClassLayout.parseInstance(new A());
15         System.out.println(layout3.toPrintable());
16     }
17 
18     public static class A{
19         boolean aBoolean;
20         byte aByte;
21         short aShort;
22         char aChar;
23         int aInt;
24         float aFloat;
25         long aLong;
26         double aDouble;
27         String string;
28         Object object;
29     }
30 }

      显示结果:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1) //markword
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0) //markword
      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


[Ljava.lang.Object; object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)//markword
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)//markword
      8     4                    (object header)                           4c 23 00 f8 (01001100 00100011 00000000 11111000) (-134208692)//类型指针
     12     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)//数组长度
     16     0   java.lang.Object Object;.<elements>                        N/A                                                    //对齐填充
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


com.map.JolTest$A object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)//markword
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)//markword
      8     4                    (object header)                           65 cc 00 f8 (01100101 11001100 00000000 11111000) (-134165403)//类型指针
     12     4                int A.aInt                                    0  //int4字节
     16     8               long A.aLong                                   0  //long8字节
     24     8             double A.aDouble                                 0.0 //double8字节
     32     4              float A.aFloat                                  0.0 //float4字节
     36     2              short A.aShort                                  0  //short2字节
     38     2               char A.aChar                                      //char2字节
     40     1            boolean A.aBoolean                                false //boolean1字节
     41     1               byte A.aByte                                   0 //byte1字节
     42     2                    (alignment/padding gap)                  //对齐填充2字节
     44     4   java.lang.String A.string                                  null //string开启指针压缩4字节,关闭指针压缩8字节
     48     4   java.lang.Object A.object                                  null //reference开启指针压缩4字节,关闭指针压缩8字节
     52     4                    (loss due to the next object alignment)  //对齐填充4字节
Instance size: 56 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total

三、JVM对象内存分配

    P1:对象内存分配流程

  

      1)新创建的对象它会进行逃逸分析,如果符合条件就会直接在栈上分配

      2)确定不会逃出作用域后,会尽可能的进行标量替换,分配在TLAB(空间在Eden区),分配失败进行CAS重试

      3)如果是大对象且不符合逃逸分析条件会在堆中进行分配      

四、栈上分配&逃逸分析&标量替换

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

    P2:对象逃逸分析:就是分析对象的动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,这样该对象就会逃出当前作用域

    代码清单:

 1 public User test1(){
 2     User user = new User();
 3     user.setId(1);
 4     user.setName("张三");
 5     return user; 
 6 }
 7 
 8 public void test2(){
 9     User user = new User();
10     user.setId(2);
11     user.setName("李四");
12     //保存到数据库
13 }

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

     2)JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK1.7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)

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

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

    P5:栈上分配示例:

      代码清单:

 1 /**
 2  * 栈上分配,标量替换
 3  * 代码调用了1亿次alloc(),如果是分配到堆上,大概需要1GB以上堆空间,如果堆空间小于该值,必然会触发GC。
 4  *
 5  * 使用如下参数不会发生GC
 6  * ‐Xmx15m ‐Xms15m ‐XX:+DoEscapeAnalysis ‐XX:+PrintGC ‐XX:+EliminateAllocations
 7  * 使用如下参数都会发生大量GC
 8  * ‐Xmx15m ‐Xms15m ‐XX:‐DoEscapeAnalysis ‐XX:+PrintGC ‐XX:+EliminateAllocations
 9  * ‐Xmx15m ‐Xms15m ‐XX:+DoEscapeAnalysis ‐XX:+PrintGC ‐XX:‐EliminateAllocations
10 */
11 public class AllotOnStack {
12 
13     public static void main(String[] args) {
14         long start = System.currentTimeMillis();
15         for (int i = 0; i < 100000000; i++) {
16             alloc();
17         }
18         long end = System.currentTimeMillis();
19         System.out.println(end - start);
20     }
21 
22     private static void alloc() {
23     User user = new User();
24     user.setId(1);
25     user.setName("张三");
26     }
27 }

     使用逃逸分析结果:

[GC (Allocation Failure)  4096K->660K(15872K), 0.0008183 secs]
7 //执行时间

    不使用逃逸分析结果:

。。。。。。。。。。。。略
[GC (Allocation Failure) 4816K->720K(15872K), 0.0003180 secs] [GC (Allocation Failure) 4816K->720K(15872K), 0.0003976 secs] [GC (Allocation Failure) 4816K->720K(15872K), 0.0003021 secs] [GC (Allocation Failure) 4816K->720K(15872K), 0.0006813 secs] [GC (Allocation Failure) 4816K->720K(15872K), 0.0003442 secs] [GC (Allocation Failure) 4816K->720K(15872K), 0.0003602 secs] [GC (Allocation Failure) 4816K->720K(15872K), 0.0004295 secs] [GC (Allocation Failure) 4816K->720K(15872K), 0.0007626 secs] [GC (Allocation Failure) 4816K->720K(15872K), 0.0007821 secs] [GC (Allocation Failure) 4816K->720K(15872K), 0.0008138 secs] 821 //执行时间

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

五、对象内存回收机制

  P1:对象在堆中的分配:

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

     MinorGC和FullGC有什么不同呢?

     * MinorGC/YoungGC:指发生新生代的垃圾收集动作,MinorGC非常频繁,回收速度一般也比较快。

     * MajorGC/FullGC:一般会回收老年代,年轻代,方法区的垃圾,MajorGC的速度一般会比MinorGC的慢10倍以上。

     大量的对象被分配到Eden区,Eden区满了后会触发MinorGC,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,survivor区的两块区域进行交换(From和To区)下一次Eden区满了后又会触发MinorGC,把Eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例很合适,让eden区尽量的大,survivor区够用即可,JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy

    代码清单:

1 //添加运行JVM参数: ‐XX:+PrintGCDetails
2 public class GCTest {
3 
4 public static void main(String[] args) throws InterruptedException {
5         byte[] allocation1 = new byte[55000*1024];
6     }
7 }

运行结果:

  Heap
  PSYoungGen total 75264K, used 61454K [0x000000076c300000, 0x0000000771700000, 0x00000007c0000000)
  eden space 64512K, 95% used [0x000000076c300000,0x000000076ff03b20,0x0000000770200000)
  from space 10752K, 0% used [0x0000000770c80000,0x0000000770c80000,0x0000000771700000)
  to space 10752K, 0% used [0x0000000770200000,0x0000000770200000,0x0000000770c80000)
  ParOldGen total 172032K, used 0K [0x00000006c4800000, 0x00000006cf000000, 0x000000076c300000)
  object space 172032K, 0% used [0x00000006c4800000,0x00000006c4800000,0x00000006cf000000)
  Metaspace used 3227K, capacity 4496K, committed 4864K, reserved 1056768K
  class space used 350K, capacity 388K, committed 512K, reserved 1048576K

    结论:Eden区内存几乎已经被分配完全(即使程序什么也不做,新生代也会使用至少几M内存),假如我们再来一创建一些较小的对象呢?

    代码清单:

1 //添加运行JVM参数: ‐XX:+PrintGCDetails
2 public class GCTest {
3 
4 public static void main(String[] args) throws InterruptedException {
5         byte[] allocation1 = new byte[55000*1024];
6         byte[] allocation2 = new byte[8000*1024];
7     }
8 }

  运行结果:

  [GC (Allocation Failure) [PSYoungGen: 60164K->792K(75264K)] 60164K->55800K(247296K), 0.0299496 secs] [Times: user=0.09 sys=0.02, real=0.03 secs]
  Heap
  PSYoungGen total 75264K, used 9437K [0x000000076c300000, 0x0000000775600000, 0x00000007c0000000)
  eden space 64512K, 13% used [0x000000076c300000,0x000000076cb71600,0x0000000770200000)
  from space 10752K, 7% used [0x0000000770200000,0x00000007702c6030,0x0000000770c80000)
  to space 10752K, 0% used [0x0000000774b80000,0x0000000774b80000,0x0000000775600000)
  ParOldGen total 172032K, used 55008K [0x00000006c4800000, 0x00000006cf000000, 0x000000076c300000)
  object space 172032K, 31% used [0x00000006c4800000,0x00000006c7db8010,0x00000006cf000000)
  Metaspace used 3227K, capacity 4496K, committed 4864K, reserved 1056768K
  class space used 350K, capacity 388K, committed 512K, reserved 1048576K

  总结:因为给allocation2分配内存的时候eden区内存几乎被分配完了,所以说会触发一次MinorGC,GC期间虚拟机发现allocation1无法存入Survior空间,所以只好把新生代的对象提前转移到老年代去,老年代上的空间足够存放allocation1,所以就不会出现FullGC。执行MinorGC后,后面分配的对象如果能够存在Eden区的话,还是会在Eden区分配内存

    代码清单:

1 //添加运行JVM参数: ‐XX:+PrintGCDetails
2 public class GCTest {
3 
4 public static void main(String[] args) throws InterruptedException {
5         byte[] allocation1 = new byte[55000*1024];
6         byte[] allocation2 = new byte[1*1024];
7     }
8 }

 运行结果:

  Heap
  PSYoungGen total 75264K, used 61454K [0x000000076c300000, 0x0000000771700000, 0x00000007c0000000)
  eden space 64512K, 95% used [0x000000076c300000,0x000000076ff03b20,0x0000000770200000)
  from space 10752K, 0% used [0x0000000770c80000,0x0000000770c80000,0x0000000771700000)
  to space 10752K, 0% used [0x0000000770200000,0x0000000770200000,0x0000000770c80000)
  ParOldGen total 172032K, used 0K [0x00000006c4800000, 0x00000006cf000000, 0x000000076c300000)
  object space 172032K, 0% used [0x00000006c4800000,0x00000006c4800000,0x00000006cf000000)
  Metaspace used 3227K, capacity 4496K, committed 4864K, reserved 1056768K
  class space used 350K, capacity 388K, committed 512K, reserved 1048576K

  总结:Eden区够用,不会触发YoungGC

     P2:对象如何进入老年代?

      1)大对象直接进入老年代:大对象就是需要大量连续内存空间的对象(比如:字符串、数组)JVM参数-XX:PretenureSizeThreshold可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在Serial和ParNew两个收集器下有效。比如设置JVM参数:-XX:PretenureSizeThreshold=10000(单位是字节)-XX:+UseSerialGC,再执行下上面的第一个程序会发现大对象直接进入了老年代

      为什么大对象直接进入老年代?

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

      2)长期存活的对象将进入老年代:既然虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须识别哪些对象放在新生代,哪些对象应该放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)标识。

      如果对象在Eden出生并经过第一次MinorGC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微不同,最高不会超过15),就会被晋升到老年代中,对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。

      3)动态年龄判断:当前放对象的Survivor区域里(其中一块区域,放对象的那块S1区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个对象总和超过了Survivor的S1区的50%,此时就会把年龄n(含)以上的对象放入老年代。这个规则其实希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在MinorGC之后触发的

      4)老年代空间分配担保:

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

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

      就会看一个"-XX:-HandlePromotionFailure"(jdk1.8默认设置了)的参数是否设置了

      如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次MinorGC后进入老年代的对象的平均大小

      如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次FullGC,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生OOM

      如果触发MinorGC之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发FullGC,FullGC完之后如果还是没有空间放MinorGC之后存活的对象,则也会发生OOM

六、对象内存回收

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

  P1:引用计数算法:

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

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

      所谓对象之间的相互引用问题,如下面代码所示:除了对象objA和objB相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致他们的引用计数器都不为0,于是引用计数算法无非通知GC回收器回收他们。

    代码清单:

 1 public class ReferenceCountingGc {
 2 Object instance = null;
 3 
 4     public static void main(String[] args) {
 5     ReferenceCountingGc objA = new ReferenceCountingGc();
 6     ReferenceCountingGc objB = new ReferenceCountingGc();
 7     objA.instance = objB;
 8     objB.instance = objA;
 9     objA = null;
10     objB = null;
11     }
12 }

  P2:可达性分析算法:

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

    GCRoots的根节点:虚拟机栈(栈帧的本地变量表)中引用的对象、方法区中的静态变量的引用对象、方法区中常量引用的对象、本地方法栈中的JNI(Native方法)的引用对象

  P3:常见的引用类型:

    强引用:普通对象的变量引用

 1 public static User user = new User(); 

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

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

    弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会之间回收掉

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

    虚引用:他是最弱的一种引用

  P4:finalize()方法最终判断对象是否存活:

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

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

    1)第一次标记并进行一次筛选:筛选的条件是此对象是否有必要执行finalize()方法,当对象没有重写finalize方法,对象直接被回收

    2)第二标记:如果这个对象覆盖了finalize方法,finalize方法是对象逃脱死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关系即可,譬如把自己赋值给某个类或对象的成员变量,那在第二次标记时它将移除"即将回收"的集合。如果对象这时候还没有逃脱,那基本上它就真的被回收了

    注意:一个对象的finalize()方法只会被执行一次,也就是通过调用finalize()方法自我救命的机会就只有一次。

    代码清单:

 1 public class User {
 2 
 3     private Integer id;
 4     private String name;
 5 
 6     public Integer getId() {
 7         return id;
 8     }
 9 
10     public void setId(Integer id) {
11         this.id = id;
12     }
13 
14     public String getName() {
15         return name;
16     }
17 
18     public void setName(String name) {
19         this.name = name;
20     }
21 
22     @Override
23     protected void finalize() throws Throwable {
24         OOMTest.list.add(this);//救活自己,重新进入GCRoots引用链
25         System.out.println("id:"+this.id+"**********即将被回收");
26     }
27 
28     public User(Integer id, String name) {
29         this.id = id;
30         this.name = name;
31     }
32 
33     public User() {}
34 }
 1 public class OOMTest {
 2 
 3     public static List<User> list = new ArrayList<>();
 4 
 5     public static void main(String[] args) {
 6         List<Object> list = new ArrayList<>();
 7         int i = 0;
 8         int j = 0;
 9         while (true) {
10             list.add(new User(i++, UUID.randomUUID().toString()));//正数是被GCRoots引用
11             new User(j--, UUID.randomUUID().toString());//负数是没有被GCRoots引用
12         }
13     }
14 }

  运行结果:

1 id:-329506**********即将被回收
2 id:-329507**********即将被回收
3 id:-329508**********即将被回收
4 id:-329509**********即将被回收
5 id:-329510**********即将被回收

   P5:判断一个类是无用的类:

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

    同时满足下面3个条件才能算是"无用的类"

    * 该类所有的实例已经被回收,也就是Java堆中不存在该类的任何实例(通过对象头的类型指针指向方法区)

    * 加载该类的ClassLoader已经被回收(每个类加载器都有自己的空间,他会引用着被他加载的所有类)

    * 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

原文地址:https://www.cnblogs.com/Mapi/p/14329303.html