对象的内存布局

对象头(Mark Word)具体包括什么

Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。
关于锁升级的过程请查看synchronize文章。
对象的四种引用
一、强引用
开发中用的最多的就是强引用。
String str="abc";
只要某个对象与强引用关联,那么JVM在内存不足的情况下,宁愿抛出outOfMemoryError错误,也不会回收此类对象。
如果我们想要JVM回收此类被强引用关联的对象,我们可以显示地将str置为null,那么JVM就会在合适的时间回收此对象。
二、软引用
java中使用SoftRefence来表示软引用,如果某个对象与软引用关联,那么JVM只会在内存不足的情况下回收该对象。
那么利用这个特性,软引用可以用来做缓存。
import java.lang.ref.SoftReference;
public class TestRef {
public static void main(String args[]) {
SoftReference<String> str = new SoftReference<String>(new String("abc"));
System.out.println(str.get()); //输出abc
System.gc();//通知JVM进行内存回收
System.out.println(str.get()); //输出abc - 因为内存够用
}
}
软引用适合做缓存,在内存足够时,直接通过软引用取值,无需从真实来源中查询数据,可以显著地提升网站性能。
当内存不足时,能让JVM进行内存回收,从而删除缓存,这时候只能从真实来源查询数据。
三、弱引用
java中使用WeakReference来表示弱引用。
如果某个对象与弱引用关联,那么当JVM在进行垃圾回收时,无论内存是否充足,都会回收此类对象。
import java.lang.ref.WeakReference;
public class TestRef {
public static void main(String args[]) {
WeakReference<String> str = new WeakReference<String>(new String("abc"));
System.out.println(str.get()); //输出abc
//通知JVM进行内存回收
System.gc();
System.out.println(str.get()); //输出null
}
}
被弱引用关联的对象,总是会在垃圾回收时被回收掉。
当第一次输出System.out.println(str.get());有可能取不到str的值。
这是因为我们在声明了弱引用之后,立即对其输出。
而gc线程是一个优先级很低的守护线程,还来不及扫描该该对象所在的区域,
即来不及对该对象的回收。如果我们在声明之后的很长时间后,再次get,是有可能get不到值的。
弱引用可以在回调函数防止内存泄露。因为回调函数往往是匿名内部类,一个非静态的内部类会隐式地持有外部类的一个强引用,
当JVM在回收外部类的时候,此时回调函数在某个线程里面被回调的时候,JVM就无法回收外部类,造成内存泄漏。
ThreadLocal就是靠弱引用来防止内存泄露的,详见ThreadLocal源码文章。
四、虚引用
java中使用PhantomReference来表示虚引用。
虚引用,引用就像形同虚设一样,就像某个对象没有引用与之关联一样。
若某个对象与虚引用关联,那么在任何时候都可能被JVM回收掉。
虚引用不能单独使用,必须配合引用队列一起使用。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class TestRef {
public static void main(String args[]) {
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> str = new PhantomReference<String>("abc", queue);
System.out.println(str.get()); //输出null
}
}
可见使用get方法无法获取该对象的值。
当垃圾回收器准备回收一个对象时,如果发现它与虚引用关联,就会在回收它之前,将这个虚引用加入到引用队列中。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被回收。
如果确实要被回收,就可以做一些回收之前的收尾工作。
创建对象的过程
一、检测类是否被加载
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,
并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
二、为新生对象分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。
三、初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),
这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,
程序能访问到这些字段的数据类型所对应的零值。
四、进行必要的设置
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、
对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
五、执行init方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,
但从Java程序的视角来看,对象创建才刚开始,<init>方法还没有执行,所有的字段都还为零。
所以一般来说,执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,
这样一个真正可用的对象才算完全产生出来。
检测类是否被加载没有加载的先加载→为新生对象分配内存→将分配到的内存空间都初始化为零值→对对象进行必要的设置→执行<init>方法把对象进行初始化
汇编码
0 new #2 <T> 申请内存 内部变量初始值为0
3 dup
4 invokespecial #3 <T.<init>> 赋值 在赋值之前,初始化之后,为半初始化状态
7 astore_1
8 return
对象怎么定位
java对象在访问的时候,我们需要通过java虚拟机栈的reference类型的数据去操作具体的对象。
由于reference类型在java虚拟机规范中只规定了一个对象的引用,
并没有定义这个这个引用应该通过那种方式去定位、访问java堆中的具体对象实例,
所以一般的访问方式也是取决与java虚拟机的类型。目前主流的访问方式有通过句柄和直接指针两种方式。
一、句柄访问

