java基础原理记录

八种基本数据类型

常见概念
1byte=8b(bit),1KB=1024B,1MB=1024KB,1GB=1024MB
为什么1个字节为8位二进制勒?标准来自于IBM system/360,导致后来的硬件都是按照这个标签设计的
在UTF-8下中文占3个字节,在GBK下面中文占2字节, 英文在utf-8和gbk下面都是占一个字节
byte
byte的取值范围为-128~127(-2的7次方到2的7次方-1),byte也就是俗称的1个字节
byte[] intArrya=new byte[1024*1024]; 占用1M 堆内存
整型
java可以表示整数类型的有short(短整型)、int(整型)、long(长整型)
  • short
short的取值范围为-32768~32767,占用2个字节(-2的15次方到2的15次方-1)
  • int
int的取值范围为-2147483648~2147483647,占用4个字节(-2的31次方到2的31次方-1)
为何32位最大的正整数不是4294967295而是2147483647?
因为左起第一位做符号保留,0 正数,1.负数

1int = 4 byte
int[] intArrya=new int[1024*1024]; 占用4M 堆内存
  • long
long的取值范围为(-9223372036854774808~9223372036854774807),占用8个字节(-2的63次方到2的63次方-1)
long[] intArryay=new long[1024*1024]; 占用8M 堆内存
浮点型
float和double是表示浮点型的数据类型,他们之间的区别在于他们的精确度不同
  • float
占用4个字节,需要用f表示
  • double
占用8个字节 double型比float型存储范围更大,精度更高,
boolean型
这个类型只有两个值,true和false 占用一个字节,false=0 true=1
char型
用于存放字符的数据类型,占用2个字节,因为没有符号位,所以是(0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111 1111 (65535)),
采用unicode编码,它的前128字节编码与ASCII兼容
字符的存储范围在\u0000~\uFFFF,在定义字符型的数据时候要注意加' ',比如 '1'表示字符'1'而不是数值1,
char+char,char+int——类型均提升为int
一些概念
转换规则:
从低位类型到高位类型自动转换;从高位类型到低位类型需要强制类型转换:
算术运算 中的类型转换:1 基本就是先转换为高位数据类型,再参加运算,结果也是最高位的数据类型;
2 byte short char运算会转换为Int;

java IO整理

字节与字符的区别
它们完全不是一个位面的概念,所以两者之间没有“区别”这个说法,字节Byte是一种计量单位,表示数据量多少,它是计算机信息技术用于计量存储容量的一种计量单位。
字符是指计算机中使用的文字和符号,不同编码里,字符和字节的对应关系不同。
例如:UTF-8编码中,一个英文字符等于一个字节,一个中文(含繁体)等于三个字节
     在GBK编码,一个英文字符等于一个字节,一个汉字占两个字节
字节流与字符流
字节流在输出的时候直接操作文件本身。
字符流在输出的时候会使用缓冲区,最直观的就是如果我们在写文件的时候,如果使用字符流,不flush或者close(),内容
是不会写到文件里面的,而字节流输出时候是实时的。

java类的整理

java类的生命周期
当我们编写一个xx.java文件时候,会被编译成XX.class的文件,俗称字节码文件也就是Java程序对应的16进制格式的文件,只有字节码才能在Java虚拟机中运行。
java类的生命周期就是指一个class文件从加载到卸载的全过程。
java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况
加载

类的加载方式有多种:

1、根据classes路径,找到所有.class文件,然后再从class文件里面读取文件的字节码。
2、从程序依赖的jar包里面读取
3、动态代理生成各种代理类

Class.forName()与ClassLoader.load()区别

Class.forName()底层是调用Class.forName0(String name, boolean initialize, ClassLoader loader)
默认initialize是true,也就是加载+初始化(执行static静态代码块)
ClassLoader.load()底层是调用Classloder. loadClass(String name, boolean resolve)
默认resolve是false,也就是不解析,就只是加载改类。

Class.forName()的场景

例如加载mysql驱动器的代码,Class.froName(“com.mysql.jdbc.Driver”),这种是显示声明,
其实:Driver这个类只有一个static块,我们必须要初始化才可以使用
 public class Driver extends NonRegisteringDriver implements java.sql.Driver {

     public Driver() throws SQLException {

     }
     static {
         try {
             DriverManager.registerDriver(new Driver());
         } catch (SQLException var1) {
             throw new RuntimeException("Can\'t register driver!");
         }
     }
 }

class、Class、Object的关系

