JVM系列之深入理解对象

 

目录

一、JVM中普通对象的创建过程

二、对象的内存布局

三、对象的访问

四、判断对象的存活(GC Roots)

五、各种引用(强、软、弱、虚)

六、对象的分配策略(栈上分配对象、堆中的优化技术TLAB、对象优先在Eden分配、空间分配担保、大对象直接进入老年代、长期存活对象进入老年代、动态对象年龄判定)


一、JVM中普通对象的创建过程


1、类加载 :class文件通过ClassLoader加载到内存JVM后解释执行成机器语言,给CPU去执行;

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

3、分配内存:接下来虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。如何分配呢?(1.针对规整的内存空间使用指针碰撞、2.针对不规整的内存空间使用到空闲列表:记录空闲的内存位置);这就要看我们堆内存使用的是什么垃圾收集器。多线程在堆内存存放对象的时候,会出现抢占内存的情况,为了保证并发安全问题:使用CAS乐观锁机制和本地线程分配缓冲,一般会默认开启本地线程分配缓冲;

4、内存空间初始化:设置一些初始值,基本数据类型初始值,引用类型null;

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

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

二、对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。实例数据(成员变量作为对象的属性,当然是放在堆里了。对象在堆里,对象中的内容就是各种字段)

第一部分对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个java数组,那么在对象头中还有一块用于记录数组长度的数据。

第二部分实例数据:类的成员变量在不同对象中各不相同,都有自己的存储空间(成员变量在堆中的对象中的实例数据中),基本类型和引用类型的成员变量都在这个对象的空间中,作为一个整体存储在堆。而类的方法却是该类的所有对象共享的,只有一套,对象使用方法的时候方法才被压入栈,方法不使用则不占用内存。而类中的static静态的、final常量的成员就保存在方法区了。

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。

三、对象的访问

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

对Sun HotSpot而言,它是使用直接指针访问方式进行对象访问的,使用直接指针访问, reference中存储的直接就是对象地址。

四、判断对象的存活(GC Roots)

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

判断对象存活一般有两种方式:引用计数法、可达性分析法

1、引用计数法:在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用失效时,计数器减1.Python在用,但主流虚拟机没有使用,因为存在对象相互引用的情况,这个时候需要引入额外的机制来处理,这样做影响效率。

2、可达性分析:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

作为GC Roots的对象包括下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。
  5. 等等,上面就是常说的GC Roots

上面指的是对象的回收,但一般我们只讨论对象的回收,那么对于某个类的回收条件有哪些?

1.该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例

2.加载该类的ClassLoader已经被回收

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

五、各种引用(强、软、弱、虚)

强引用  =

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

软引用  SoftReference

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

弱引用  WeakReference

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

虚引用  PhantomReference

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

  

六、对象的分配策略(栈上分配对象、堆中的优化技术TLAB、对象优先在Eden分配、空间分配担保、大对象直接进入老年代、长期存活对象进入老年代、动态对象年龄判定)

上面大概的介绍了一下对象本身,但是我们知道对象是在内存空间分配的,JVM这么多个内存分区,堆内存也都具体细分区,那么对象是如何一个怎样的分配策略呢?

策略一、栈中分配对象

new出来的对象在符合逃逸分析(对象的作用域只在方法内,不传递给其他方法,或者不被其他线程使用,也就是说没有逃出去)的时候会在栈上分配空间,而不会占用堆内存的空间。如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。3种常见的指针逃逸场景。分别是 全局变量赋值,方法返回值,实例引用传递

在Java虚拟机中,对象是创建在Java堆中,这是每一位有经验Java开发人员在熟悉不过的常识。Java堆中的对象是对于各个线程都是共享和可见的,主要持有这个对象的引用,就可以轻松访问存储在对象的数据。虚拟机的垃圾收集器也可以回收不再使用的对象,但是回收动作无论是筛选可回收对象,还是回收和整理内存都需要消耗时间和性能。如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将是一个大胆而又很不错的注意。大家都知道,JVM虚拟机中80-90%的对象都是朝生夕灭的,如果对象所占用的内存空间随着栈帧出栈而销毁,那垃圾收集器的压力就会大大减小。

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

   static void allocate() {
       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;
      }
  }
}

这段代码在调用的过程中 myObject这个对象属于全局逃逸,JVM可以做栈上分配。

JVM默认是开启逃逸分析的,如果关闭逃逸分析开关的话,对象就会在堆上进行空间分配,JVM在频繁的进行垃圾回收(GC),正是这一块的操作(对比比栈上分配)导致性能有较大的差别。

策略二、堆中的优化技术,本地线程分配缓冲(TLAB)

会在Eden区划一块占Eden区1%的区域来当做TLAB,给每个线程 单独放小对象,就不会出现先占某块内存的情况。

策略三、对象优先在Eden分配

新生的没有年龄的对象优先进入Eden区

为什么新生代要分为Eden、from、to三个区?据统计朝生夕灭的是90%的对象,8:1:1的分配比例正好可以达到这一统计规律,每次有90%的空间存放新生对象。而from和to使用的是复制回收算法。

策略四、大对象直接进入老年代

因为Eden区、From区、To区、Older区,一般是8:1:1:20的比例,当大对象在前三个区都分配不下就直接进入older区,即使在Eden区放得下,当晋级的时候from和to区也放不下。所以就有个这个大对象直接进入老年代的策略。

JVM这个参数可以多大的对象时大对象。

策略五、长期存活对象进入老年代

新生的对象会进入Eden区,经历过一次GC后,会晋级进入From区,之后的几次回收,如果对象会在from区和to区复制来复制去(每存活一次就会在对象头的GC分代年龄加1)依旧能存活几次,就会进入老年代。

策略六、空间分配担保

JVM提供了空间分配担保策略,就是你怎么知道older区,每次当有晋级对象或者大对象有空间给他们分配呢?那又不可能每次放对象都调用fullGC,会很消耗性能的,所以说空间担保就保证了这一点。你就放心的放吧。如果真的放不下了,担保失败了就会去触发FullGc/majorGc;

策略七、动态对象年龄判定

from区和to区统称为survivor区幸存区,一般分代的对象年龄要在survivor区达到15才能晋级进去老年代,动态对象年龄判定可以在对象不到15就可以进入老年代,提前进入。

猜你喜欢

转载自blog.csdn.net/sunbinkang/article/details/111385796