第一步:分配内存
现在知道new出来的对象是在java堆里的了,那是具体是怎么在java堆里找到一块合适的空地儿的呢
指针碰撞
假如堆里的空间是规整的,用过的内存放一边,没用过的放在另一边,两者中间有个明确的分界点,那就只需要把分界点往空的那一边挪一定距离就可以了。
空闲列表
与上面那种方式相对应的,就是堆里的内存是零散的,空闲的内存和用过的内存穿插着,那只能由虚拟机维护一个列表,记录从哪儿到哪儿的内存是空着的,例如1-10,30-50,100-105等等。
所以这两种方法用哪种就取决于堆里的内存规不规整,而这个又取决于垃圾收集器的算法是否带有压缩整理的功能。Serial,ParNew等收集器是带Compact功能的,而CMS是不带的。
不管用哪种方式分配内存,我们想象中的都是单线程的操作,如果考虑并发就会发现有问题,两个线程同时需要内存的时候,A线程正在移动指针呢B线程又来移,或者A线程正在把1-10标记为自己使用了,B线程同时做了同样的操作就很尴尬了,解决这种情况也有两种方法:
一是对分配的操作进行同步处理。
二是一的一种优化,相当于当线程创建时使用同步操作分配给线程一段空间,后面这个内存要分配对象的内存时只需要在自己的本地内存里面分配就好了,不用考虑并发的问题,当本地空间不够了,再去公共内存申请,也就是说只有从公共空间申请内存时才需要同步,提高了效率。这种方式名字叫TLAB
(ThreadLocalAllocationBuffer),可以通过-XX:+/-UseTLAB参数来设定。
第二步 初始化
将分配到的空间处理化为零值(不包括对象头),保证对象的实例字段在不赋初始值时就可以使用,程序可以访问到字段的数据类型对应的零值。例如int i
的初始值是0
第三步 设置对象头
这个对象对应的类是什么?怎么找到类的元数据信息?对象的哈希码,GC分代年龄等
第四步 执行init方法
一个对象的内存分为三块:对象头
,实例数据
,对齐填充
对象头
-
运行时数据
:哈希码,GC分代年龄,锁状态,线程持有的锁,偏向线程ID,偏向时间戳等(这些名词目前还不太了解) -
类型指针
:指向类的元数据的指针,虚拟机通过类型指针可以知道这个对象是哪个类的实例(备注:不是所有的虚拟机都是这样实现的)。加入对象是一个数组,还需要存储数组长度。
实例数据
类里面各个字段的内容喽
对齐填充
Hotspot VM要求对象的大小必须是8字节的整数倍,所以你懂得,不够的就用这块来凑
对象是在堆里的,对象的引用是在虚拟机栈里的,那具体怎么通过引用找到对象呢,也有两种办法:
通过句柄
java堆里有个句柄池
,引用存的实际是句柄的地址,句柄里存着对象在堆里的地址
和类型数据在方法区(永久代)里的地址
。这种方法有个好处是引用的地址很稳定,是句柄的地址,当对象的地址变化时(很常见,因为在垃圾收集时常常需要移动对象的地址),只需要修改句柄里的地址,而不需要修改虚拟机栈里引用的地址。直接指针访问
,名字很直接,就是引用直接存着对象在堆里的地址,类型数据的地址在对象头里,它的好处是访问对象很快,少一次指针定位。
内存相关的异常模拟
- 堆溢出:初始化一个List,While循环往list里面添加new的自己的对象就行了,OutOfMemoryError
出现这种问题要判断是内存泄漏
还是内存溢出
,泄漏表示该被GC的对象没有被清理掉,溢出表示对象都确实是程序依然会使用的,应该存活的,那就要检查程序设计的问题或者确实是堆内存太小,使用-Xmx
和-Xms
合理设置堆大小 - 栈溢出:之前的内容里说到,栈溢出会有两种异常,一种是StackOverflowError,一种是OutOfMemoryError,我个人的理解是一个线程已经申请到一定的栈空间之后,它不停地往里面放东西,最后放不下了,就会触发StackOverflowError,也是说主机内存其实还有,但是分配给栈的就这么多。但是如果是线程连一开始的栈空间都申请不到了,意思是主机内存都不够了,那就是OutOfMemoryError了。针对这两个思路,想触发StackOverflowError,就在单线程里调用一个方法不停地创建基础变量就好了,例如int,在while中不停地++。想触发OutOfMemoryError,就不停地创建线程即可,线程都要一直运行哦不然内存会被回收。
- 方法区和运行时常量池溢出:在while中使用String.valueOf(i++).intern()即可,intern是我目前知道的唯一一个在运行时把变量放到常量池的方法。否则在方法中定义的变量应该是在虚拟机栈里的,只有在加载类时能确定的变量值才会在方法区。
- 直接内存溢出:Unsafe类了解一下:
好啦,终于到要虐我千百遍的垃圾收集器了