class是个关键字,一般用与修饰类
Class是个特殊的类,一般用与反射,用来获取类的字段,属性,构造器,方法等等
Object是一个特殊的类,所有的类都继承该类,包括Class也继承Object

类名.class, class.forName(), getClass()三者区别
Class.forName()在运行时加载;
Class.class和getClass()是在编译器加载,即.class是静态加载,.getClass()是动态加载。
,.getClass()只有对象对实例方法才有,类的Class类实例是通过.class获得的
连接
连接包括(验证->准备->解析)三个部分
验证:包括件格式验证,是否以魔数开头,主版本是否在当前虚拟机处理范围内等
准备:为类变量分配内存,并设置类变量初始值。(static修饰的变量)例如给int赋值0
解析:类和接口的解析、字段解析、类方法解析、接口方法解析
初始化
如果一个类被直接引用,就会触发初始化,间接引用除外,例如调用类的静态方法

类的实例化与初始化
一个类如果想实例化,那么JVM首先会检查相关类型是否已经加载并初始,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。
在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化

那些情况会触发类的初始化

1、遇到new,getstatic pubstatic invokestatic等字节码指令。
读取或者设置一个类的静态字段(如果静态字段被final修饰,就不会初始化,
因为已经在编译期间将结果放入常量池,变量必须要是基本类型或者字符串)
2、利用reflect方法对类进行反射调用以上几种凡是。
3、当初始化一个类,如果父类没初始化,首先初始化父类。
4、虚拟机启动时候,需要指定一个要执行的类,也就是main方法。

java字节码

编译和查看编译内容

编译:javac Test.java
查看.calss文件 javap -verbose  Test
 或者 vi Test.class 然后输入%!xxd将文件转成16进制

ClassFile结构体

ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
magic
为了方便虚拟机识别一个文件是否是class类型的文件,SUN公司规定每个class文件都必须以一个word(四个字节)作为开始,
这个数字就是魔数。魔数是由四个字节的无符号数组成的,而class文件的名字还挺好听的的,其魔数就是0xCAFEBABE
minor_version、major_version
其实就是Class文件的副、主版本
constant_pool[]
常量池,constant_pool是一种表结构,它包含Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。
常量池中的每一项都具备相同的格式特征——第一个字节作为类型标记用于识别该项是哪种类型的常量,称为“tag byte”。
常量池的索引范围是1至constant_pool_count−1
access_flags
访问标志,access_flags是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。
例如 ACC_PUBLIC, ACC_SUPER 就是public

String

 String类型的最大长度是Integer.MaxValue
 String的定义方法归纳起来总共为三种方式:
 使用关键字new,如:String s1 = new String("myString");
 直接定义,如:String s1 = "myString";
 串联生成,如:String s1 = "my" + "String";编译期间会优化成直接定义。

String a = "12";
String b = "1" + "2";// 编译后虚拟机直接优化成 LDC "12"
String c = "1" + new String("2");// NEW java/lang/StringBuilder append
System.out.println(a == b);
System.out.println(a == c);

String d = "ddd";//会放到常量池
String f = d.intern();//直接从常量池里面拿数据
System.out.println(d == f);//true  常量池和常量池里面数据比较

String e = new String("ddd");//new 新实例
System.out.println(e == d);//false 常量池和新的实例比较

String g = e.intern();//直接从常量池里面拿数据
System.out.println(g == d);//true



String s3="hello" ;String s4="hello";
System.out.println(s3 == s4);//true 直接从常量池里面拿数据

java.util包源码分析

Collection
ArrayList
* 原理概括
    基于数组,没有扩容因子 当数组满了就扩容为1.5倍
    数组的特性就是查找是O(1),删除和新增都是O(N)
    当数组数据量比较大时候,扩容比较费时间
LinkedList
* 原理概括
   链表的特性就是随机读麻烦,删除与插入方便,只需要移动链表指针
   LinkedList基于双向链表,定义了first与last两个特殊节点,
   添加时候直接last添加,删除时候直接first删除
   指定位置插入与查找,可以当前index是否大于链表长度一半,
   决定从first还是last循环,提高查询效率
Map
HashMap
  • 源码注释
hashMap实现Map接口,允许key或者value为空(一般key为null的value放在链表数组第一位)
源码:putForNullKey(value)方法,这样做的好处是查询key为null的速度快。
hashMap不完全和HashTable类似,因为hashMap非线程安全。
hashMap初始容量为16,构造方法提供capacity参数修改初始容量的值
当我们可以预估hasMap需要存放的值,就可以设定初始容量,必须扩容带来的开销
hashMap加载因子为0.75f,当hashMap容量达到capacity*loadFactor时候表示hashMap需要扩容
至于hashMap的默认加载因子设定的值为什么是0.75f,其实是处于空间和时间上面考虑的,
如果loadFactor过大,节省了空间,但是加大了链表查询速度,反之相反。