使用句柄访问方式,java堆将会划分出来一部分内存去来作为句柄池,reference中存储的就是对象的句柄地址。
而句柄中则包含对象实例数据的地址和对象类型数据(如对象的类型,实现的接口、方法、父类、field等)的具体地址信息。
下边以一个例子来简单的说明一下:
Object obj = new Object();
Object obj表示一个本地引用,存储在java栈的本地便变量表中,表示一个reference类型的数据。
new Object()作为实例对象存放在java堆中,同时java堆中还存储了Object类的信息(对象类型、实现接口、方法等)的具体地址信息,
这些地址信息所执行的数据类型存储在方法区中。
使用句柄访最大的好处是reference中存储着稳定的句柄地址,因为对象的移动在GC过程中是非常普遍的行为,
对象的移动会导致实例数据的地址发生变化。当对象移动之后(垃圾收集时移动对象是非常普遍的行为),
只需要改变句柄中的对象实例地址即可,reference不用修改。
此方式的好处是对象引用中保存的是稳定的对象句柄的地址,带来的缺点就是访问效率受影响。
二. 直接指针访问

如果使用指针访问,那么java堆对象的布局中就必须考虑如何放置访问类型的相关信息
(如对象的类型,实现的接口、方法、父类、field等),而reference中存储的就是对象的地址。
使用指针访问的好处是访问速度快,它减少了一次指针定位的时间开销,访问速度快。
由于java是面向对象的语言,在开发中java对象的访问非常的频繁,
因此这类开销积少成多也是非常可观的,反之则提升访问速度。
缺点是当对象地址发生变化是引用中保存的数据也需要变化。
对象怎么分配

