八种基本数据类型
常见概念
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进行操作会丢失最新执行更新线程的valuevoid 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 进行排序,具体取决于使用的构造方法 - 非线程安全
- 实现原理:是一个有序的key-value集合,基于红黑树(Red-Black tree)实现。
- LinkedHashMap
- LinkedHashMap是HashMap的一个子类,它保留插入的顺序,先进先出,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap
- 非线程安全
- 当LinkedHashMap工作在这个模式时,不能再迭代器中使用get()操作。Map的遍历建议使用entrySet的方式
for(Map.Entryentry: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
- 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;
}
}