如果我们想使用线程安全的hashMap,可以使用
Map m = Collections.synchronizedMap(new HashMap(...))
其实就是用SynchronizedMap(new HashMap())包装了一层hashMap,然后对map的所有方法加
synchronized (mutex){...}同步代码块锁

如果我们在迭代器里面修改增加或者减小manp的值会抛出ConcurrentModificationException
  • 原理概括
HashMap实际上是一个“链表散列”的数据结构,即数组+链表,Entry[] table+entry链表

JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

hashMap的容量是2的倍数,这样会让key散列的更均匀
static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
}
 & 与运算只有两个位都是1,结果才是1,
如果1的个数越多,那么出现1的次数才最多,那么hash 散列的就更均匀
比如4的二进制是0100
3的二进制是   4-1=3= 0101
0001 0010 0011 0101
1&4,2&4,3&4,4&4 结果值区间范围在{0 、4}
1&3,2&3,3&3,4&3 结果值区间范围在{0 、1、2、3}
可以明显看出来如果length如果是2的倍数,则indexFor散列的就跟均匀一点

如果hashmap不扩容会导致什么?
那就退化成了链表,这样查询效率就非常低,因为hash冲突是固定的
hashmap 假设在极端低情况下如果不出现hash冲突,其实就演变成数组,查询效率极高。
  • 非线程安全地方

    • 并发put导致相同hash值可能丢失

    如果多个线程同时put,并且对应的key的hash都一样,如果并发同时执行到createEntry方法
    同时获取到头结点的table[bucketIndex]值e,这时候同时对e进行操作会丢失最新执行更新线程的value

    void createEntry(int hash, K key, V value, int bucketIndex) {

    //并发同时获取到e值
    Entry<K,V> e = table[bucketIndex];
    //会导致最新执行的值被覆盖
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
    

    }

    • 当hashMap在扩容时候多线程环境下面会导致死循环
如果同时多个线程触发了rehash操作,会导致HashMap中的链表中出现循环节点,进而使得后面get的时候,会死循环。
 void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
  • hasMap遇到hash冲突解决
采用链地址法,将所有关键字为同义词的记录存储在同一线性链表中
关键代码取hash与indexFor
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}
  • HashTable

    • 实现原理 散列表,数组+链表结构实现
    • 线程安全(synchronized对方法加锁),key值不可以为空
    • HashTable实现了Map接口和Dictionary抽象类
    • 初始容量11 初始因子0.75f
  • ThreeMap

    • 实现原理:是一个有序的key-value集合,基于红黑树(Red-Black tree)实现。
      该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法
    • 非线程安全
  • LinkedHashMap
    • LinkedHashMap是HashMap的一个子类,它保留插入的顺序,先进先出,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap
    • 非线程安全
    • 当LinkedHashMap工作在这个模式时,不能再迭代器中使用get()操作。Map的遍历建议使用entrySet的方式
      for(Map.Entry entry:map.entrySet()){
      System.out.println(entry.getKey()+":"+entry.getValue());
      
      }
  • WeakHashMap
原理和hashMap类似,通过WeakReference和ReferenceQueue实现的。
WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,
它会保存被GC回收的“弱键”
如果某个业务的数据不是特别重要,可以放到weakHashMap里面处理,可以随机的丢去一些数据,
WeakHashMap中的key都存在强引用,那么WeakHashMap就会退化成HashMap。
如果在系统中希望通过WeakHashMap自动清除数据,请尽量不要在系统的其他地方强引用WeakHashMap的key
List
  • LinkedList
    • 原理:LinkedList 删除和新增效率高,基于链表实现的
    • 数组长度扩容是1.5倍,默认容量是10
    • foeach性能比迭代性能差,通过反编译可看出,foeach的代码都是转成迭代,但多一步赋值操作
  • ArrayList
    • 原理:ArrayList的随机访问更高,基于数组实现的ArrayList可直接定位到目标对象
  • Vector
    • Vector是线程安全的,而ArrayList不是
    • 由于Vector中的方法基本都是synchronized的,其性能低于ArrayList
    • Vector可以定义数组长度扩容的因子,默认容量是10

      Thread

  • Thread
    一般启动线程有两种方式。最终都是调用
    private native void start0();
    native方法会调用run(),如果是子类方式启动线程,就是调用子类重写的run(),如果是实现runable接口方式
    启动线程就是调用Thread的run(),最终也是调用接口重写的run()
    public void run() {
           if (target != null) {
               target.run();
           }
    }
    

