文章目录
运行时常量池
运行时常量池是方法区的一部分。class文件含有常量池表,常量池表存放了编译期生成的字面量与符号引用,这部分内容在类加载后存放在方法区的运行时常量池
《java虚拟机规范》并没有做细节要求,具体实现会根据虚拟机的不同而不同。
对象的创建
(1). 根据new 指令定位类的符号引用,检查是否被加载过。
(2). 加载完成以后,虚拟机为新生对象分配内存,对象所需要的内存在类加载完成后可以确定。然后再堆中划分一块大小确定的内存
分配内存的方式
指针碰撞:所有被使用过的内存放在一边,空闲的内存放在另一边,中间有一个指针作为分界点的指示器,分配内存的过程就是向空闲方向挪动一段与对象所需内存大小相等的距离。
空闲列表:如果内存不是规整的,虚拟机就必须维护一张表,记录哪些内存是可用的,分配内存就是在表中找到一个足够大的内存空间,并更新表记录。
知识小贴士:选择哪种分配方式取决于java堆是都规整,Java堆是否规整又取决于所使用的垃圾收集器,就是说分配方式跟垃圾收集器有关系。
对象的内存布局:
对象在推内存中的存储布局分为三部分:对象头,对其填充,实例数据
对象头的另外一部分是类型指针(指向类型元数据),java虚拟机通过这个来确定该对象是哪个类的实例。
实列数据部分是对象真正存储的有效信息
对齐填充,HotSpot虚拟机要求对象起始地址必须是8的倍数
对象的访问定位
对象的访问是通过栈上的reference(指向对象的引用)来操作具体对象。
总的来说,通过reference这个引用来访为具体对象。然而这个reference通过什么方式去定位,访问到堆中对象的具体位置。
主流的访问方式:
-
使用句柄:java堆划出一块内存来作为句柄池,句柄池存放一个个句柄,每个句柄中包含了对象实例数据与类型数据各自的具体地址信息,reference引用中存放的是对象的句柄地址。
-
使用直接指针,reference存储的 是对象的实例数据的地址,对象实例数据存放了到对象类型的指针。
两种访问方式的各自优势:
-
使用句柄的优势,对象移动(垃圾收集时移动对象是非常普遍的行为)时,只需要改变句柄中指向对象实列的指针,不需要改变reference引用。
-
使用句柄劣势:需要两次指针定位,有一次额外的指针定位的开销。对象的访问是非常频繁的,所以这个额外的成本是非常可观的。
HotSpot主要使用直接指针的方式(有例外情况,使用特殊的收集器会有一次额外的转发)。
OutOfMemoryError异常
-
Java堆溢出
Java堆用于存储堆实例,不断创建对象实例(不会被垃圾回收),随着对象数量的增加,总容量触及容量限制就会报OOM
异常报错信息:“java.lang.OutOfMemoryError: java heap space”
-
处理java内存问题的简略策略
垃圾收集器与内存分配策略
-
为什么要关注垃圾收集与内存分配?
当排查内存溢出,当垃圾收集成为系统达到高并发量的瓶颈时,就需要实施必要的监控和调节。
-
垃圾收集器与内存分配主要关注的是java堆与方法区,这两个区域有着很显著的不确定性,只有在程序运行时才知道创建哪些对象,与创建什么对象。
不确定性:
-
一个接口的不同实现类需要的内存可能不一样
-
一个方法所执行的不同分支所需要的内存也不一样
-
判断对象死亡
-
如何判断对象死亡?
- 引用计数算法:在对象里
引用
- 引用计数算法:在对象里
JDK1.2版之后,Java对引用的概念进行扩充
- 强引用,Object object=new Object();,这就是一个强引用关系,只要强引用关系还存在,就永远不会被回收。
- 软引用,有用非必须的对象。如果被软引用关联,在OOM时,会把这些对象进行回收,如果还是没有足够的内存,再报OOM。
- 弱引用,也是描述非必须对象,但是强度比软引用弱,弱引用关联的对象只能生存到下一次垃圾收集发生。
- 虚引用,完全不会对生存时间产生影响,目的是对象被收集器回收时收到一个系统通知。
判定对象死亡(两次标记)
- 对象在可达性分析后,发现没有与GC Roots相连的引用链,将会进行第一次标记 。
- 随后进行筛选,筛选是否需要执行finalize()方法,如果finalize()方法已经被系统调用过,或没有覆盖finalize()方法,这是没有必要执行finalize()
回收方法区
判定一个类型是否属于不再使用的类
- 该类的所有实例都已经被回收
- 加载该类的类加载器已经被回收
垃圾收集算法
分代收集理论
分代收集理论建立在三条假说之上:
- 弱分代假说,决大多数对象都是朝生夕灭
- 强分代假说,熬过越多次垃圾收集过程的对象越难以被消亡
- 跨代引用假说,跨代引用相对于同代引用来说只是极少数
标记-清除算法
缺点:
- 效率不稳定
- 造成内存碎片化