在JVM中,堆是线程共享的,因此堆上的对象对于各个线程都是共享和可见的,
只要持有对象的引用,就可以访问堆中存储的对象数据。
虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但对于垃圾收集器来说,
无论筛选可回收对象,还是回收和整理内存都需要耗费时间。
如果确定一个对象的作用域不会逃逸出方法之外,那可以将这个对象分配在栈上。
这样,对象所占用的内存空间就可以随栈帧出栈而销毁。
在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,
那大量的对象就会随着方法的结束而自动销毁了,无须通过垃圾收集器回收,可以减小垃圾收集器的负载。
JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。
栈上分配的技术基础:
一是逃逸分析:逃逸分析的目的是判断对象的作用域是否有可能逃逸出方法外(局部)。
1.方法逃逸:例如作为调用参数传递到其他方法中。
2.线程逃逸:有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量。
二是标量替换:允许将对象打散分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。
首先,new一个对象时,首先会尝试在栈上分配,当然对象要够小并且做了逃逸分析之后它逃逸不出去,
这种情况会进行栈上分配,栈上分配的好处是速度非常快,不用垃圾回收,在栈里只要被弹出,就回收了。
如果对象如果特别大,直接在老年代分配,老年代要经过full GC才会回收。
如果对象不是很大,这时会尝试线程本地分配,简称TLAB(Thread Local Allocation Buffer)线程本地缓冲区。
JVM参数里是可以对线程本地的大小进行设置的。当然TLAB也是在堆中的。
JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)
默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,
它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,
并且TLAB上的分配由于是线程私有所以没有锁开销。
因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),
每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,
而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
为什么需要TLAB,这是多线程分配的一个优化,试想,多个线程如果同时操控一个堆,
如果要在堆上分配对象,那么是不是要加锁(或者原子操作)保证分配的原子性?就会发生指针碰撞,
每分配一个对象都来个原子操作,那还怎么玩?所以TLAB就是这样一种神奇的存在,
每个线程单独持有一个Allocation Buffer,自己玩自己的,当自己的buffer不够的时候,
再重新搞一块buffer过来自己用(这时候需要原子操作),通过减少大量的原子操作来提高Allocator的性能。
TLAB分配的对象可以共享吗?
一句话,只要是Heap上的对象,所有线程都是可以共享的,就看你有没有本事访问到了。
在GC的时候只从root sets来扫描对象,而不管你到底在哪个TLAB中。
一个对象占多少字节
对象头 8字节
类型指针 8字节默认 JVM默认开启压缩,由于开启压缩,它压缩到4字节
实例数据 如果里面有实例,比如String s = new String();8字节默认
JVM默认开启压缩,由于开启压缩,它压缩到4字节
Object obj = new Object();
答案是16字节
如果没有实例数据,那么示例数据是0字节
那么对象头8字节+类型指针4字节=12字节
jvm对象对齐是按照8字节倍数,那么12字节会自动扩充到16字节
答案是16字节
hashcode
先说哈希函数,也叫散列函数,也称杂凑函数或杂凑算法。
函数,不就是给个输入(参数),然后返回一个输出(返回值)嘛。
那么哈希函数就是把任意长的输入消息串通过算法变化成固定长的输出串的一种函数。
这个输出串称为该消息的杂凑值。一般用于产生消息摘要,密钥加密等。
通常这个输出是不能反算的,也就是输入是x,输出是y,x可以算出y,但是y不能反算为x。所以哈希函数是单向的。
它的输入不变,输出就不会变。也就是输入永远是x,那么输出的y,就永远与x对应。
常见是用在加密上,如MD5等。MD5相对来说还是非常安全的,目前没人能解密。下面以MD5举例:
输入是你的密码,输出是经过MD5算法后的一个输出串。
那么如果有多个用户密码相同的情况下,比如都用123456当密码,那就会发现MD5算法后的串都是一样的。
那么此时有一个黑客,他入侵了你的数据库,发现有大量的重复的MD5加密后的密码,
他们有一个常用密码表的数据库(彩虹表),这个彩虹表就是明文密码与MD5加密后的意义对应。
他就会去匹配,如果能对上,他也就知道你的密码了。这是一个暴力破解的过程,只是彩虹表把这个过程简化了。
最早的暴力破解是采用随机字符串组合,来看能不能与MD5加密后的密码撞上。
所以单独使用MD5还不是很安全,所以有了盐值加密。这是哈希算法的一个应用。
盐值加密就是用一个随机字符串,随机字符串+MD5产生的密码。那么验证密码的时候随机字符串怎么取?
那么有可能这个随机字符串就是你的username。或者你就产生一个随机数,然后你用MD5算法,将这个随机数
写入到数据库,当下次匹配密码的时候,将MD5加密后的密码,和加密后的随机数,组合。就是你的密码了。
不管怎么样,只要避开彩虹表的功击就行了。
那么hashcode是个什么东西呢。先看一下哈希表
java里有一个数据结构叫哈希表,一个是HashMap,一个叫HashSet,他俩底层都是HashMap
它存储的是K,V对,key和value。它查找起来非常快,它的内部实现是怎样的?
它的内部是一个数组,当然这个长度会动态增长,假如这个长度是16,在这个哈希表里存一个K,V对,存到什么位置?
假设K是A1,V是B1,存的话会根据A1这个对象求出它的hashcode,假如求出的hashcode是358,他会取模,
358%16=8,那么他就找到数组8的位置,存进去了。好处就在于当你要查找这个对象的时候非常方便。
下次查找,直接求A1的hashcode,然后取模,到固定的位置去找。
那么可能出现一个问题,就是2个对象的hashcode运算后并取模后的结果一样。
也就是都存在了8的位置上,那么怎么解决呢?
1、如果该位置的对象不够多,那就存一个链表。在8的位置上存为链表。也就是先找到8的位置,然后遍历链表取出。
2、如果超出链表的长度,当超过8的时候,变成红黑树,红黑树是一种平衡的二叉查找树。
这就是哈希表的内部结构。所以也就理解了hashcode是干嘛用的了,就是为了寻找对象的时候速度特别快。
hashcode的算法,要根据不同的虚拟机来定,不同的虚拟机,有不同的算法。
HotsPot(JVM)是采用xor-shift算法实现,异或+移位运算的。实现后会产生一个int类型的数。
用这个数来查找hash表中对象的位置。
equals
equals的默认实现,和==是一样的。那么调用equals比对两个对象,这两个对象指向的不是同一个对象必然是false。
通常重写equals,那么hashcode方法也要重写。
那么这个equals在什么时候用?是在hashmap里
hashmap根据key寻找value,会先根据key的hashcode取模找到对应的数组位置,
然后循环比对(equals)链表中的value。
如果重写了equals,没有重写hashcode,那么就找不到准确的位置了。
在java中,String类事重写了这两个方法的。