每天都是面对对象编程,你真的了解你的对象吗?

每天都是面对对象编程,你真的了解你的对象吗?

 

Java是一种面向对象的编程语言,详细本身对对象的理解是否只有一句话来描述:一切皆对象,new出来的对象都在堆上!等等,这不是2句话?不,后面这句只是我写这篇文章的起因。初学Java你们都说new出来的对象都在堆上,对此深信不疑!可是后续愈加对这句话产生怀疑,想一想每一个类的toString方法都会new一个StringBuffer,这样作堆内存岂不是增大一倍?For循环中建立对象为何没有堆溢出?建立的对象到底在堆中占用多少内存?怀着以上疑问往下看,本篇文章做为Java对象的综合整理来描述何谓对象。java

 

Java中一切皆对象,对象的建立主要以下:程序员

People people = new People();

如今面试都是各类文字坑,例如:问这个对象是否在堆上分配内存?怎么回答,是?不是?面试

这个问题,要根据上下文来回答,就是要根据这行代码所处的环境来回答,何谓环境:运行环境JRE、书写位置,不一样环境结果不同。想知道结果,先Get到如下知识点:算法

逃逸分析是JDK6+版本后默认开启的技术(如今都JDK15了,都是旧技术了==!),主要分析方法内部的局部变量的引用做用域,用于作后续优化。逃逸分析以后一个方法内的局部变量被分为3类逃逸对象编程

  • 全局逃逸对象: 对外部而言,该对象能够在类级别上直接访问到(调用类获取对象实例)
  • 参数逃逸对象:对外部而言,该对象能够在方法级别上直接访问到(调用方法获取对象实例)
  • 未逃逸对象:对外部而言,该对象仿佛不存在同样,不可嗅探

后续优化指的是对未逃逸的优化,主要分为标量替换和锁消除数组

标量替换:在Java中8种基本数据类型已是能够直接分配空间的,不可再被细化,称为标准变量,简称标量。对象的引用是内存地址也不可再被细化,也能够称为标量。而Java对象则是由多个标量聚合而来,称为聚合量。按照这种标准将Java对象的成员变量拆分替换为标量的过程,称为标量替换。这个过程会致使对象的分配不必定在堆中,而是在栈上或者寄存器中。安全

锁消除Java锁是针对多线程而使用的,当在单线程环境下使用锁后被JIT编译器优化后就会移除掉锁相关代码,这个过程就是锁消除(属于优化,不影响对象)。多线程

指针压缩:32位机器对象的引用指针使用32位表示,在64位使用64位表示,一样的配置而内存占用增多,这样真的好吗?JDK给出指针优化技术,将64位(8字节)指针引用(Refrence类型)压缩为32位(4字节)来节省内存空间。jvm

对象的逃逸

一个标准大小=32byte的Java对象(后面会写如何计算)编程语言

class People {
    int i1;
    int i2;
    int i3;
    byte b1;
    byte b2;
    String str;
}

未逃逸对象

public class EscapeAnalysis {

    public static void main(String[] args) throws IOException {
        // 预估:在不发生GC状况下32M内存
        for (int j = 0; j < 1024 * 1024; j++) {
            unMethodEscapeAnalysis();
        }
        // 阻塞线程,便于内存分析
        System.in.read();
    }

    /**
     * people对象引用做用域未超出方法做用域范围
     */
    private static void unMethodEscapeAnalysis() {
        People people = new People();
        // do  something
    }
}

 

未开启逃逸分析

启动JVM参数

-server -Xss108k -Xmx1G -Xms1G -XX:+PrintGC -XX:-UseTLAB -XX:-DoEscapeAnalysis -XX:-EliminateAllocations

启动控制台:无输出:未发生GC

 

堆内存查看

$ jps
3024 Jps
16436 EscapeAnalysis
24072 KotlinCompileDaemon

 

$ jmap -histo 16436

 num     #instances         #bytes  class name
