学习日记-JVM(2)
栈
- 主管程序的运行,生命周期和线程同步
- 对于栈来说,不存在垃圾回收
- 8大基本类型+对象引用+实例的方法
堆(Heap)
- 一个jvm只有一个堆内存,堆内存大小是可以调节的
- 堆内存分为三个区域:新生区(伊甸园区,幸存区0区,幸村区1区),养老区(老年代),永久区(jdk1.8后改为元空间)
- 堆内存满了,会产生OOM错误
新生区
- 类诞生和成长的地方,甚至死亡
- 伊甸园,所有的对象都是在伊甸园区new出来的,满了出现一次轻量级垃圾回收
永久区
- 常驻内存,用来存放jdk自身携带的class对象,interface元数据,存储一些java运行时环境和类信息,这个区域不存在垃圾回收
- jdk1.6之前:永久代,常量池在方法区
- jdk1.7:永久代,慢慢退化,出现去永久代,常量池在堆中
- jdk1.8:无永久代,常量池在堆中
Jprofiler
- 分析dump文件,快速定位内存泄漏
- 获取堆中的数据,获得大的对象等
垃圾回收(GC)
- JVM GC只回收堆区和方法区内的基本类型数据和对象。
- 回收:对象没有引用了或者对象不可达
- 怎么判断对象是否存活?常见的有两种算法,分别是 引用计数法 和 可达性分析法
引用计数法
- 在对象里添加一个被引用的计数器,每当有地方引用了它,计数器就加1,引用失效时,计数器就减1。
- 在触发回收内存的时候,遍历所有对象,把计数器值等于0的找出来,释放掉即可。无法回收互相引用的对象
标记 - 清除算法
- 首先标记出所有存活的对象,再扫描整个空间中未被标记的对象直接回收。
- 标记的是“存活”的对象,再进行清除,只需要两个步骤即可:
- 先通过可达性分析法,通过根对象(GC Roots)顺着引用链先把这些存活对象都标出来
- 遍历这个区域所有对象,把没标记存活的对象,直接清理掉即可。
- 缺点:由于回收后没有进行整理的操作,所以会存在内存空间碎片化的问题
标记 - 复制算法(新生区)
- 常规的复制算法,是把内存分成两块大小相同的空间(1 : 1),每次只使用其中一块,当使用中的这块内存用完了,就把存活的对象移动到另一块内存中,再把使用过的这块内存空间一次性清理掉
- 标记-复制算法,在这个基础之上对其进行了优化,IBM曾有过一项针对新生代的研究,结论是绝大多数情况下,新生代区域里的对象有98%都熬不过第一次回收。所以不需要按照 1 : 1 的比例来实现复制算法,而是可以按照 8 : 1 : 1 的比例来分配内存空间,也就是一个80%的Eden空间和两个10%的Survivor空间。
- 每次分配内存,只使用Eden和其中一块Survivor空间,发生GC回收时,把Eden和其中一块Survivor空间中存活的对象,复制到另一块空闲的Survivor空间,然后直接把Eden和使用过的那块Survivor空间清理掉。
标记 - 整理算法(老年区)
- 标记-清除算法会产生内存碎片,不适合哪些需要大量连续内存空间的场景,而标记-整理算法,就是在其基础之上,增加了整理这个操作,去解决这些内存空间碎片化的问题
- 和标记-清除算法一样,先标记,但清除之前,会先进行整理,把所有存活的对象往内存空间的左边移动,然后清理掉存活对象边界以外的内存,即完成了清除的操作。标记-整理 算法是在 标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。
- 老年代里的对象存活率很高,不适合使用标记-复制的算法。而且老年代存储大对象的概率要比新生代大很多,这些大对象需要连续的内存空间来存储,标记-清除这个算法也不适合。所以大多数的老年代都采用标记-整理来作为这个区域的回收算法。