精华内容
下载资源
问答
  • 对象属性过多内存大
    万次阅读
    2020-09-02 17:25:09

    对象的结构

    在JVM中,一般来说,Java对象都是分配在堆中,那么对象在堆中长什么样呢?

    在这里插入图片描述

    对象头包含以下几个部分:

    • MarkWord:包含对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode、分代年龄等。
    • Class Pointer:一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例,默认开启压缩指针,占32位,关闭压缩,占64位。
    • 数组的长度:可选的,只有当对象是一个数组对象时才会有这个部分。
    • 对象的属性:占用内存空间取决于对象的属性数量和类型。
    • 对齐:为了保证对象头的字节数是8的倍数。

    在这里插入图片描述

    MarkWord

    MarkWord用来存储线程的锁状态、hashcode、分代年龄等信息,当对象处于不同状态时,MarkWord中的数据也跟随着对象的状态而变化。

    以上是Java对象处于5种不同状态时,Mark Word中64个bit的表现形式,上面每一行代表对象处于某种状态时的样子。

    注意以下几点:

    • 偏向锁标志位+锁标志位共同表示当前对象锁的状态。
    • 分代年龄:在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC(除了CMS垃圾收集器)的年龄阈值为15,并发GC(CMS垃圾收集器)的年龄阈值为6。由于分代年龄只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold参数最大值为15的原因。
    • hashCode:31位的对象标识hashCode,采用延迟计算方式,只有在使用时才会计算,并会将结果写到该对象头中。如果对象在加锁前计算了hashcode,此时状态为无锁不可偏向,因为hashcode占用了偏向锁标志位的数据区域,这里的hashcode是指系统中最初始的hashcode,也就是调用基类Object.hashCode()方法产生的,而不是重写Object.hashCode()方法产生的hashcode,也可以使用System.identityHashCode(object)方法来获取。

    项目中可以引入JOL(Java Object Layout)来打印java对象头在内存中的字节码。

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

    下面通过代码对上面的5中状态进行验证:

    无锁不可偏向

    JVM刚启动时4s(默认值为4s,可以通过JVM参数-XX:BiasedLockingStartupDelay修改)之内创建的对象的锁状态为无锁不可偏向。

        @Test
        public void testNoLockByBeforeFiveSecond() {
            Object object = new Object();
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    

    运行结果如下:

    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
    

    当一个对象计算了hashCode,锁的状态就会变成无锁不可偏向,因为hashcode占用了偏向锁标志位的数据区域。

        @Test
        public void testNoLockByHashCode() throws InterruptedException {
            TimeUnit.SECONDS.sleep(5); // 参数是4s,这里用休眠5s
            Object object = new Object();
            System.out.println(Integer.toHexString(object.hashCode()));
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    

    运行结果如下:

    5e8c92f4
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           01 f4 92 8c (00000001 11110100 10010010 10001100) (-1936526335)
          4     4        (object header)                           5e 00 00 00 (01011110 00000000 00000000 00000000) (94)
          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
    

    可以发现对象的hashcode为5e8c92f4,与对象头中的hashcode一致。

    匿名偏向锁

    JVM启动4s后创建的对象锁的状态为匿名偏向锁。

        public void testAnonymousBiasLock() throws InterruptedException {
            TimeUnit.SECONDS.sleep(5); // 或者设置JVM args:-XX:BiasedLockingStartupDelay=0
            Object object = new Object();
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    

    运行结果如下:

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
          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
    

    偏向锁

    当没有发生资源的竞争,锁会偏向第一个持有锁的线程。

        @Test
        public void testBiasLock() throws InterruptedException {
            TimeUnit.SECONDS.sleep(5);
            Object object = new Object();
            synchronized (object) {
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }
    

    运行结果如下:

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 98 4e 00 (00000101 10011000 01001110 00000000) (5150725)
          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
    

    注意此时对象头中会存有一个线程ID,但是此ID不是java中thread对象getId()方法返回的id,也不是操作系统中ID,这个ID是JVM生成的,可以通过jstack命令查看线程的ID。

    $ jstack 16344
    ....
    "main" #1 prio=5 os_prio=0 tid=0x0000000002239800 nid=0x4c18 in Object.wait() [0x000000000280e000]
       java.lang.Thread.State: WAITING (on object monitor)
            at java.lang.Object.wait(Native Method)
            - waiting on <0x000000076b605d30> (a java.lang.Thread)
            at java.lang.Thread.join(Thread.java:1252)
            - locked <0x000000076b605d30> (a java.lang.Thread)
            at java.lang.Thread.join(Thread.java:1326)
            at com.morris.concurrent.syn.upgrade.SynchronizedStatus.testBiasLock(SynchronizedStatus.java:74)
    ....
    

    tid对应对象头中的线程id,而nid对应操作系统级别的线程id。

    轻量级锁

    当对象头中的锁偏向于线程T1,T1释放锁后,其他线程来获取,此时偏向锁会升级为轻量级锁(线程之间交替执行,没有资源竞争)。

        /**
         * 偏向锁 -> 轻量级锁
         */
        @Test
        public void testLightLock() throws InterruptedException {
            TimeUnit.SECONDS.sleep(5);
            Object object = new Object();
    
            Thread t = new Thread(() -> {
                synchronized (object) {
                }
            });
            t.start();
            t.join();
    
            synchronized (object) {
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
    
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    

    运行结果如下:

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           f0 e0 7c 02 (11110000 11100000 01111100 00000010) (41738480)
          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
    
    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
    

    当JVM刚启动或者使用JVM参数-XX:-UseBiasedLocking=false禁用了偏向锁,这时无锁不可偏向锁就会升级为轻量级锁。

        @Test
        public void testLightLock2() throws InterruptedException {
            Object object = new Object();
            synchronized (object) {
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }
    

    运行结果如下:

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           88 e5 7a 02 (10001000 11100101 01111010 00000010) (41608584)
          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
    

    注意轻量级锁释放后,对象头中锁的状态会变为无锁不可偏向。

    重量级锁

    当产生了资源竞争,轻量级锁会升级为重量级锁。

        @Test
        public void testHeavyLock() throws InterruptedException {
            Object object = new Object();
    
            Thread t = new Thread(() -> {
                synchronized (object) {
                }
            });
            t.start();
    
            synchronized (object) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }
    

    运行结果如下:

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           fa c1 ec 1b (11111010 11000001 11101100 00011011) (468500986)
          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
    

    Class Pointer

    JVM通过这个指针确定对象是哪个类的实例,该指针的长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

    如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,默认开启,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

    • 每个Class的属性指针(即静态变量)
    • 每个对象的属性指针(即对象变量)
    • 普通对象数组的每个元素指针

    当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如JDK8中指向元空间的Class对象指针、本地变量、堆栈元素、入参、返回值和NULL指针等。

    问题:Object object = new Object()中object对象到底占用多少个字节?

    答案:16个字节。

    开启压缩指针:markword(64bit) + classpointer(32bit) + 对齐(32bit) = 16byte 。

    不开启压缩指针:markword(64bit) + classpointer(64bit) = 16byte 。

    更多相关内容
  • C++对象内存模型

    千次阅读 2016-11-21 14:37:07
    谈VC++对象模型 (美)简.格雷 程化 译 译者前言 一个C++程序员,想要进一步提升技术水平的话,应该多了解一些语言的语意细 节。对于使用VC++的程序员来说,还应该了解一些VC++对于C++的诠释。 Inside the ...

    谈VC++对象模型
    (美)简.格雷
    程化    译

    译者前言

    一个C++程序员,想要进一步提升技术水平的话,应该多了解一些语言的语意细 节。对于使用VC++的程序员来说,还应该了解一些VC++对于C++的诠释。 Inside the C++ Object Model虽然是一本好书,然而,书的篇幅多一些,又和具体的VC++关系小一些。因此,从篇幅和内容来看,译者认为本文是深入理解C++对象模型比较好 的一个出发点。
    这篇文章以前看到时就觉得很好,旧文重读,感觉理解得更多一些了,于是产生了翻译出来,与大家共享的想法。虽然文章不长,但时间有限,又若干次在翻译时打盹睡着,拖拖拉拉用了小一个月。
    一方面因本人水平所限,另一方面因翻译时经常打盹,错误之处恐怕不少,欢迎大家批评指正。

    本文原文出处为MSDN。如果你安装了MSDN,可以搜索到C++ Under the Hood。否则也可在网站上找到http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnarvc/html/jangrayhood.asp 

    1 前言

    了解你所使用的编程语言究竟是如何实现的,对于C++程序员可能特别有意义。首 先,它可以去除我们对于所使用语言的神秘感,使我们不至于对于编译器干的活感到完全不可思议;尤其重要的是,它使我们在Debug和使用语言高级特性的时 候,有更多的把握。当需要提高代码效率的时候,这些知识也能够很好地帮助我们。

    本文着重回答这样一些问题:
    1* 类如何布局?
    2* 成员变量如何访问?
    3* 成员函数如何访问?
    4* 所谓的“调整块”(adjuster thunk)是怎么回事?
    5* 使用如下机制时,开销如何:
      * 单继承、多重继承、虚继承
      * 虚函数调用
      * 强制转换到基类,或者强制转换到虚基类
      * 异常处理
    首先,我们顺次考察C兼容的结构(struct)的布局,单继承,多重继承,以及虚继承;
    接着,我们讲成员变量和成员函数的访问,当然,这里面包含虚函数的情况;
    再接下来,我们考察构造函数,析构函数,以及特殊的赋值操作符成员函数是如何工作的,数组是如何动态构造和销毁的;
    最后,简单地介绍对异常处理的支持。

    对每个语言特性,我们将简要介绍该特性背后的动机,该特性自身的语意(当然,本 文决不是“C++入门”,大家对此要有充分认识),以及该特性在微软的 VC++中是如何实现的。这里要注意区分抽象的C++语言语意与其特定实现。微软之外的其他C++厂商可能提供一个完全不同的实现,我们偶尔也会将 VC++的实现与其他实现进行比较。

    2 类布局

    本节讨论不同的继承方式造成的不同内存布局。

    2.1 C结构(struct)

    由于C++基于C,所以C++也“基本上”兼容C。特别地,C++规范在“结构”上使用了和C相同的,单的内存布局原则:成员变量按其被声明的顺序排列,按具体实现所规定的对齐原则在内存地址上对齐。 所有的C/C++厂商都保证他们的C/C++编译器对于有效的C结构采用完全相同的布局。这里,A是一个简单的C结构,其成员布局和对齐方式都一目了然

    1. struct  A {  
    2.    char  c;  
    3.    int  i;  
    4. };  

    译者注:从上图可见,A在内存中占有8个字节,按照声明成员的顺序,前4个字节包含一个字符(实际占用1个字节,3个字节空着,补对齐),后4个字节包含一个整数。A的指针就指向字符开始字节处。

    2.2 有C++特征的C结构

    当然了,C++不是复杂的C,C++本质上是面向对象的语言:  继承、封装,以及多态 。原始的C结构经过改造,成了面向对象世界的基石——类。除了成员变量外,C++类还可以封装成员函数和其他东西。然而,有趣的是,除非 为了实现虚函数和虚继承引入的隐藏成员变量外,C++类实例的大小完全取决于一个类及其基类的成员变量!成员函数基本上不影响类实例的大小。

    这里提供的B是一个C结构,然而,该结构有一些C++特征:控制成员可见性的“public/protected/private”关键字、成员函数、静态成员,以及嵌套的类型声明。虽然看着琳琅满目,实际上,只有成员变量才占用类实例的空间 。要注意的是,C++标准委员会不限制由“public/protected/private”关键字分开的各段在实现时的先后顺序,因此,不同的编译器实现的内存布局可能并不相同。( 在VC++中,成员变量总是按照声明时的顺序排列)。

    1. struct  B {  
    2. public :  
    3.    int  bm1;  
    4. protected :  
    5.    int  bm2;  
    6. private :  
    7.    int  bm3;  
    8.    static   int  bsm;  
    9.    void  bf();  
    10.    static   void  bsf();  
    11.    typedef   void * bpv;  
    12.    struct  N { };  
    13. };  

    译者注:B中,为何static int bsm不占用内存空间?因为它是静态成员,该数据存放在程序的数据段 中,不在类实例中。

    2.3 单继承

    C++ 提供继承的目的是在不同的类型之间提取共性。比如,科学家对物种进行分类,从而有种、属、纲等说法。有了这种层次结构,我们才可能将某些具备特定性质的东 西归入到最合适的分类层次上,如“怀孩子的是哺乳动物”。由于这些属性可以被子类继承,所以,我们只要知道“鲸鱼、人”是哺乳动物,就可以方便地指出“鲸 鱼、人都可以怀孩子”。那些特例,如鸭嘴兽(生蛋的哺乳动物),则要求我们对缺省的属性或行为进行覆盖。
    C++中的继承语法很简单,在子类后加上“:base”就可以了。下面的D继承自基类C。

    1. struct  C {  
    2.    int  c1;  
    3.    void  cf();  
    4. };  

    1. struct  D : C {  
    2.    int  d1;  
    3.    void  df();  
    4. }; 

    既然派生类要保留基类的所有属性和行为,自然地,每个派生类的实例都包含了一份完整的基类实例数据。在D中,并不是说基类C的数据一定要放在D的数据之前,只不过这样放的话,能够保证D中的C对象地址,恰好是D对象地址的第一个字节。这种安排之下,有了派生类D的指针,要获得基类C的指针,就不必要计算偏移量 了。几乎所有知名的C++厂商都采用这种内存安排(基类成员在前)。 在单继承类层次下,每一个新的派生类都简单地把自己的成员变量添加到基类的成员变量之后 。 看看上图,C对象指针和D对象指针指向同一地址。

    2.4 多重继承

    大多数情况下,其实单继承就足够了。但是,C++为了我们的方便,还提供了多重继承。

    比如,我们有一个组织模型,其中有经理类(分任务),工人类(干活)。那么,对 于一线经理类,即既要从上级经理那里领取任务干活,又要向下级工人分任务的角色来说,如何在类层次中表达呢?单继承在此就有点力不胜任。我们可以安排经理 类先继承工人类,一线经理类再继承经理类,但这种层次结构错误地让经理类继承了工人类的属性和行为。反之亦然。当然,一线经理类也可以仅仅从一个类(经理 类或工人类)继承,或者一个都不继承,重新声明一个或两个接口,但这样的实现弊处太多:多态不可能了;未能重用现有的接口;最严重的是,当接口变化时,必 须多处维护。最合理的情况似乎是一线经理从两个地方继承属性和行为——经理类、工人类。

    C++就允许用多重继承来解决这样的问题:

    1. struct  Manager ... { ... };  
    2. struct  Worker ... { ... };  
    3. struct  MiddleManager : Manager, Worker { ... };  

    这样的继承将造成怎样的类布局呢?下面我们还是用“字母类”来举例:

     

    1. struct  E {  
    2.    int  e1;  
    3.    void  ef();  
    4. };  

     

    1. struct  F : C, E {  
    2.    int  f1;  
    3.    void  ff();  
    4. };  
    结构F从C和E多重继承得来。单继承相同的是,F实例拷贝了每个基类的所有数据。 与单继承不同的是,在多重继承下,内嵌的两个基类的对象指针不可能全都与派生类对象指针相同: 
    1. F f;  
    2. // (void*)&f == (void*)(C*)&f;   
    3. // (void*)&f < (void*)(E*)&f;  
    译者注:上面那行说明C对象指针与F对象指针相同,下面那行说明E对象指针与F对象指针不同。

    观察类布局,可以看到F中内嵌的E对象,其指针与F指针并不相同。正如后文讨论强制转化和成员函数时指出的,这个偏移量会造成少量的调用开销。

    具体的编译器实现可以自由地选择内嵌基类和派生类的布局。 VC++ 按照基类的声明顺序 先排列基类实例数据,最后才排列派生类数据。 当然,派生类数据本身也是按照声明顺序布局的(本规则并非一成不变 ,我们会看到,当一些基类有虚函数而另一些基类没有时,内存布局并非如此)。

    2.5 虚继承

    回到我们讨论的一线经理类例子。让我们考虑这种情况:如果经理类和工人类都继承自“雇员类”,将会发生什么?

    1. struct  Employee { ... };  
    2. struct  Manager : Employee { ... };  
    3. struct  Worker : Employee { ... };  
    4. struct  MiddleManager : Manager, Worker { ... };  
    如果经理类和工人类都继承自雇员类,很自然地,它们每个类都会从雇员类获得一份数据拷贝。如 果不作特殊处理,一线经理类的实例将含有两个 雇员类实例,它们分别来自两个雇员基类 。 如果雇员类成员变量不多,问题不严重;如果成员变量众多,则那份多余的拷贝将造成实例生成时的严重开销。更糟的是,这两份不同的雇员实例可能分别被修改,造成数据的不一致。因此,我们需要让经理类和工人类进行特殊的声明,说明它们愿意共享一份雇员基类实例数据。

    很不幸,在C++中,这种“共享继承”被称为“虚继承” ,把问题搞得似乎很抽象。虚继承的语法很简单,在指定基类时加上virtual关键字即可。

    1. struct  Employee { ... };  
    2. struct  Manager :  virtual  Employee { ... };  
    3. struct  Worker :  virtual  Employee { ... };  
    4. struct  MiddleManager : Manager, Worker { ... }; 
    使用虚继承,比起单继承和多重继承有更大的实现开销、调用开销。回忆一下,在单继承和多重继承的情况下,内嵌的基类实例地址比起派生类实例地址来,要么地址相同(单继承,以及多重继承的最靠左基类) ,要么地址相差一个固定偏移量(多重继承的非最靠左基类) 。 然而,当虚继承时,一般说来,派生类地址和其虚基类地址之间的偏移量是不固定的,因为如果这个派生类又被进一步继承的话,最终派生类会把共享的虚基类实例数据放到一个与上一层派生类不同的偏移量处。 请看下例:

     
    1. struct  G :  virtual  C {  
    2.    int  g1;  
    3.    void  gf();  
    4. };  
    译者注: GdGvbptrG(In G, the displacement of G’s virtual base pointer to G)意 思是:在G中,G对象的指针与G的虚基类表指针之间的偏移量,在此可见为0,因为G对象内存布局第一项就是虚基类表指针; GdGvbptrC(In G, the displacement of G’s virtual base pointer to C)意思是:在G中,C对象的指针与G的虚基类表指针之间的偏移量,在此可见为8。

     
     

    1. struct  H :  virtual  C {  
    2.    int  h1;  
    3.    void  hf();  
    4. };  
     
    1. struct  I : G, H {  
    2.    int  i1;  
    3.    void  _if();  
    4. };  
    暂时不追究vbptr成员变量从何而来。 从上面这些图可以直观地看到,在G对象中,内嵌的C基类对象的数据紧跟在G的数据之后,在H对象中,内嵌的C基类对象的数据也紧跟在H的数据之后。但是, 在I对象中,内存布局就并非如此了。VC++实现的内存布局中,G对象实例中G对象和C对象之间的偏移,不同于I对象实例中G对象和C对象之间的偏移。当 使用指针访问虚基类成员变量时,由于指针可以是指向派生类实例的基类指针,所以,编译器不能根据声明的指针类型计算偏移,而必须找到另一种间接的方法,从 派生类指针计算虚基类的位置。 
    VC++ 中,对每个继承自虚基类的类实例,将增加一个隐藏的“虚基类表指针”(vbptr) 成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类 而言,“虚基类表指针”与虚基类之间的偏移量。 
    其 它的实现方式中,有一种是在派生类中使用指针成员变量。这些指针成员变量指向派生类的虚基类,每个虚基类一个指针。这种方式的优点是:获取虚基类地址时, 所用代码比较少。然而,编译器优化代码时通常都可以采取措施避免重复计算虚基类地址。况且,这种实现方式还有一个大弊端:从多个虚基类派生时,类实例将占 用更多的内存空间;获取虚基类的虚基类的地址时,需要多次使用指针,从而效率较低等等。

    在VC++中,G拥有一个隐藏的“虚基类表指针”成员,指向一个虚基类表,该表的第二项是G dGvbptrC。(在G中,虚基类对象C的地址与G的“虚基类表指针”之间的偏移量  当对于所有的派生类来说偏移量不变时,省略“d”前的前缀))。比如,在32位平台上,GdGvptrC是8个字节。同样,在I实例中的G对象实例也有 “虚基类表指针”,不过该指针指向一个适用于G处于I之中” 的虚基类表,表中一项为IdGvbptrC,值为20。

    观察前面的G、H和I, 我们可以得到如下关于VC++虚继承下内存布局的结论:
    1 首先排列非虚继承的基类实例;
    2 有虚基类时,为每个基类增加一个隐藏的vbptr,除非已经从非虚继承的类那里继承了一个vbptr;
    3 排列派生类的新数据成员;
    4 在实例最后,排列每个虚基类的一个实例。

    该布局安排使得虚基类的位置随着派生类的不同而“浮动不定”,但是,非虚基类因此也就凑在一起,彼此的偏移量固定不变。

    3 成员变量

    介绍了类布局之后,我们接着考虑对不同的继承方式,访问成员变量的开销究竟如何。

    没有继承: 没有任何继承关系时,访问成员变量和C语言的情况完全一样:从指向对象的指针,考虑一定的偏移量即可。

    1. C* pc;  
    2. pc->c1; // *(pc + dCc1);   
    译者注:pc是指向C的指针。
    a. 访问C的成员变量c1,只需要在pc上加上固定的偏移量dCc1(在C中,C指针地址与其c1成员变量之间的偏移量值),再获取该指针的内容即可。

    单继承: 由于派生类实例与其基类实例之间的偏移量是常数0,所以,可以直接利用基类指针和基类成员之间的偏移量关系,如此计算得以简化。

    1. D* pd;  
    2. pd->c1; // *(pd + dDC + dCc1); // *(pd + dDc1);   
    3. pd->d1; // *(pd + dDd1);  
    译者注:D从C单继承,pd为指向D的指针。
    a. 当访问基类成员c1时,计算步骤本来应该为“pd+dDC+dCc1”,即为先计算D对象和C对象之间的偏移,再在此基础上加上C对象指针与成员变量c1 之间的偏移量。然而,由于dDC恒定为0,所以直接计算C对象地址与c1之间的偏移就可以了。
    b. 当访问派生类成员d1时,直接计算偏移量。

    多重继承 :虽然派生类与某个基类之间的偏移量可能不为0,然而,该偏移量总是一个常数。只要是个常数,访问成员变量,计算成员变量偏移时的计算就可以被简化。可见即使对于多重继承来说,访问成员变量开销仍然不大。

    1. F* pf;  
    2. pf->c1; // *(pf + dFC + dCc1); // *(pf + dFc1);   
    3. pf->e1; // *(pf + dFE + dEe1); // *(pf + dFe1);   
    4. pf->f1; // *(pf + dFf1);   
    译者注:F继承自C和E,pf是指向F对象的指针。
    a. 访问C类成员c1时,F对象与内嵌C对象的相对偏移为0,可以直接计算F和c1的偏移;
    b. 访问E类成员e1时,F对象与内嵌E对象的相对偏移是一个常数,F和e1之间的偏移计算也可以被简化;
    c. 访问F自己的成员f1时,直接计算偏移量。

    虚继承: 当类有虚基类时,访问非虚基类的成员仍然是计算固定偏移量的问题。然而,访问虚基类的成员变量,开销就增大了 , 因为必须经过如下步骤才能获得成员变量的地址:
    1. 获取“虚基类表指针”;
    2. 获取虚基类表中某一表项的内容;
    3. 把内容中指出的偏移量加到“虚基类表指针”的地址上。

    然而,事情并非永远如此。正如下面访问I对象的c1成员那样,如果不是通过指针访问,而是直接通过对象实例,则派生类的布局可以在编译期间静态获得,偏移量也可以在编译时计算,因此也就不必要根据虚基类表的表项来间接计算了。 

    1. I* pi;  
    2. pi->c1; // *(pi + dIGvbptr + (*(pi+dIGvbptr))[1] + dCc1);   
    3. pi->g1; // *(pi + dIG + dGg1); // *(pi + dIg1);   
    4. pi->h1; // *(pi + dIH + dHh1); // *(pi + dIh1);   
    5. pi->i1; // *(pi + dIi1);   
    6. I i;  
    7. i.c1; // *(&i + IdIC + dCc1); // *(&i + IdIc1);   
    译者注:I继承自G和H,G和H的虚基类是C,pi是指向I对象的指针。
    a. 访问虚基类C的成员c1时,dIGvbptr是“在I中,I对象指针与G的“虚基类表指针”之间的偏移”,*(pi + dIGvbptr)是虚基类表的开始地址,*(pi + dIGvbptr)[1]是虚基类表的第二项的内容(在I对象中,G对象的“虚基类表指针”与虚基类之间的偏移),dCc1是C对象指针与成员变量c1之 间的偏移;
    b. 访问非虚基类G的成员g1时,直接计算偏移量;
    c. 访问非虚基类H的成员h1时,直接计算偏移量;
    d. 访问自身成员i1时,直接使用偏移量;
    e. 当声明了一个对象实例,用点“.”操作符访问虚基类成员c1时,由于编译时就完全知道对象的布局情况,所以可以直接计算偏移量。

    当访问类继承层次中,多层虚基类的成员变量时,情况又如何呢?比如,访问虚基类 的虚基类的成员变量时?一些实现方式为:保存一个指向直接虚基类的指针,然后就可以从直接虚基类找到它的虚基类,逐级上推。VC++优化了这个过程。 VC++在虚基类表中增加了一些额外的项,这些项保存了从派生类到其各层虚基类的偏移量。

    4 强制转化

    如果没有虚基类的问题,将一个指针强制转化为另一个类型的指针代价并不高昂。如果在要求转化的两个指针之间有“基类-派生类”关系,编译器只需要简单地在两者之间加上或者减去一个偏移量即可(并且该量还往往为0)。

    1. F* pf;  
    2. (C*)pf; // (C*)(pf ? pf + dFC : 0); // (C*)pf;   
    3. (E*)pf; // (E*)(pf ? pf + dFE : 0);   

    C和E是F的基类,将F的指针pf转化为C*或E*,只需要将pf加上一个相应的偏移量。转化为C类型指针C*时,不需要计算,因为F和C之间的偏移量为 0。转化为E类型指针E*时,必须在指针上加一个非0的偏移常量dFE。C ++规范要求NULL指针在强制转化后依然为NULL , 因此在做强制转化需要的运算之前,VC++会检查指针是否为NULL。当然,这个检查只有当指针被显示或者隐式转化为相关类型指针时才进行;当在派生类对 象中调用基类的方法,从而派生类指针在后台被转化为一个基类的Const “this” 指针时,这个检查就不需要进行了,因为在此时,该指针一定不为NULL。

    正如你猜想的,当继承关系中存在虚基类时,强制转化的开销会比较大。具体说来,和访问虚基类成员变量的开销相当。 

    1. I* pi;  
    2. (G*)pi; // (G*)pi;   
    3. (H*)pi; // (H*)(pi ? pi + dIH : 0);   
    4. (C*)pi; // (C*)(pi ? (pi+dIGvbptr + (*(pi+dIGvbptr))[1]) : 0);   
    译者注:pi是指向I对象的指针,G,H是I的基类,C是G,H的虚基类。
    a. 强制转化pi为G*时,由于G*和I*的地址相同,不需要计算;
    b. 强制转化pi为H*时,只需要考虑一个常量偏移;
    c. 强制转化pi为C*时,所作的计算和访问虚基类成员变量的开销相同,首先得到G的虚基类表指针,再从虚基类表的第二项中取出G到虚基类C的偏移量,最后根据pi、虚基类表偏移和虚基类C与虚基类表指针之间的偏移计算出C*。

    一般说来,当从派生类中访问虚基类成员时,应该先强制转化派生类指针为虚基类指针,然后一直使用虚基类指针来访问虚基类成员变量。这样做,可以避免每次都要计算虚基类地址的开销。 见下例。

    /* before: */             ... pi->c1 ... pi->c1 ...
    /* faster: */ C* pc = pi; ... pc->c1 ... pc->c1 ...
    译者注:前者一直使用派生类指针pi,故每次访问c1都有计算虚基类地址的较大开销;后者先将pi转化为虚基类指针pc,故后续调用可以省去计算虚基类地址的开销。

    5 成员函数

    一个C++成员函数只是类范围内的又一个成员。X类每一个非静态的成员函数都会接受一个特殊的隐藏参数——this指针,类型为X* const。 该指针在后台初始化为指向成员函数工作于其上的对象。同样,在成员函数体内,成员变量的访问是通过在后台计算与this指针的偏移来进行。
     

    1. struct  P {  
    2.    int  p1;  
    3.    void  pf();  // new   
    4.    virtual   void  pvf();  // new   
    5. };  

    P有一个非虚成员函数pf(),以及一个虚成员函数pvf()。很明显,虚成员 函数造成对象实例占用更多内存空间,因为虚成员函数需要虚函数表指针。这一点以后还会谈到。这里要特别指出的是,声明非虚成员函数不会造成任何对象实例的 内存开销。现在,考虑P::pf()的定义。

    1. void  P::pf() {  // void P::pf([P *const this])   
    2.    ++p1;   // ++(this->p1);   
    3. }  

    这里P:pf()接受了一个隐藏的this指针参数 , 对于每个成员函数调用,编译器都会自动加上这个参数。同时,注意成员变量访问也许比看起来要代价高昂一些,因为成员变量访问通过this指针进行,在有的 继承层次下,this指针需要调整,所以访问的开销可能会比较大。然而,从另一方面来说,编译器通常会把this指针缓存到寄存器中,所以,成员变量访问 的代价不会比访问局部变量的效率更差。
    译者注:访问局部变量,需要到SP寄存器中得到栈指针,再加上局部变量与栈顶的偏移。在没有虚基类的情况下,如果编译器把this指针缓存到了寄存器中,访问成员变量的过程将与访问局部变量的开销相似。

    5.1 覆盖成员函数

    和成员变量一样,成员函数也会被继承。与成员变量不同的是,通过在派生类中重新定义基类函数,一个派生类可以覆盖,或者说替换掉基类的函数定义。覆盖是静态 (根据成员函数的静态类型在编译时决定)还是动态 (通过对象指针在运行时动态决定),依赖于成员函数是否被声明为“虚函数”。

    Q从P继承了成员变量和成员函数。Q声明了pf(),覆盖了P::pf()。Q还声明了pvf(),覆盖了P::pvf()虚函数。Q还声明了新的非虚成员函数qf(),以及新的虚成员函数qvf()。

    1. struct  Q : P {  
    2.    int  q1;  
    3.    void  pf();  // overrides P::pf   
    4.    void  qf();  // new   
    5.    void  pvf();  // overrides P::pvf   
    6.    virtual   void  qvf();  // new   
    7. };  
    对于非虚 的成员函数来说,调用哪个成员函数是在编译 时,根据“->”操作符左边指针表达式的类型静态决定 的。特别地,即使ppq指向Q的实例,ppq->pf()仍然调用的是P::pf(),因为ppq被声明为“P*”。(注意,“->”操作符左边的指针类型决定隐藏的this参数的类型。)

    1. P p; P* pp = &p; Q q; P* ppq = &q; Q* pq = &q;  
    2. pp->pf(); // pp->P::pf(); // P::pf(pp);   
    3. ppq->pf(); // ppq->P::pf(); // P::pf(ppq);   
    4. pq->pf(); // pq->Q::pf(); // Q::pf((P*)pq); (错误!)   
    5. pq->qf(); // pq->Q::qf(); // Q::qf(pq);   
    译者注:标记“错误”处,P*似应为Q*。因为pf非虚函数,而pq的类型为Q*,故应该调用到Q的pf函数上,从而该函数应该要求一个Q* const类型的this指针。

    对于虚函数 调用来说,调用哪个成员函数在运行时 决定。不管“->”操作符左边的指针表达式的类型如何,调用的虚函数都是由指针实际指向的实例类型所决定 。比如,尽管ppq的类型是P*,当ppq指向Q的实例时,调用的仍然是Q::pvf()。

    1. pp->pvf();  // pp->P::pvf(); // P::pvf(pp);   
    2. ppq->pvf(); // ppq->Q::pvf(); // Q::pvf((Q*)ppq);   
    3. pq->pvf(); // pq->Q::pvf(); // Q::pvf((P*)pq); (错误!)   
    译者注:标记“错误”处,P*似应为Q*。因为pvf是虚函数,pq本来就是Q*,又指向Q的实例,从哪个方面来看都不应该是P*。

    为了实现这种机制,引入了隐藏的vfptr 成员变量。 一个vfptr被加入到类中(如果类中没有的话),该vfptr指向类的虚函数表(vftable)。类中每个虚函数在该类的虚函数表中都占据一项。每项保存一个对于该类适用的虚函数的地址。因此,调用虚函数的过程如下:取得实例的vfptr;通过vfptr得到虚函数表的一项;通过虚函数表该项的函数地址间接调用虚函数。也就是说,在普通函数调用的参数传递、调用、返回指令开销外,虚函数调用还需要额外的开销。

    回头再看看P和Q的内存布局,可以发现,VC++编译器把隐藏的vfptr成员变量放在P和Q实例的开始处。这就使虚函数的调用能够尽量快一些。实际上,VC++的实现方式是,保证任何有虚函数的类的第一项永远是vfptr。 这就可能要求在实例布局时,在基类前插入新的vfptr,或者要求在多重继承时,虽然在右边,然而有vfptr的基类放到左边没有vfptr的基类的前面(如下)。

    1. class  CA  
    2. {   int  a;};  
    3. class  CB  
    4. {   int  b;};  
    5. class  CL :  public  CB,  public  CA  
    6. {   int  c;};  
    对于CL类,它的内存布局是:
    int b;
    int a;
    int c;
    但是,改造CA如下:

    1. class  CA  
    2. {  
    3.    int  a;  
    4.    virtual   void  seta(  int  _a ) { a = _a; }  
    5. };  
    对于同样继承顺序的CL,内存布局是:
    vfptr;
    int a;
    int b;
    int c;

    许多C++的实现会共享或者重用从基类继承来的vfptr。比如,Q并不会有一个额外的vfptr,指向一个专门存放新的虚函数qvf()的虚函数表。Qvf项只是简单地追加 到P的虚函数表的末尾。如此一来,单继承的代价就不算高昂。一旦一个实例有vfptr了,它就不需要更多的vfptr。新的派生类可以引入更多的虚函数,这些新的虚函数只是简单地在已存在的,“每类一个”的虚函数表的末尾追加新项。

    5.2 多重继承下的虚函数

    如果从多个有虚函数的基类继承,一个实例就有可能包含多个vfptr。考虑如下的R和S类:
     

    1. struct  R {  
    2.    int  r1;  
    3.    virtual   void  pvf();  // new   
    4.    virtual   void  rvf();  // new   
    5. };  

    1. struct  S : P, R {  
    2.    int  s1;  
    3.    void  pvf();  // overrides P::pvf and R::pvf   
    4.    void  rvf();  // overrides R::rvf   
    5.    void  svf();  // new   
    6. }; 

    这里R是另一个包含虚函数的类。因为S从P和R多重继承,S的实例内嵌P和R的实例,以及S自身的数据成员S::s1。注意,在多重继承下,靠右的基类R,其实例的地址和P与S不同。 S::pvf覆盖了P::pvf()和R::pvf(),S::rvf()覆盖了R::rvf()。

    1. S s; S* ps = &s;  
    2. ((P*)ps)->pvf(); // (*(P*)ps)->P::vfptr[0])((S*)(P*)ps)   
    3. ((R*)ps)->pvf(); // (*(R*)ps)->R::vfptr[0])((S*)(R*)ps)   
    4. ps->pvf();       // one of the above; calls S::pvf() 
    译者注:
     调用((P*)ps)->pvf()时,先到P的虚函数表中取出第一项,然后把ps转化为S*作为this指针传递进去;
     调用((R*)ps)->pvf()时,先到R的虚函数表中取出第一项,然后把ps转化为S*作为this指针传递进去;

    因为S::pvf()覆盖了P::pvf()和R::pvf(),在S的虚函数 表中,相应的项也应该被覆盖。然而,我们很快注意到,不光可以用P*,还可以用R*来调用pvf()。问题出现了:R的地址与P和S的地址不同。表达式 (R*)ps与表达式(P*)ps指向类布局中不同的位置。因为函数S::pvf希望获得一个S*作为隐藏的this指针参数,虚函数必须把R*转化为 S*。因此,在S对R虚函数表的拷贝中,pvf函数对应的项,指向的是一个“调整块 ”的地址,该调整块使用必要的计算,把R*转换为需要的S*。
    译者注:这就是“thunk1: this-= sdPR; goto S::pvf”干的事。先根据P和R在S中的偏移,调整this为P*,也就是S*,然后跳转到相应的虚函数处执行。

    在微软VC++实现中,对于有虚函数的多重继承,只有当派生类虚函数覆盖了多个基类的虚函数时,才使用调整块。  

    5.3 地址点与“逻辑this调整”

    考虑下一个虚函数S::rvf(),该函数覆盖了R::rvf()。我们都知道S::rvf()必须有一个隐藏的S*类型的this参数。但是,因为也可以用R*来调用rvf(),也就是说,R的rvf虚函数槽可能以如下方式被用到:

    1. ((R*)ps)->rvf();  // (*((R*)ps)->R::vfptr[1])((R*)ps) 
    所 以,大多数实现用另一个调整块将传递给rvf的R*转换为S*。还有一些实现在S的虚函数表末尾添加一个特别的虚函数项,该虚函数项提供方法,从而可以直 接调用ps->rvf(),而不用先转换R*。MSC++的实现不是这样,MSC++有意将S::rvf编译为接受一个指向S中嵌套的R实例,而非 指向S实例的指针(我们称这种行为是“给派生类的指针类型与该虚函数第一次被引入时接受的指针类型相同”)。所有这些在后台透明发生,对成员变量的存取, 成员函数的this指针,都进行“逻辑this调整”。

    当然,在debugger中,必须对这种this调整进行补偿。

    1. ps->rvf();  // ((R*)ps)->rvf(); // S::rvf((R*)ps)   
    译者注:调用rvf虚函数时,直接给入R*作为this指针。

    所以,当覆盖非最左边的基类的虚函数时,MSC++一般不创建调整块,也不增加额外的虚函数项。

    5.4 调整块

    正如已经描述的,有时需要调整块来调整this指针的值(this指针通常位于 栈上返回地址之下,或者在寄存器中),在this指针上加或减去一个常量偏移,再调用虚函数。某些实现(尤其是基于cfront的)并不使用调整块机制。 它们在每个虚函数表项中增加额外的偏移数据。每当虚函数被调用时,该偏移数据(通常为0),被加到对象的地址上,然后对象的地址再作为this指针传入。

    1. ps->rvf();  
    2. // struct { void (*pfn)(void*); size_t disp; };   
    3. // (*ps->vfptr[i].pfn)(ps + ps->vfptr[i].disp);  
    译者注:当调用rvf虚函数时,前一句表示虚函数表每一项是一个结构,结构中包含偏移量;后一句表示调用第i个虚函数时,this指针使用保存在虚函数表中第i项的偏移量来进行调整。

    这种方法的缺点是虚函数表增大了,虚函数的调用也更加复杂。

    现代基于PC的实现一般采用“调整—跳转”技术:

    1. S::pvf-adjust:  // MSC++   
    2. this  -= SdPR;  
    3. goto  S::pvf()  
    当然,下面的代码序列更好(然而,当前没有任何实现采用该方法):

    1. S::pvf-adjust:  
    2. this  -= SdPR;  // fall into S::pvf()   
    3. S::pvf() { ... }  
    译者注:IBM的C++编译器使用该方法。

    5.5 虚继承下的虚函数

    T虚继承P,覆盖P的虚成员函数,声明了新的虚函数。如果采用在基类虚函数表末尾添加新项的方式,则访问虚函数总要求访问虚基类。在VC++中,为了避免获取虚函数表时,转换到虚基类P的高昂代价,T中的新虚函数通过一个新的虚函数表获取 ,从而带来了一个新的虚函数表指针。该指针放在T实例的顶端。
     

    1. struct  T :  virtual  P {  
    2.    int  t1;  
    3.    void  pvf();          // overrides P::pvf   
    4.    virtual   void  tvf();  // new   
    5. };  
    6. void  T::pvf() {  
    7.    ++p1; // ((P*)this)->p1++; // vbtable lookup!   
    8.    ++t1; // this->t1++;   

    如上所示,即使是在虚函数中,访问虚基类的成员变量也要通过获取虚基类表的偏移,实行计算来进行。这样做之所以必要,是因为虚函数可能被进一步继承的类所覆盖,而进一步继承的类的布局中,虚基类的位置变化了。 下面就是这样的一个类:
     

    1. struct  U : T {  
    2.    int  u1;  
    3. };  

    在此U增加了一个成员变量,从而改变了P的偏移。因为VC++实现中,T::pvf()接受的是嵌套在T中的P的指针,所以,需要提供一个调整块,把this指针调整到T::t1之后(该处即是P在T中的位置)。

    5.6 特殊成员函数

    本节讨论编译器合成到特殊成员函数中的隐藏代码。

    5.6.1 构造函数和析构函数

    正如我们所见,在构造和析构过程中,有时需要初始化一些隐藏的成员变量。最坏的情况下,一个构造函数要执行如下操作:
    1 * 如果是“最终派生类”,初始化vbptr成员变量,调用虚基类的构造函数;
    2 * 调用非虚基类的构造函数
    3 * 调用成员变量的构造函数
    4 * 初始化虚函数表成员变量
    5 * 执行构造函数体中,程序所定义的其他初始化代码

    (注意:一个“最终派生类”的实例,一定不是嵌套在其他派生类实例中的基类实例)
    所以,如果你有一个包含虚函数的很深的继承层次,即使该继承层次由单继承构成,对象的构造可能也需要很多针对虚函数表的初始化。
    反之,析构函数必须按照与构造时严格相反的顺序来“肢解”一个对象。
    1 * 合成并初始化虚函数表成员变量
    2 * 执行析构函数体中,程序定义的其他析构代码
    3 * 调用成员变量的析构函数(按照相反的顺序)
    4 * 调用直接非虚基类的析构函数(按照相反的顺序)
    5 * 如果是“最终派生类”,调用虚基类的析构函数(按照相反顺序)

    在VC++中,有虚基类的类的构造函数接受一个隐藏的“最终派生类 标志”,标示虚基类是否需要初始化。对于析构函数,VC++采用“分层析构模型”,代码中加入一个隐藏的析构函数,该函数被用于析构包含虚基类的类(对于 “最终派生类”实例而言);代码中再加入另一个析构函数,析构不包含虚基类的类。前一个析构函数调用后一个。

    5.6.2 虚析构函数与delete操作符

    假如A是B的父类,  
    A* p = new B();  
    如果析构函数不是虚拟的,那么,你后面就必须这样才能安全的删除这个指针:  
    delete (B*)p;  
    但如果构造函数是虚拟的,就可以在运行时动态绑定到B类的析构函数,直接:  
    delete p;  
    就可以了。这就是虚析构函数的作用。
    实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
    考虑结构V和W。

    1. struct  V {  
    2.    virtual  ~V();  
    3. }; 
     
    1. struct  W : V {  
    2.    operator delete ();  
    3. }; 
    析构函数可以为虚。 一个类如果有虚析构函数的话,将会象有其他虚函数一样,拥有一个虚函数表指针,虚函数表中包含一项,其内容为指向对该类适用的虚析构函数的地址。这些机制和普通虚函数相同。 虚析构函数的特别之处在于:当类实例被销毁时,虚析构函数被隐含地调用。调用地(delete发生的地方)虽然不知道销毁的动态类型,然而,要保证调用对该类型合适的delete操作符。  例如,当pv指向W的实例时,当W::~W被调用之后,W实例将由W类的delete操作符来销毁。
    1. V* pv =  new  V;  
    2. delete  pv;    // pv->~V::V(); // use ::operator delete()   
    3. pv = new  W;  
    4. delete  pv;    // pv->~W::W(); // use W::operator delete() 动态绑定到 W的析构函数,W默认的析构函数调用{delete this;}   
    5. pv = new  W;  
    6. ::delete  pv;  // pv->~W::W(); // use ::operator delete() 
    译者注:
     V没有定义delete操作符,delete时使用函数库的delete操作符;
     W定义了delete操作符,delete时使用自己的delete操作符;
     可以用全局范围标示符显示地调用函数库的delete操作符。
    为 了实现上述语意,VC++扩展了其“分层析构模型”,从而自动创建另一个隐藏的析构帮助函数——“deleting析构函数”,然后,用该函数的地址来替 换虚函数表中“实际”虚析构函数的地址。析构帮助函数调用对该类合适的析构函数,然后为该类有选择性地调用合适的delete操作符。

    6 数组

    堆上分配空间的数组使虚析构函数进一步复杂化。问题变复杂的原因有两个:
    1、 堆上分配空间的数组,由于数组可大可小,所以,数组大小值应该和数组一起保存。因此,堆上分配空间的数组会分配额外的空间来存储数组元素的个数;
    2、 当数组被删除时,数组中每个元素都要被正确地释放,即使当数组大小不确定时也必须成功完成该操作。然而,派生类可能比基类占用更多的内存空间,从而使正确释放比较困难。

    1. struct  WW : W {  int  w1; };  
    2. pv = new  W[m];  
    3. delete  [] pv;  // delete m W's (sizeof(W) == sizeof(V))   
    4. pv = new  WW[n];  
    5. delete  [] pv;  // delete n WW's (sizeof(WW) > sizeof(V)) 
    译者注:WW从W继承,增加了一个成员变量,因此,WW占用的内存空间比W大。然而,不管指针pv指向W的数组还是WW的数组,delete[]都必须正确地释放WW或W对象占用的内存空间。

     

    虽 然从严格意义上来说,数组delete的多态行为C++标准并未定义,然而,微软有一些客户要求实现该行为。因此,在MSC++中,该行为是用另一个编译 器生成的虚析构帮助函数来完成。该函数被称为“向量delete析构函数”(因其针对特定的类定制,比如WW,所以,它能够遍历数组的每个元素,调用对每 个元素适用的析构函数)。

    7 异常处理

    简单说来,异常处理是C++标准委员会工作文件提供的一种机制,通过该机制,一个函数可以通知其调用者“异常”情况的发生,调用者则能据此选择合适的代码来处理异常。该机制在传统的“函数调用返回,检查错误状态代码”方法之外,给程序提供了另一种处理错误的手段。

    因 为C++是面向对象的语言,很自然地,C++中用对象来表达异常状态。并且,使用何种异常处理也是基于“抛出的”异常对象的静态或动态类型来决定的。不光 如此,既然C++总是保证超出范围的对象能够被正确地销毁,异常实现也必须保证当控制从异常抛出点转换到异常“捕获”点时(栈展开),超出范围的对象能够 被自动、正确地销毁。
    考虑如下例子:

    1. struct  X { X(); };  // exception object class   
    2. struct  Z { Z(); ~Z(); };  // class with a destructor   
    3. extern   void  recover( const  X&);  
    4. void  f( int ), g( int );  
    5.   
    6. int  main() {  
    7.    try  {  
    8.       f(0);  
    9.    } catch  ( const  X& rx) {  
    10.       recover(rx);  
    11.    }  
    12.    return  0;  
    13. }  
    14.   
    15. void  f( int  i) {  
    16.    Z z1;  
    17.    g(i);  
    18.    Z z2;  
    19.    g(i-1);  
    20. }  
    21.   
    22. void  g( int  j) {  
    23.    if  (j < 0)  
    24.       throw  X();  
    25. }  
    译者注:X是异常类,Z是带析构函数的工作类,recover是错误处理函数,f和g一起产生异常条件,g实际抛出异常。
    这 段程序会抛出异常。在main中,加入了处理异常的try & catch框架,当调用f(0)时,f构造z1,调用g(0)后,再构造z2,再调用g(-1),此时g发现参数为负,抛出X异常对象。我们希望在某个调 用层次上,该异常能够得到处理。既然g和f都没有建立处理异常的框架,我们就只能希望main函数建立的异常处理框架能够处理X异常对象。实际上,确实如 此。当控制被转移到main中异常捕获点时,从g中的异常抛出点到main中的异常捕获点之间,该范围内的对象都必须被销毁。在本例中,z2和z1应该被 销毁。
    谈到异常处理的具体实现方式,一般情况下,在抛出点和捕 获点都使用“表”来表述能够捕获异常对象的类型;并且,实现要保证能够在特定的捕获点真正捕获特定的异常对象;一般地,还要运用抛出的对象来初始化捕获语 句的“实参”。通过合理地选择编码方案,可以保证这些表格不会占用过多的内存空间。
    异 常处理的开销到底如何?让我们再考虑一下函数f。看起来f没有做异常处理。f确实没有包含try,catch,或者是throw关键字,因此,我们会猜异 常处理应该对f没有什么影响。错!编译器必须保证一旦z1被构造,而后续调用的任何函数向f抛回了异常,异常又出了f的范围时,z1对象能被正确地销毁。 同样,一旦z2被构造,编译器也必须保证后续抛出异常时,能够正确地销毁z2和z1。
    要 实现这些“展开”语意,编译器必须在后台提供一种机制,该机制在调用者函数中,针对调用的函数抛出的异常动态决定异常环境(处理点)。这可能包括在每个函 数的准备工作和善后工作中增加额外的代码,在最糟糕的情况下,要针对每一套对象初始化的情况更新状态变量。例如,上述例子中,z1应被销毁的异常环境当然 与z2和z1都应该被销毁的异常环境不同,因此,不管是在构造z1后,还是继而在构造z2后,VC++都要分别在状态变量中更新(存储)新的值。
    所有这些表,函数调用的准备和善后工作,状态变量的更新,都会使异常处理功能造成可观的内存空间和运行速度开销。正如我们所见,即使在没有使用异常处理的函数中,该开销也会发生。
    幸运的是,一些编译器可以提供编译选项,关闭异常处理机制。那些不需要异常处理机制的代码,就可以避免这些额外的开销了。

     

    8 小结

    好了,现在你可以写C++编译器了(开个玩笑)。
    在 本文中,我们讨论了许多重要的C++运行实现问题。我们发现,很多美妙的C++语言特性的开销很低,同时,其他一些美妙的特性(译者注:主要是和“虚”字 相关的东西)将造成较大的开销。C++很多实现机制都是在后台默默地为你工作。一般说来,单独看一段代码时,很难衡量这段代码造成的运行时开销,必须把这 段代码放到一个更大的环境中来考察,运行时开销问题才能得到比较明确的答案。

    展开全文
  • .Net 垃圾回收和大对象处理

    千次阅读 2015-03-15 21:28:41
    英文原文:Maoni Stephens,编译:赵玉开...比如内存碎片整理 —— 在内存中移动大对象的成本是昂贵的,让我们研究一下垃圾回收器是如何处理大对象的,大对象对程序性能有哪些潜在的影响。 对象堆和垃圾回收 在.Net 1.

    英文原文:Maoni Stephens,编译:赵玉开(@玉开Sir)

    CLR垃圾回收器根据所占空间大小划分对象。大对象和小对象的处理方式有很大区别。比如内存碎片整理 —— 在内存中移动大对象的成本是昂贵的,让我们研究一下垃圾回收器是如何处理大对象的,大对象对程序性能有哪些潜在的影响。

    大对象堆和垃圾回收

    在.Net 1.0和2.0中,如果一个对象的大小超过85000byte,就认为这是一个大对象。这个数字是根据性能优化的经验得到的。当一个对象申请内存大小达到这个阈值,它就会被分配到大对象堆上。这意味着什么呢?要理解这个,我们需要理解.Net垃圾回收机制。

    如大多人所知道的,.Net GC是按照“代”来回收的。程序中的对象共有3代,0代、1代和2代,0代是最年轻的对象,2代对象存活的时间最长。GC按代回收垃圾也是出于性能考虑的;通常的对象都会在0代是被回收。例如,在一个asp.net程序中,和每一个请求相关的对象都应该在请求结束时回收掉。而没有被回收的对象会成为1代对象;也就是说1代对象是常驻内存对象和马上消亡对象之间的一个缓冲区。

    从代的角度看,大对象属于2代对象,因为只有在2代回收时才会处理大对象。当某代垃圾回收执行时,会同时执行更年轻代的垃圾回收。比如:当1代垃圾回收时会同时回收1代和0代的对象,当2代垃圾回收时会执行1代和0代的回收.

    代是垃圾回收器区分内存区域的逻辑视图。从物理存储角度看,对象分配在不同的托管堆上。一个托管堆(managed heap)是垃圾回收器从操作系统申请的内存区(通过调用windows api VirtualAlloc)。当CLR载入内存之后,会初始化两个托管堆,一个大对象堆(LOH –large object heap)和一个小对象对(SOH – small object heap)。

    内存分配请求就是将托管对象放到对应的托管堆上。如果对象的大小小于85000byte,它会被放置在SOH;否则会被放在LOH上。

    对于SOH,对象在执行一次垃圾回收之后,会进入到下一代。也就是说如果在第一次执行垃圾回收时,存活下来的对象会进入第二代,如果在第2次垃圾回收之后该对象仍然没有被当作垃圾回收掉,它就会成为2代对象;2代对象就是最老的对象不会在提升代数。

    当触发垃圾回收时,垃圾回收器会在小对象堆做碎片整理,将存活下来的对象移动到一起。而对于大对象堆,由于移动内存的开销很大,CLR团队选择只是清除它们,将回收掉的对象组成一个列表,以便满足下次有大对象申请使用内存,相邻的垃圾对象会被合并成一块空闲的内存块。

    需要时时留意的是,直到.Net 4.0中也不会对大对象堆做碎片整理操作,将来也许会做。因此如果你要分配大对象并不想他们被移动,你可以使用fixed语句。

    如下小对象堆SOH的回收示意图


    上图中第一次垃圾回收之前有四个对象obj0-3;在第一垃圾回收之后obj1和obj3被回收了,同时obj2和obj0移动到一起了;在第二次垃圾回收之前有分配了三个对象obj4-6;在第二次执行垃圾回收之后obj2和obj5被回收了,obj4和obj6被移动到obj0旁边。

    下图是大对象堆LOH回收示意图


    可以看到在未执行垃圾回收之前,一共有四个对象obj0-3;第一次二代垃圾回收之后obj1和obj2被回收掉了,回收掉之后obj1和obj2所占空间被合并到了一起,在obj4申请分配内存时就把obj1和obj2回收后释放的空间分配给它了;同时留下了一块内存碎片。如果这个碎片的大小小于85000byte,那么这个碎片就在这个程序的生命周期中永远不能被再次利用了。

    如果大对象堆上没有足够的空闲内存容纳要申请的大对象空间,CLR首先会尝试向操作系统申请内存,如果申请失败,就会触发一次二代回收来尝试释放一些内存。

    在2代垃圾回收时,可以将不需要的内存通过VirtualFree交还给操作系统。交还的过程参见下图:


    什么时候回收大对象呢?

    在讨论什么时候回收大对象之前先来看下普通的垃圾回收操作什么时机执行吧。垃圾回收在下列情况下发生:

    1. 申请的空间超过0代内存大小或者大对象堆的阈值,多数的托管堆垃圾回收在这种情况下发生

    2. 在程序代码中调用GC.Collect方法时;如果在调用GC.Collect方法是传入GC.MaxGeneration参数时,会执行所有代对象的垃圾回收,包括大对象堆的垃圾回收

    3. 操作系统内存不足时,当应用程序收到操作系统发出的高内存通知时

    4. 如果垃圾回收算法认为做二代回收是有收效时会触发二代垃圾回收

    5. 每一代对象堆的都有一个所占空间大小阈值的属性,当你分配对象到某一代,你增长了内存总量接近了该代的阈值,或者分配对象导致这一代的堆大小超过了堆阈值,就会发生一次垃圾回收。因此当你分配小对象或者大对象时,会对应消耗0代堆或者大对象堆的阈值。当垃圾回收器将对象代数提升到1代或者2代时,会消耗1、2代的阈值。在程序运行中这些阈值是动态变化的。

    大对象堆性能影响

    让我们先看下分配大对象的代价。 CLR为每个新对象分配内存时都要保证这些内存清空的,是没有被其他对象使用的(I give out is cleared)。这就意味着分配的代价完全被清理(clearing)的代价控制着(除非在分配时触发了一次垃圾回收)。如果清空1byte需要2个周期(cycles),就意味着清除一个最小的大对象需要170,000个周期。通常情况下人们不会分配超大的对象,比如说在2GHz的机器上分配16M大小的对象,大约需要16ms来清空内存。这代价太大了。

    让我们在看下回收的代价。前面提到过,大对象和2代龄对象一起回收。如果大对象或者2代对象占用空间超过其阈值时,就会触发2代对象的回收。如果2代回收因为大对象堆超过阈值被触发,2代对象堆本身没有多少对象可以做回收。如果在2代堆上没有多少对象,这问题不大。但是如果2代堆很大对象很多,过多的2代回收就会导致性能问题。如果是临时性的分配大对象,就需要很多的时间来运行垃圾回收;也就是说如果你持续的使用大对象然后又释放大对象对性能会有很大的负面影响。

    大对象堆上的巨大对象通常是数组(很少有一个对象很大的情况)。如果对象中的元素是强引用,代价会很高;如果元素之间没有相互引用,垃圾回收时就不需要遍历整个数组。例如:用一个数组来保存二叉树的节点,一种方法是在节点中强引用左右节点:

    class Node
    {
    Data d;
    Node left;
    Node right;
    }
     
    Node[] binaryTree = new Node[num_nodes];
    如果num_nodes是一个很大的数字,就意味着每个节点都至少需要查看二个引用元素。一种替代方案是在节点中保存左右节点元素的数组索引号

    class Node
    {
    Data d;
    uint left_index;
    uint right_index;
    }

    这样的话,元素之间的引用关系去掉了;可以通过binaryTree[left_index]来获得引用的节点。垃圾回收器在做垃圾回收时也不需要看相关的引用元素了。

    为大对象堆收集性能数据

    有几种方法可以收集大对象堆相关的性能数据。在我解释这些方法之前,让我们先谈一下为什么需要收集大对象堆相关的性能数据。

    在你开始上搜集某个方面的性能数据时,有可能你已经找到这方面造成性能瓶颈的证据;或者你已经没有找遍了所有方面都没有发现问题。

    在查找性能问题时.Net CLR Memory 性能计数器通常是应该先考虑使用的工具。和LOH相关的计数器有generation 2 collectioins(2代堆收集次数)和large object heap size大对象堆大小。Generation 2 collections显示的是进程启动之后2代垃圾回收操作发生的次数。Large object heap size计数器显示的是当前大对象堆的大小值,包括空闲空间;这个计数器是在每次垃圾回收操作之后做更新,并非每次分配内存都做更新。

    可以参考下图在windows性能计数器中观察.Net CLR Memory相关性能数据

    你也可以通过程序查询这些计数器的值;很多人通过程序的方式收集性能计数器来帮助查找性能瓶颈。

    当然也可以使用调试器winddbg观察大对象堆。

    最后提示一下:到目前为止,大对象堆作为垃圾回收的一部分是不做内存碎片整理的,但是这个只是一个clr的实现细节,程序代码不应该依赖这个特点。如果要确保对象不会被垃圾回收器移动,就要使用fixed语句。

    原文地址:http://blog.jobbole.com/31459/


    展开全文
  • 没有继承的对象属性排布 有继承的对象属性排布 如何计算对象大小 创建一个含有premain()方法的Java 类。 将创建好的Java类打成一个jar包 修改JVM启动配置 测试样例 参考书籍:《Java特种兵(上册)》 对象...

    目录

    对象内存结构

    没有继承的对象属性排布

    有继承的对象属性排布

    如何计算对象大小

    创建一个含有premain()方法的Java 类。

    将创建好的Java类打成一个jar包

    修改JVM启动配置

    测试样例


     参考书籍:《Java特种兵(上册)》 

    对象内存结构

    Class文件以字节码的形式存储在方法区当中,用来描述一个类本身的内存结构。当使用Class文件新建对象时,对象实例的内存结构又究竟是个什么样子呢? 

    如图所示,为了表示对象的属性、方法等信息,HotSpot VM使用对象头部的一个指针指向Class区域的方式来找到对象的Class描述,以及内部的方法、属性入口。除此之外,还在对象的头部划分了部分空间(Mark Word),用于描述与对象相关的其他信息,例如:是否加锁、GC标志位、Minor GC次数、对象默认的hashCode(System.identityHashCode(object)可获取对象的这个值)。

    在32位系统下,存放Class指针的空间大小是4字节,Mark Word空间大小也是4字节,因此就是8字节的头部,如果是数组还需要增加4字节来表示数组的长度。

    在64位系统及64位JVM下,开启指针压缩(参数是 -XX:+UseCompressedOops),那么头部存放Class指针的空间大小还是4字节,而Mark Word区域会变大,变成8字节,也就是头部最少为12字节。

    若未开启指针压缩,那么保存Class指针的空间大小也会变成8字节,那么对象头部会变成16字节。另外,在64位模式下,若未开启压缩,引用也会变成8字节。

    此外,Java对象将以8字节对齐在内存中,也就是对象占用的空间不是8字节的倍数,将会被补齐为8字节的倍数,这样做的好处是,在对象分配和查找的过程中不用考虑过多的偏移量问题。

    以下是在32位系统下一些常见对象占用的空间大小示例。 

    没有继承的对象属性排布

    在默认情况下,HotSpot VM会按照一个顺序排布对象的内部属性,这个顺序是,long/double-->int/float-->short/char-->byte/boolean-->Reference(与对象本身的属性顺序无关)。

    有继承的对象属性排布

    在HotSpot VM中,有继承关系的对象在创建时,父类的属性会被分配到相应的对象中,由于父类的属性不能和子类混用,所以它们必须单独排布在一个地方,可以认为它们就是从上到下的一个顺序。以两重继承为例,对象继承属性排布规则如下图所示。 

    这里的对齐有两种:一是整个对象的8字节对齐;二是父类到子类的属性对齐。在32位及64位压缩模式下,会按照4字节对齐。

    例如下面的例子: 

    class A {byte b;}
    class B extends A {byte b;}
    class C extends B {byte b;}

    如何计算对象大小

    有时,我们需要知道Java对象到底占用多少内存,有人通过连续调用两次System.gc()比较两次gc前后内存的使用量在计算java对象的大小,也有人根据Java虚拟机规范中的Java对象内存排列估算对象的大小,这两种方法或多或少都有问题,因为System.gc()并不一定促发GC,同一个类型的对象在32位与64位JVM中使用的内存会不一样,在64位虚拟机中是否开启指针压缩也会影响Java对象在内存中的大小。

    那么有没有一种既准确又方便的方法计算对象的大小呢?答案是肯定的。在Java 5中引入了Instrumentation类,这个类提供了计算对象内存占用量的方法;Hotspot支持instrumentation框架,其他的虚拟机也提供了类似的框架。

    使用Instrumentation类计算Java对象大小的过程如下:

    创建一个含有premain()方法的Java 类。

    package sizeof;
    
    import java.lang.instrument.Instrumentation;
    import java.lang.reflect.Array;
    import java.lang.reflect.Field;
    import java.lang.reflect.Modifier;
    import java.util.IdentityHashMap;
    import java.util.Map;
    import java.util.Stack;
    
    public class DeepObjectSizeOf {
    	
    	private static Instrumentation inst;
    
    	public static void premain(String agentArgs, Instrumentation instP) {
    		inst = instP;
    	}
    
    	public static long sizeOf(Object object) {
    		//计算当前对象的内存大小,不包含引用对象
    		return inst.getObjectSize(object);
    	}
    	
    	public static long deepSizeOf(Object obj) {//深入检索对象,并计算大小
    	       Map<Object, Object> visited = new IdentityHashMap<Object, Object>();
    	       Stack<Object> stack = new Stack<Object>();
    	       long result = internalSizeOf(obj, stack, visited);
    	       while (!stack.isEmpty()) {//通过栈进行遍历
    	          result += internalSizeOf(stack.pop(), stack, visited);
    	       }
    	       visited.clear();
    	       return result;
    	    }
    
    	    private static boolean needSkipObject(Object obj, Map<Object, Object> visited) {
    	       if (obj instanceof String) {
    	          if (obj == ((String) obj).intern()) {
    	             return true;
    	          }
    	       }
    	       return (obj == null) || visited.containsKey(obj);
    	    }
    
    	    private static long internalSizeOf(Object obj, Stack<Object> stack, Map<Object, Object> visited) {
    	       if (needSkipObject(obj, visited)) {
    	           return 0;
    	       }
    	       visited.put(obj, null);//将当前对象放入栈中
    	       long result = 0;
    	       result += sizeOf(obj);
    	       Class <?>clazz = obj.getClass();
    	       if (clazz.isArray()) {//如果数组
    	           if(clazz.getName().length() != 2) {//如果primitive type array,Class的name为2位
    	              int length =  Array.getLength(obj);
    	              for (int i = 0; i < length; i++) {
    	                 stack.add(Array.get(obj, i));
    	              }
    	           }
    	           return result;
    	       }
    	       return getNodeSize(clazz , result , obj , stack);
    	   }
    
    	   //这个方法获取非数组对象自身的大小,并且可以向父类进行向上搜索
    	   private static long getNodeSize(Class <?>clazz , long result , Object obj , Stack<Object> stack) {
    	      while (clazz != null) {
    	          Field[] fields = clazz.getDeclaredFields();
    	          for (Field field : fields) {
    	              if (!Modifier.isStatic(field.getModifiers())) {//这里抛开静态属性
    	                   if (field.getType().isPrimitive()) {//这里抛开基本关键字(因为基本关键字在调用java默认提供的方法就已经计算过了)
    	                       continue;
    	                   }else {
    	                       field.setAccessible(true);
    	                      try {
    	                           Object objectToAdd = field.get(obj);
    	                           if (objectToAdd != null) {
    	                                  stack.add(objectToAdd);//将对象放入栈中,一遍弹出后继续检索
    	                           }
    	                       } catch (IllegalAccessException ex) {
    	                           assert false;
    	                  }
    	              }
    	          }
    	      }
    	      clazz = clazz.getSuperclass();//找父类class,直到没有父类
    	   }
    	   return result;
    	  }
    }
    

    JVM会在应用程序运行之前调用这个Java 类的premain()方法(也就是在执行应用程序的main方法之前),JVM会在调用该方法时传入一个实现Instrumentation接口的实例,通过调用此接口实例的getObjectSize()方法可以计算出对象的大小(只计算当前对象的大小,不会进一步计算内部引用对象的大小)。 

    将创建好的Java类打成一个jar包

    在打包之前先创建一个MANIFEST.txt文件作为这个jar包的清单文件,其内容如下: 

    Manifest-Version: 1.0
    Premain-Class: sizeof.DeepObjectSizeOf

    按照Java类文件的包路径创建好目录(DeepObjectSizeOf.class文件放在sizeof文件夹中)。

    使用用如下命令创建jar包:

    jar -cmf MANIFEST.txt java_sizeof.jar sizeof/*

    修改JVM启动配置

    修改Eclipse IDE的JVM启动配置,增加-javaagent启动参数:

    -javaagent:jar文件路径

    我创建的 java_sizeof.jar放在D:\sizeof目录下,设置参数如下。

    测试样例

    创建一个测试类SizeOfMain.java,代码如下。

    package sizeof;
    
    public class SizeOfMain {
    	
    	public static void main(String[] args) {
    		System.out.println("new Integer(1) 对象大小:"
    				+ DeepObjectSizeOf.deepSizeOf(new Integer(1)));
    		System.out.println("new String(\"sizeof\") 对象大小:"
    				+ DeepObjectSizeOf.deepSizeOf(new String("sizeof")));
    	}
    }
    

    在64位机器上(不开启指针压缩):

    设置参数:-javaagent:d:\sizeof/java_sizeof.jar -XX:-UseCompressedOops

    执行结果:

    在64位机器上(开启指针压缩):

    设置参数:-javaagent:d:\sizeof/java_sizeof.jar -XX:+UseCompressedOops

    执行结果:

    展开全文
  • JSP九内置对象和四种属性范围解读

    万次阅读 多人点赞 2015-07-14 18:27:52
    本文首先主要讲解了JSP中四种属性范围的概念、用法与实例。然后在这个基础之上又引入了九内置对象,并对这几内置对象一个一个的进行分析的解读。内容很详细,例子都附有代码和运行的结果截图。
  • 如何有效减少Java内存占用过高

    千次阅读 2021-06-02 15:17:26
    1、是否有内存泄漏 2、业务层面,业务逻辑处理是否使用了大对象,比如上百字段的对象,这种冗余就会过多的占用内存 3、能用局部变量,就不用成员变量和静态变量 4
  • Java对象内存布局

    千次阅读 2020-06-19 11:20:31
    Java对象内存布局 (一)简述 曾经有这样一道面试题,问:Object ob = new Object()中的ob占几个字节。想回答这个题目就必须要知道Java对象内存布局问题。 对象布局研究的问题的实质就是看看java的对象内存中...
  • 深入理解Java虚拟机-Java内存区域与内存溢出异常

    万次阅读 多人点赞 2020-01-03 21:42:24
    文章目录概述运行时数据区域程序计数器(线程私有)Java虚拟机栈(线程私有)局部变量表操作数栈动态链接方法返回地址小结本地方法栈(线程私有)Java堆(全局共享)方法区(全局共享)运行时常量池直接内存HotSpot...
  • 创建对象的方式用new语句创建对象。使用反射,调用java.lang.Class或java.lang.reflect.Constructor的newInstance()实例方法。调用对象的clone()方法使用反序列化手段,调用java.io.ObjectInputStream对象的...
  • 如何优化Python占用的内存

    千次阅读 2020-12-03 19:01:36
    概述如果程序处理的数据比较多、比较复杂,那么在程序运行的时候,会占用大量的内存,当内存占用到达一定的数值,程序就有可能被操作系统终止,特别是在限制程序所使用的内存大小的场景,更容易发生问题。...
  • 谈到性能优化分析一般会涉及到:Java代码层面的,典型的循环嵌套等还会涉及到Java JVM:内存泄漏溢出等MySQL数据库优化:分库分表、慢查询、长事务的优化等今天主要分享JVM性能调优工具,文末有详细的JVM调优方法和...
  • 深入理解Java虚拟机-垃圾回收器与内存分配策略

    万次阅读 多人点赞 2020-01-04 13:08:32
    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来。 文章目录概述对象已死吗引用计数法可达性分析算法再谈引用生存还是死亡回收方法区垃圾收集算法标记-清除...
  • JVM堆内存(heap)详解

    千次阅读 2022-04-19 17:03:12
    JAVA堆内存管理是影响性能主要因素之一。...JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。 年轻代又分为
  • Redis内存优化

    万次阅读 2020-04-16 14:50:06
    使用maxmemory参数限制最大可用内存,当超出内存上限maxmemory时使用LRU等删除策略释放空间以及防止所用内存超过服务器物理内存。 2.配置内存回收策略 Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略...
  • 创建对象必然要在虚拟机中分配内存,虚拟机提供了两种策略:指正碰撞和空闲列表 指针碰撞法 很容易理解,在内存中,一遍是已经被分配的空间,一遍的未分配的空间,如果新建了8字节对象,那么指针就往未分配空间...
  • 小心踩雷,一次Java内存泄漏排查实战

    千次阅读 多人点赞 2019-06-04 08:45:00
    由于 Bean 对象不会被回收,这个属性又没有清除逻辑,所以在服务十来天没有上线重启的情况下,这个 Map 越来越,直至将内存占满。   内存满了之后,无法再给 HTTP 响应结果分配内存了,所以一直卡在 ...
  • 内存溢出是由于没被引用的对象(垃圾)过多造成JVM没有及时回收,造成的内存溢出。如果出现这种现象可行代码排查:一)是否应用中的类中和引用变量过多使用了Static修饰 如public staitc Student s;在类中的属性中使用...
  • 我编写了一个程序,可以将大约2600个文本文档解析为Python对象。这些对象有很多对其他对象的引用,它们总体上描述了文档的结构。在用pickle序列化这些对象没有问题,而且速度非常快。在在将这些文档解析成Python对象...
  • JVM内存结构 JVM内存分为线程私有区和线程共享区 线程私有区 1、程序计数器 ✓(记录当前线程执⾏到哪⼀条字节码指令...✓(线程执⾏⽅法的时候内部存局部变量会存堆中对象的地址等等数据) 线程私有的,与线程在同
  • 目录Redis内存满了怎么办?怎么优化内存?Redis主要消耗什么物理资源?Redis的内存用完了会发生什么?谈谈缓存数据的淘汰机制谈谈LRU算法如何处理被淘汰的数据?Redis怎么优化内存? Redis内存满了怎么办?怎么优化...
  • 认识JVM--第二篇-java对象内存模型

    千次阅读 2011-07-03 23:57:14
    前一段写了一篇《认识JVM》,不过在一些方面可以继续阐述的,在这里继续探讨一下,本文重点在于在heap区域内部...3、一个对象放在内存中的是如何存放的 4、调用的指令分析 5、对象宽度对其问题及空间浪费 6、指令
  • Java中GC(垃圾回收)算法

    千次阅读 2021-03-17 03:43:23
    从Java内存模型(链接)一文中,我们知道,java中几乎所有的对象实例存储在堆内存中,故而堆内存是JVM垃圾回收的主要阵地。哪些对象需要被回收?在讨论GC之前我们需要考虑一个问题?如何确定一个对象是否需要被回收?...
  • Android 性能优化--内存

    千次阅读 2021-07-06 23:15:05
    目的是防止程序发生OOM异常,以及降低程序由于内存被LowMemoryKiller(LMK)机制杀死的概率。同时,不合理的内存使用会使GC次数大大增多,从而导致程序变卡。 优化ROM,即降低程序占ROM的体积,防止ROM空间不足...
  • 闭包为什么会造成内存泄漏

    千次阅读 2021-07-04 23:35:57
    内存泄漏会对浏览器造成很的压力,之前隐隐约约有听说过“如果闭包不处理是一定存内存泄漏的”,这是真的吗?是为什么呢? 函数作用域链 创建函数outerFun()时,会创建一个预先包含全局变量对象的作用域链,保存...
  • 对List对象列表属性值的快速搜索

    千次阅读 2018-07-30 09:27:17
    对List对象列表属性值的快速搜索 对于数据的搜索已有很多成熟的方案,比如Apace Lucene框架,结合ikanalyer等分词器能实现很复杂和高效的搜索,或直接使用sql语言对数据库关键字进行搜索等。 但这些搜索都很重,...
  • Dart 内存管理机制

    千次阅读 2019-12-27 15:36:30
    本文介绍了Dart内存分配与回收
  • JVM内存结构和Java内存模型别再傻傻分不清了

    万次阅读 多人点赞 2020-03-04 20:53:30
    JVM内存结构和Java内存模型都是面试的热点问题,名字看感觉都差不多,网上有些博客也都把这两个概念混着用,实际上他们之间差别还是挺的。 通俗点说,JVM内存结构是与JVM的内部存储结构相关,而Java内存模型是与多...
  • C# 垃圾回收中的大对象

    千次阅读 2015-12-14 17:59:58
    CLR垃圾回收器根据所占...比如内存碎片整理 ------ 在内存中移动大对象的成本是昂贵的,让我们研究一下垃圾回收器是如何处理大对象的,大对象对程序性能有哪些潜在的影响。 大对象堆和垃圾回收 在.Net 1.0和2
  • C和C++安全编码笔记:动态内存管理

    千次阅读 多人点赞 2020-05-04 18:23:21
    4.1 C内存管理: C标准内存管理函数: ...(2).aligned_alloc(size_t alignment, size_t size):为一个对象分配size个字节的空间,此对象的对齐方式是alignment指定的。alignment的值必须是实现支持...
  • 刚刚做完了一个项目的性能测试,“有幸”也遇到了内存泄露的案例,所以在此和大家分享一下。 主要从以下几部分来说明,关于内存内存泄露、溢出的概念,区分内存泄露和内存溢出;内存的区域划分,了解GC回收机制;...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 93,772
精华内容 37,508
热门标签
关键字:

对象属性过多内存大