java.util.concurrent包下面常用类的原理

  • ConcurrentHashMap

    • 参考:聊聊并发(四)深入分析ConcurrentHashMap
    • key值不可以为空
    • 原理:锁分段技术

      ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成,Segment的结构和HashMap类似,是一种数组和链表结构,
       一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数组里的元素,
       当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁.
      
    • 锁:使用ReentrantLock和volatile

      写的时候首先根据hash值获取对应的segment锁,
      读的时候不加锁 根据volatile
      get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空的才会加锁重读,
      我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?
      原因是它的get方法里将要使用的共享变量都定义成volatile,
      
      如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,
      能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,
      但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),
      在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,
      是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,
      即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。
      

JDK1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全。数据结构采用:数组+链表+红黑树。
  • CopyOnWriteArrayList

    • 原理:写加Reentrant锁,读不加锁

      CopyOnWriteArrayList的核心思想是利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,
      先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,
      当然写操作的锁是必不可少的了。
      
    • 使用场景与问题:适用写多读少的场景

       内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象
      (注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。
      如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。
      之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
      针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。
      或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
      数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器
      
  • ReentrantLock

    • 原理

      ReentrantLock是基于AQS实现的,AbstractQueuedSynchronizer基础又是CAS
      
    • 分为ReentrantReadWriteLock与ReentrantLock

      需要手动去关闭锁,锁的类型有ReentrantLock(可重入锁),ReentrantReadWriteLock 读写锁
      lock()、tryLock()、lock.newCondition()、tryLock(long time, TimeUnit unit)
      和lockInterruptibly() Lock里面的接口。如果同时启动两个线程调用tryLock(),其中一个线程会获取不到锁,退出执行;
      如果调用tryLock(long time, TimeUnit unit)其中一个线程获取不到锁会等待一段时间,然后继续执行。
      condition.signal();condition.await();和wait()和notify()一个意思,不过condition是和lock绑定的
      
      • lock与synchronized区别:

        1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
        2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,
        如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
        3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
        4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
        5)Lock可以提高多个线程进行读操作的效率。
        6)lock可以实现ReentrantReadWriteLock  实现读取不加锁,而写加锁
        
  • Atomic**

    • 原理

      首先使用了volatile 保证了内存可见性。
      然后使用了CAS(compare-and-swap)算法 保证了原子性,cas是借助C来调用CPU底层指令实现的
      其中CAS算法的原理就是里面包含三个值:内存值A  预估值V  更新值 B
      当且仅当 V == A 时,V = B; 否则,不会执行任何操作。
      Unsafe是CAS的核心类
      cas会出现ABA问题
      ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,
      等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,
      于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合
      
  • volatile

    被volatile修饰的变量,具备可见性和禁止指令排序。
    通过Java的内存模型可知,线程执行的时候,分为工作内存和主内存,操作变量的时候
    一般都是先从主内存里面读取变量的值,然后再load到工作内存,例如A线程执行的期间,B线程对此变量修改值,然后write到主内存。
    但是A线程操作的还是工作内存的数据。
    
  • Synchronized

    代码块同步原理是使用monitorenter和monitorexit指令实现
    
  • ReentrantLock

    ReentrantLock是基于AQS实现的,AbstractQueuedSynchronizer基础又是CAS
    
  • ThreadPool

    线程重用的核心是,ThreadPoolExecutor里面维护一个workers Set,当我们添加线程的时候,
    如果workQueue队列的线程数小于核心线程 则创建新的worker,同时执行thread任务里面的run()方法,
    worker里面的线程一直while(true)循环调用,当workQueue队列里面有任务就会执行。
    源码:ThreadPoolExecutor.Worker类
       /** Delegates main run loop to outer runWorker  */
            public void run() {
                runWorker(this);
            }
    
    final void runWorker(Worker w) {
            Thread wt = Thread.currentThread();
            Runnable task = w.firstTask;
            w.firstTask = null;
            w.unlock(); // allow interrupts
            boolean completedAbruptly = true;
            try {
            //循环执行run方法,getTask()如果没有要执行的任务会堵塞
                while (task != null || (task = getTask()) != null) {
                    w.lock();
                    // If pool is stopping, ensure thread is interrupted;
                    // if not, ensure thread is not interrupted.  This
                    // requires a recheck in second case to deal with
                    // shutdownNow race while clearing interrupt
                    if ((runStateAtLeast(ctl.get(), STOP) ||
                         (Thread.interrupted() &&
                          runStateAtLeast(ctl.get(), STOP))) &&
                        !wt.isInterrupted())
                        wt.interrupt();
                    try {
                        beforeExecute(wt, task);
                        Throwable thrown = null;
                        try {
                            task.run();
                        } catch (RuntimeException x) {
                            thrown = x; throw x;
                        } catch (Error x) {
                            thrown = x; throw x;
                        } catch (Throwable x) {
                            thrown = x; throw new Error(x);
                        } finally {
                            afterExecute(task, thrown);
                        }
                    } finally {
                        task = null;
                        w.completedTasks++;
                        w.unlock();
                    }
                }
                completedAbruptly = false;
            } finally {
                processWorkerExit(w, completedAbruptly);
            }
        }
    