----------------------------------------------
   1:       1048576       33554432  cn.tinyice.demo.object.People
   2:          1547        1074176  [B
   3:          6723        1009904  [C
   4:          4374          69984  java.lang.String

 

此时堆中共建立了1024*1024个实例,每一个实例32byte,共32M内存

开启逃逸分析

启动JVM参数

-server -Xss108k -Xmx1G -Xms1G -XX:+PrintGC -XX:-UseTLAB -XX:-DoEscapeAnalysis -XX:-EliminateAllocations

启动控制台:无输出:未发生GC

 

堆内存查看

$ jps
3840 Jps
24072 KotlinCompileDaemon
25272 EscapeAnalysis

 

$ jmap -histo 25272

 num     #instances         #bytes  class name
----------------------------------------------
   1:       1048576       33554432  cn.tinyice.demo.object.People
   2:          1547        1074176  [B
   3:          6721        1009840  [C
   4:          4372          69952  java.lang.String

此时与未开启一致,仍然是在堆中建立了1024*1024个实例,每一个实例32byte,共32M内存

开启逃逸分析和标量替换

启动JVM参数

-server -Xss108k -Xmx1G -Xms1G -XX:+PrintGC -XX:-UseTLAB -XX:+DoEscapeAnalysis -XX:+EliminateAllocations

堆内存查看

$ jps
7828 Jps
21816 EscapeAnalysis
24072 KotlinCompileDaemon

 

$ jmap -histo 21816

 num     #instances         #bytes  class name
----------------------------------------------
   1:         92027        2944864  cn.tinyice.demo.object.People
   2:          1547        1074176  [B
   3:          6721        1009840  [C
   4:          4372          69952  java.lang.String

此时堆中仅建立了92027个实例,内存占用少了11倍。

启动控制台:无输出:未发生GC,说明实例的确未分配到堆中

 

 

未分配到堆中,是由于一部分分配到了栈中,这种未逃逸对象若是分配到栈上,则其生命周期随栈一块儿,使用完毕自动销毁。下面为java对象分配的具体细节。

 

对象的内存分配

 

实例分配原则
  1. 尝试栈上分配
    • 基于逃逸分析和标量替换,将线程私有对象直接分配在栈上
    • 在函数调用完毕后自动销毁对象,不须要GC回收
    • 栈空间很小,默认108K,不能分配大对象
  2. 尝试TLAB
    • 判断是否使用TLAB(Thread Local Allocation Buffer)技术
      • 虚拟机参数 -XX:+UseTLAB,-XX:-UseTLAB,默认开启
      • 虚拟机参数-XX:TLABWasteTargetPercent 来设置TLAB占用eEden空间百分比,默认1%
      • 虚拟机参数-XX:+PrintTLAB 打印TLAB的使用状况
      • TLAB自己占用eEden区空间,空间很小不能存放大对象,
      • 每一个线程在Java堆中预先分配了一小块内存,当有对象建立请求内存分配时,就会在该块内存上进行分配
      • 使用线程控制安全,不须要在Eden区经过同步控制进行内存分配
  3. 尝试老年代分配(堆分配原则)
    • 若是能够直接进入老年代,直接在老年代分配
  4. 以上都失败时(注意分配对象时很容易触发GC,堆分配原则)
    • 内存连续时:使用指针碰撞(Serial、ParNew等带Compact过程的收集器)
      • 分配在堆的Eden区,该区域内存连续
      • 指针始终指向空闲区的起始位置。
      • 在新对象分配空间后,指针向后移动了该对象所占空间的大小个单位,从而指向新的空闲区的起始位置
      • 对象分配过程当中使用了CAS加失败重试的方式来保证线程安全(CAS即原子操做)
      • 若是成功:则进行对象头信息设置
    • 内存不连续时:使用空闲列表(CMS这种基于Mark-Sweep算法的收集器)
      • 若是堆空间不是连续的,则JVM维护一张关系表,来使内存逻辑上连续从而达到对象分配的目
Image [13].png
堆分配原则:

  • 优先在Eden(伊甸园)区进行分配
    • 可经过-XX:SurvivorRation=8来肯定Eden与Survivor比例为 8:1
    • 新生代存在2个Survivor区域(From和To),当新生代10份时,Survivor共占2份,Eden占8份
    • 新建对象会先在Eden中分配
      • 空间足够时直接分配
      • 当Eden空间不足时
        • 将Eden内的对象进行一次Minor Gc 回收准备放入进入From类型的Survivor区
          • From类型的Survivor区
            • 空间足够时,放置GC对象时将GC对象回收进来
            • 空间不足时,将GC对象直接放入老年代中
        • Minor GC后Eden空间仍然不足
          • 新建对象直接进入老年代
  • 长期存活的对象移交老年代(永久代)
    • 在Eden的对象通过一次Minor GC进入Survivo 区后,对象的对象头信息年龄字段Age+1
    • Survivor区对象每通过一次Minor GC对象头信息年龄字段Age+1
      • 会在From Survivor和ToSurvivor 区进行来回GC(复制算法)
    • 当对象的年龄达到必定值(默认15岁)时就会晋升到老年代
    • -XX:MaxTenuringThreshold=15设置分代年龄为15
  • 大对象直接进入老年代(永久代)
    • 大对象为占用堆内大量连续空间的对象(数组类、字符串)
    • -XX:MaxTenuringThreshold=4M 能够设置大于4M的对象直接进入老年代
  • 动态年龄判断
    • GC回收对象时并不必定必须严格要求分代年龄进行晋升老年代
    • 当Survivor区的同年龄对象的总和大于Survivor空间1/2时
      • 年龄大于等于该年龄(相同年龄)的对象均可以直接进入老年代
  • 老年代对象分配使用空间分配担保
    • 新生代全部对象大小小于老年代可用空间大小时,Minor GC是安全的
      • 至关于新生代全部对象均可以放到老年代里面,于是不会出现溢出等现象
    • 相反,Minor GC是不安全的
      • 至关于新生代对象只能有一部分能够放入老年代,另外一部分会由于空间不足而放入失败
      • 安全措施-XX:HandlePromotionFailure=true,容许担保失败
      • 发生MinorGC以前,JVM会判断以前每次晋升到老年代的平均大小是否大于老年代剩余空间的大小
        • 若小于于而且容许担保失败则进行一次Minor GC
          • 对象GC预测平稳,不会发生大量对象忽然进入老年代致使其空间不足而溢出
        • 若小于而且不容许担保失败则进行一次full GC
          • 即便对象GC预测平稳,可是不保证不会激增,因此安全点仍是先去Full GC下
          • 回收全部区域,给老年代清理出更多空间
        • 若小于即便容许担保失败也进行一次full GC
          • 即Minor GC后的存活对象数量忽然暴增,即便容许担保失败可是仍是极大多是不安全的
          • 回收全部区域,给老年代清理出更多空间

对象实例组成

  • 对象头

    • MarkWord(必须)
    • 类型指针:指向对象的类元数据(非必须)
    • 数组长度(数组类型对象才有)
  • 实例数据

    • 对象的字段属性,方法等,存储在堆中
  • 数据填充

    • JVM要求java的对象占的内存大小应该是8bit的倍数
    • 实例数据有可能不是8的倍数,须要使用0进行填充对齐
 
MarkWord结构
 
25Bit
 
4bit
1bit
2bit
 
锁状态
23bit
2bit
偏向锁
锁标志
哈希码
分代年龄
0
01
无锁
指向锁记录的指针
00
轻量锁
指向重量锁的指针
10
重量锁
11
GC标记
线程ID
时间戳
分代年龄
1
01
偏向锁
 

对象的初始化

因为对象初始化涉及到类加载,这里很少描述

  • 分配到的空间设置为0
  • 数据填充0,8字节对齐
  • 对象头信息设置
  • 调用<init>进行初始化(类的实例化)

给个示例先体会下

public class ClinitObject {

    static ClinitObject clinitObject;

    static {
        b = 2;
        clinitObject = new ClinitObject();
        System.out.println(clinitObject.toString());
    }

    int a = 1;
    static int b;
    final static int c = b;
    final static String d = new String("d");
    String e = "e";
    String f = "f";

    public ClinitObject() {
        e = d;
        a = c;
    }

    @Override
    public String toString() {
        return "ClinitObject{" + "\n" +
                "\t" + "a=" + a + "\n" +
                "\t" + "b=" + b + "\n" +
                "\t" + "c=" + c + "\n" +
                "\t" + "d=" + d + "\n" +
                "\t" + "e=" + e + "\n" +
                "\t" + "f=" + f + "\n" +
                '}';
    }

    public static void main(String[] args) {
        System.out.println(clinitObject.toString());
    }
}

控制台

ClinitObject{
	a=0
	b=2
	c=0
	d=null
	e=null
	f=f
}
ClinitObject{
	a=0
	b=2
	c=2
	d=d
	e=null
	f=f
}
 

对象的大小计算

  • 普通对象

    • 4或8字节(MarkWord)+4或8字节(klass Reference)+实例数据长度+ 0填充(Padding)
  • 数组对象

    • 4或8字节(MarkWord)+4或8字节(klass Reference)+4字节(ArrayLength)+实例数据长度+0填充(Padding)
  • 其它说明:

    • 对象头(MarkWord)在32位JVM中为4字节,在64位JVM中为8字节
    • 为了节约空间,使用了指针压缩技术:
      • JDK6开始对类型指针(Reference)进行压缩,压缩前8字节,压缩后4字节
        • 参数 -XX:+UseCompressedOops
      • JDK8开始新增元数据空间metaSpace,因而新增参数来控制指针压缩:
        • -XX:+UseCompressedClassPointers(指针压缩开关,堆内存>=32G时,自动关闭)
        • -XX:CompressedClassSpaceSize (Reference指向的类元数据空间大小,默认1G,上限32G)
    • 数据填充(Padding)为保证对象大小为8的整数倍的数据填充,使数据对齐
  • 经常使用数据类型大小

数据类型
占用空间(byte)
byte
1
short
2
int
4
long
8
char
2
float
4
double
8
boolean
1或4 ,计算大小时为1,判断真假时为4(底层为int常量0,1)
Object(存储的是引用指针)
由计算机位数和是否指针压缩决定 4或8 字节

对象的定位

java源码中调用对象在JVM中是经过虚拟机栈中本地变量标的reference来指向对象的引用来定位和访问堆中的对象的,访问方式存在主流的2种
  • 句柄访问

    • jvm堆中单独维护一张reference与对象实例数据(实例化数据)和对象类型数据(ClassFile数据)的关系表
    • 经过该关系表来查找到java实例对象
  • 直接访问(Sun HotSpot 采用该方式)

    • reference直接指向了java堆中对象的实例数据(实例化数据),该实例对象的类型指针(Reference)指向类型数据(ClassFile数据)

 

指针压缩示例

public class CompressedClassPointer {

    public static void main(String[] args) throws IOException {
        People people=new People();
        System.in.read();
    }
}

启用指针压缩(默认)

JVM参数

-server -XX:+UseCompressedOops -XX:+UseCompressedClassPointers -XX:CompressedClassSpaceSize=1G

堆内存查看

$ jps
11440
11744 RemoteMavenServer
14928 KotlinCompileDaemon
15540 Launcher
15908 Jps
9996 CompressedClassPointer

 

$ jmap.exe -histo 9996

 num     #instances         #bytes  class name
----------------------------------------------
... 
233:             1             32  cn.tinyice.demo.object.People

关闭指针压缩

JVM参数

-server -XX:-UseCompressedOops

堆内存查看

$ jps
11440
11744 RemoteMavenServer
14928 KotlinCompileDaemon
8448 CompressedClassPointer

 

$ jmap.exe -histo 8448

 num     #instances         #bytes  class name
----------------------------------------------
...
254:             1             40  cn.tinyice.demo.object.People

 

 

示例解析

示例中开启以后对象大小会减小8byte。而指针压缩是8字节变4字节,按理说应该少4字节即32位,为何这个样子?

 

开启压缩指针时的对象大小计算

/**
 * Size(People) =
 * 8(mark word)+4(klass reference)+ 4(i1)+4(i2)+4(i2)+1(b1)+1(b2)+4(str reference) + 2(padding)
 * |----------------------------------- 30 byte ---------------------------------|----00-------/
 * |---------------------------------------- 32 byte ------------------------------------------/
 */

 

关闭压缩指针时的对象大小计算

/**
 * Size(People) =
 * 8(mark word)+8(klass reference)+ 4(i1)+4(i2)+4(i2)+1(b1)+1(b2)+8(str reference) + 2(padding)
 * |----------------------------------- 38 byte ---------------------------------|----00-------/
 * |---------------------------------------- 40 byte ------------------------------------------/
 */

 

这里就看到区别了,是数据填充形成的,java为了便于数据管理,因而对象都是8字节对齐的,不足的使用0进行填充(padding)。

至于对象的实例化,会在写类加载流程是再作描述。

 

原文地址: 程序员微录  每天都是面对对象编程,你真的了解你的对象吗?

相关文章
相关标签/搜索