1.堆
万物皆对象,那么对象存放在什么地方了呢,是如何进行无效对象的处理呢?
堆就是存放对象的一块内存,几乎所有的对象实例以及数组都存放在这么一片内存中。
我们如何知道对象是否有用呢?
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,就是只有这两个对象时相互调用的,这个时候计数器也不可能是0,但是从我们角度来看这两个对象确实是无用的对象。
可达性分析
我们找到了一些基本不会成为无效对象的对象,把这些对象当做是 GC Root ,所以我们就可以从一个对象开始向上查找,如果最后发现对象链最终没有指向这些 GC Root,那么我们就认为这些对象时无用的对象,可以被回收。
那些对象可以被当做是GC Root呢
- 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中应用的对象。
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(native方法)引用的对象
那么既然知道对象的状态了,那么我们是如何处理这些对象的呢?
首先堆内存其实也是划分了几个区域:
1.首先是新生代:
- Eden区:这个区域存放的是首次进入到堆内存的对象。
- s0区:当Eden区域填满时,会发生一次Minor GC,这时失效的对象就会被回收销毁,但是依然存活的对象就会进入到s0区域。
- s1区:此区域和s0区域是相关联的,当每次Minor GC过后依然存活,会将s0区域的对象直接复制到s1同时清空Eden和s0。同时这些对象的年龄也随着Minor GC增加。
2.老年代: - tentired区:一些大的对象或者是年龄足够大的对象会进入到这个区域,这个区域最终存放的是最不容易被回收的对象。
2.方法区
1.方法区的历史:
- 1.6:运行时常量池就是在Perm Gen区(也就是方法区)中,字符串常量池就是在运行时常量池中。
- 1.7:运行时常量池依然在Perm Gen区(也就是方法区)中,但是永久代的转移工作就已经开始了,将譬如符号引用转移到了native heap;字面量转移到了java heap;类的静态变量转移到了java heap。但是运行时常量池依然还存在,只是很多内容被转移,其只存着这些被转移的引用;字符串常量池被分配到了Java堆的主要部分。也就是字符串常量池从运行时常量池分离出来了。
- 1.8:JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。同时永久代被移除,以元空间代替。元空间并不在虚拟机中,而是使用本地内存;字符串常量池存在于Java堆中。
2.方法区、元空间、永久代
很多人并不是很清楚他们三个之间的区别。
- 方法区:实际上方法区只是JVM的一个规范,就是一个逻辑的概念,是JVM 所有线程共享的、用于存储类的信息、常量池、方法数据、方法代码。
- 永久代:永久代就是对于方法区逻辑概念的一个实现,并且只有HotSpot虚拟机中有这个实现。
- 元空间:元空间就是在1.7以及之后永久代的一个转变。
3.为什么使用元空间替换永久代?
表面上看是为了避免OOM异常。
因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。
当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。因为此时的元空间已经不在虚拟机中了,而是使用的直接内存(可以理解为我们花钱买的内存条),所以就会减少内存溢出的问题。
更深层的原因还是要合并HotSpot和JRockit的代码,JRockit从来没有所谓的永久代,也不需要开发运维人员设置永久代的大小,但是运行良好。同时也不用担心运行性能问题了,在覆盖到的测试中, 程序启动和运行速度降低不超过1%,但是这点性能损失换来了更大的安全保障。
- 由于永久代内存经常不够用或者发生内存泄露,爆出异常 java.lang.OutOfMemoryError: PermGen 。
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为GC带来不必要的复杂度,而且回收效率偏低。