JDBC

jdbc中execute、executeQuery和executeUpdate的区别
execute是指在不知道执行sql的类型情况下使用的,一般使用场景不多,当然如果我们
执行存储过程,需要用 execute
当我们知道DML的sql的场景就可以使用对应的executeQuery或者executeUpdate

java对象的内存布局

https://blog.csdn.net/qqHJQS/article/details/100944914
https://blog.csdn.net/yunqiinsight/article/details/80431831
对象头 header
1、用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,
这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为"Mark Word”
占用4(32)/8(64)byte
2、存储指向对象类型数据的指针oop
    指针就是确定这个对象是哪个类的实例 也就是常说的 xxx.class

占用4(32)/8(64)byte
3、java数组

实例数据 instance
对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来
对齐填充 Padding
对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,
换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,
就需要通过对齐填充来补全,HotSpot的对齐方式为8字节对齐:
(Header + Instance Data + Paddding) % 8 = 0 并且 0 <= padding < 8
指针压缩
开启指针压缩指令-XX:+UseCompressedOops,关闭指令-XX:-UseCompressedOops,只在64位才有效且默认开启,也就是说32位没有指针压缩的概念
名称 32位 64位 开启指针压缩
header 8 16 12
ref 4 8 4
arrayHeader 12 24 16
对象占用内存大小
java对象组成部分主要由
1、 markoop+klassoop
   如果是64位 8+8=16
   如果是64位且开启了指针压缩,klassoop指针会从8压缩到4 占用内存8+4=12
2、instance data
 如果开启了指针压缩,所有引用类型大小都占用4byte
3、padding

例子:
   一、开启了指针压缩
   static class NodeA{
        int age;//占用4byte
    }

    对象头12+ instance4=16
    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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
        12     4    int NodeA.age                                 0
   二、开启了指针压缩
   static class NodeA{
        Integer age;// 开启了指针压缩占用4byte
    }
    对象头12+ instance4=16
     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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
         12     4   java.lang.Integer NodeA.age                                 null
    Instance size: 16 bytes
   三、不开启指针压缩
    static class NodeA{
                   Integer age;
      }

        对象头16+ instance 8=24
    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)                           30 37 40 27 (00110000 00110111 01000000 00100111) (658519856)
        12     4                     (object header)                           02 00 00 00 (00000010 00000000 00000000 00000000) (2)
        16     8   java.lang.Integer NodeA.age                                 null
   Instance size: 24 bytes

     三、不开启指针压缩
        static class NodeA{
           int age;
       }
        对象头16+ instance 4+ padding4 =24
        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)                           30 e7 f1 09 (00110000 11100111 11110001 00001001) (166848304)
            12     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
            16     4    int NodeA.age                                 0
            20     4        (loss due to the next object alignment)
       Instance size: 24 bytes
Shallow 与 Retained区别
Shallow Size 对象自身占用的内存大小,不包括它引用的对象
针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。当然这里面还会包括一些java语言特性的数据存储单元。
针对数组类型的对象,它的大小是数组元素对象的大小总和。
Retained Size Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C, C就是间接引用)

Unsafe

通过unsafe可以计算对象的field的内存偏移量
 private static void offsetTest() {
        MemoryUtil.NodeA nodeA = new MemoryUtil.NodeA();
        try {
            long ageOffset = reflectGetUnsafe().objectFieldOffset(nodeA.getClass().getDeclaredField("age"));
            long nodeBOffset = reflectGetUnsafe().objectFieldOffset(nodeA.getClass().getDeclaredField("nodeB"));
            //markword 占用12,ageOffset为第一个变量所以偏移量12
            log.info("ageOffset field offset {}", ageOffset);
            //16
            log.info("nodeBOffset field offset {}", nodeBOffset);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    private static Unsafe reflectGetUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return null;
        }
    }