虚拟机遇到一条
new
指令时,是如何创建对象呢?
可以用一个流程图来解答
类加载
就是把
class
加载到
JVM
的运行时数据区的过程,后面会细说。
(1)检查加载:检查这个指令的参数能否在常量池定位到一个符号引用,并且检查当前类是否已经被初始化,解析,初始化过。
(2)分配内存:分配内存有两种方式:
一:是指针碰撞,我们都知道对象都是首先在Eden区分配的,如果那个区的内存空间是连续的整齐的,就会用一个指针来当作已经被分配的内存和未被分配内存的临界点,当new一个对象的时候,就在指针的后面给对象分配内存,然后指针跟着移动到已经分配的对象的后面。
二:空闲列表
如果内存空间不是整齐的,而是乱序的,就不能采用上面的分配方式了,这个时候会用到空闲列表了,就是记录一下已经用的内存和未用的内存,当new出一个新对象的时候,通过记录的数据把未使用的内存空间来给这个对象。
上面两种分配方式具体使用哪种,还要看垃圾回收期的类型,如果垃圾回收期用的是复制算法,或者是标记整理算法的话就会用到指针碰撞,如果是标记清除算法的话,就用空闲列表的方式分配内存空间。
设想一下如果碰见多个线程同时在new对象的时候,会不会造成并发的情况呢(就是A线程的对象和B线程的对象同时抢到一块内存空间),答案是很有可能会,如何解决这种问题呢,JVM为我们提供了两种来解决并发分配内存的方案.
一:就是通过加锁的方式来保证原子性,利用CAS的自旋锁的机制来保证同一时间只能一个线程来得到内存空间,不过这种最大方案的最大缺点是太消耗CPU资源了,因为自旋锁就是比较和交换,失败一直重复尝试。
二:就是线程分配缓冲区,简称TLAB
原理就是在Eden区会有一块很小的区域来创建一个TLAB区,这个区域专门来创建对象用,因为这块区域是线程私有的,所以不会分配对象内存空间的时候存在并发安全的问题,但是
底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。
(3)内存空间初始化
简单一句话就是赋予变量初始值,比如int a就给a一个初始值0,Boolean a就给一个false
(4)设置
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(
Java classes
在
Java hotspot VM
内部表示为类 元数据)、对象的哈希码、对象的 GC
分代年龄等信息。这些信息存放在对象的对象头之中。
(5)对象初始化
和上面内存空间初始化不同,
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从
Java
程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。
所以,一般来说,执行
new
指令之后会接着把对象按照程序员的意愿进行初始化
(
构造方法
)
,这样一个真正可用的对象才算完全产生出来。
对象的内存布局
在
HotSpot
虚拟机中,对象在内存中存储的布局可以分为
3
块区域:对象头(
Header
)、实例数据(
Instance Data
)和对齐填充(
Padding
)。
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(
HashCode
)、
GC
分代年龄、锁状态标志、线程持有的锁、偏向线程
ID
、偏向时间戳等。
对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个
java
数组,那么在对象头中还有一块用于记录数组长度的数据。
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于
HotSpot VM
的自动内存管理系统要求对对象的大小必须
是
8
字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全
对象的访问定位
建立对象是为了使用对象,我们的
Java
程序需要通过栈上的
reference
数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。
句柄:
如果使用句柄访问的话,那么
Java
堆中将会划分出一块内存来作为句柄池,
reference
中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类
型数据各自的具体地址信息。
使用句柄来访问的最大好处就是
reference
中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实
例数据指针,而
reference
本身不需要修改
.
直接指针:
如果使用直接指针访问,
reference
中存储的直接就是对象地址。
这两种对象访问方式各有优势, 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在
Java
中非常频
繁,因此这类开销积少成多后也是一项非常可观的执行成本。
对
Sun HotSpot
而言,它是使用直接指针访问方式进行对象访问的。
判断对象的存活
一:引用计数法(Python在用)
在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加
1
,当引用失效时,计数器减
1.
Python
在用,但主流虚拟机没有使用,因为存在对象相互引用的情况,这个时候需要引入额外的机制来处理,这样做影响效率
当两个对象互相引用的时候, 那么他们永久不会失效,那么垃圾回收器也就不会回收了
这种的对象用这种算法就不会被回收
二:可达性分析算法(java在用)
来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“
GC Roots
”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为
引用链(
Reference Chain
),当一个对象到
GC Roots
没有任何引用链相连时,则证明此对象是不可用的。
作为
GC Roots
的对象包括下面几种
强引用 (比如Objec o=new Objec());
静态变量 static修饰的;
常量
final ;
本地方法栈
Native
方法
这四种是重点,剩下的不说了,记不住
引用的几种类型:
简单来讲就四种:强>软>弱>虚 和男人的生命周期很符合啊
强引用:Objec o=new Objec(),垃圾回收的时候宁愿发生OOM都不会回收。
软引用
SoftReference 系统将要发生内存溢出(
OuyOfMemory
)之前,这些对象就会被回收
弱引用
WeakReference 只要发生GC都会回收 实际运用(
WeakHashMap
、
ThreadLocal
)
虚引用
PhantomReference 只有在检查gc能否正常工作的时候用,平常用不到,了解即可
对象的分配策略
1 对象优先分配到Eden区
2 大对象直接进入老年代 设置-XX:PretenureSizeThreshold=4m,超过4m直接进入老年代
3当对象的年龄是15的时候直接进入老年代
4当Survivor区的对象的内存大小超过Survivor区一半的时候全部都进入老年代
5空间担保:对象放入老年代的时候,会判断一下放入的大小,如果能放下就放下,放不下就发生一次老年代的垃圾回收(),再放入回收的对象
虚拟机的优化技术
逃逸分析
分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用。
如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高 JVM 的效率。
意思就是说本来是在堆上分配对象的,但是虚拟机通过逃逸分析的方法判断你这个对象不会出这个方法,也不会被其他方法引用,所以就把这个对象在栈上分配了。栈的生命周期很短,随着线程的消失而消失,这样就不会对这部分对象进行垃圾回收了,来达成优化的效果。