文章目录
- 方法区
- 1.StringTable
- 2.StringTable的位置
- 3.StringTable的调优
- 垃圾回收
- 1. 判断垃圾
- 2. 5种引用
- 3. 垃圾回收算法
方法区
前面提到了方法区中的组成,它的组成主要是:
- class(例如它的属性,方法等)
- 常量池(StringTable等)
- 类加载器
在jdk 1.8中,方法区的实现是一个元空间,这个元空间是在本地内存中,同样是包括上面的3种信息,只是这时候的StringTable是在堆中的。
而在jdk 1.6中,方法区的实现是永久代,这个永久代也是包括上面3种信息的。
方法区出现内存溢出,可能就会抛出错误,如果是在jdk 1.8中,抛出的是OutOfMemoryError: Metaspace
,如果是jdk 1.6中,那么抛出的是OutOfMemoryError: PerGon space
。
1.StringTable
首先说一下StringTable所具有的特点:
- 具有延迟特性,也就是说这个字符串只有在使用的时候,如果发现这个字符串没有在StringTable中,那么就会生成对应的字符串对象,并放入到StringTable中。如果已经在StringTable中了,那么就不需要生成对象放入到StringTable中
- 字符串常量利用
+
进行拼接的时候,实现的原理首先经过编译器的优化,然后判断拼接好的字符串是否有StringTable中了,如果没有,那么就会生成对应的字符串对象,并放入到StringTable中,否则,就是从StringTable中取出的。
如下面的代码所示:
public class Demo1 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = "a" + "b";
System.out.println(s3 == s4); //true(都是串池中的字符串对象)
}
}
通过命令javap -v Demo1.class
反编译(要求要在正确的路径中),然后可以看到下面的信息:
- 字符串变量利用
+
进行拼接的时候,实现的原理是通过生成一个StringBuilder对象,调用append方法来进行字符串拼接的,最后再调用toString方法返回一个字符串对象的,而在toString方法的内部,是通过new创建的字符串对象。所以最后返回的字符串对象是在堆中的。
如下面的代码所示:
public class Demo1 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4); //false
}
}
通过javap -v Demo1.class
反编译之后,如下图所示:
我们可以尝试将上面的代码,将s3和s4的顺序调换,即先执行String s4 = s1 + s2;
,再执行String s3 = "ab"
,判断是否输出的是true还是false,如果是true,说明字符串变量拼接之后,赋值给s4的是串池中的字符串对象,否则是堆中的字符串对象。运行完毕之后,发现结果为false
,证明字符串变量拼接之后的,最后返回的是堆中的字符串对象。
- 可以尝试调用intern方法,来将字符串放入到StringTable中,然后将StringTable中的对应的串对象返回。但是在不同版本中的jdk中,实现的原理是不同的。
如果是在jdk 1.8中,字符串调用intern方法的时候,如果发现这个字符串已经在StringTable中了,那么就会直接从串池中取出对应的字符串对象返回,否则,如果不在StringTable中,那么就会将这个字符串放入到StringTable
中,然后再返回StringTable中的字符串对象。
public class Demo1 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s4 = s1 + s2;
//在jdk 1.8中,通过调用intern方法,尝试将字符串对象添加到串池中
//串池中已经有这个字符串对象对应的字符串了,那么就不会添加到串池中,否则
//就会添加到串池中,并将串池中的字符串对象返回
s4.intern();
String s3 = "ab";
System.out.println(s3 == s4); //true
}
}
在上面的代码中,最后的结果输出为true。因为先执行s4.intern()
,会尝试将s4这个字符串添加到串池中,如果串池中没有"ab"这个字符串就添加,此时的确没有,所以就将s4添加到串池中,然后执行s3 = "ab"
代码,发现串池中有"ab"这个字符串,所以直接从串池中取出,此时s3同样是指向的是s4对应的字符串对象,所以结果返回true.
如果我们将代码s4.intern()
和String s3 = "ab"
调换一下顺序,最后的结果就变成了false.这是因为s3 = "ab"
会创建对应的字符串对象保存在串池中,然后再执行s4.intern()
,发现串池中已经有"ab"这个字符串了,就不会将s4这个字符串对象添加到串池中,而s4是堆中的字符串对象,而s3是串池中的,所以就不相同,因此返回的是false。
如果是在jdk 1.6中,字符串调用intern方法的时候,如果发现这个字符串已经在StringTable中了,那么就会直接从串池中取出对应的字符串对象返回,否则如果不在StringTable中,那么就会将这个字符串复制一份,然后将复制好的对象放入到StringTable中
,再返回StringTable中的对象。
2.StringTable的位置
上面说到了,在jdk 1.6中,StringTable是保存在永久代中,而在jdk 1.8中,StringTable是保存在堆中的。我们可以通过验证最后抛出的异常,从而得知StringTable所在的位置。如果抛出的是OutOfMemoryError: ParGon space
,说明在jdk 1.6中StringTable是在永久代中,而在jdk 1.8中,抛出的是OutOfMemoryError: heap space
,那么是在堆中的。
3.StringTable的调优
由于StringTable底层是哈希表,所以只要我们将这个结构的初始容量设计的合理,从而可以让字符串更加分散,减少发生碰撞的次数。同时我们也可以通过调用interrn方法,从而避免有相同内容的字符串变量加入到StringTable中,从而造成溢出。
垃圾回收
1. 判断垃圾
要判断哪些对象是垃圾,然后回收这些对象,主要有以下几种方式:
-
引用计数法
所谓的引用计数法,就是说每一个对象都有一个引用计数器,如果这个对象被其他的对象引用了,那么引用计数器就会+1,同理,如果这个对象被其他的对象释放,那么就会将他的引用计数器的值-1.当这个对象的引用计数器的值变成了0,那么就可以认为是一个垃圾,是需要被回收的。
但是引用计数器会存在循环依赖的问题,就是说,如果A对象引用了B,而B对象也引用了A,此时两者的引用计数器的值都为1,这时候2者都没有办法被释放,所以并不建议使用引用计数器来判断哪些对象是垃圾,是需要被回收的。 -
可达性分析法
所谓的可达性分析法,就是说从GC ROOT开始出发,沿着某一个路径,可以访问到的某一些节点,那么这些路径就称为引用链。如果某一个对象和GC ROOT之间没有引用链,那么这个对象就可以认为是一个垃圾,是需要被回收的。GC Roots的对象分为以下几种:
①虚拟机栈中的引用对象,入线程调用方法堆栈的参数、局部变量、临时变量等。
②在方法区中类静态属性引用的对象。如Java类的引用类型静态变量。
③在方法区中常量引用对象,如字符串常量池的引用。
④在本地方法栈中的JNI(Native方法)引用的对象。
⑥Java虚拟机内部的引用,如基本类型对应的Class对象,一些常驻异常对象(NullPointException)等,还有系统类加载器。
⑦所有被同步锁(synchronize关键字)持有的对象。
⑧反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地缓存代码等。 -
三色标记法
所谓的三色标记法,就是说用三种颜色(灰,黑,白)来标记不同的对象。- 白色来标记垃圾对象
- 灰色表示GC ROOT可以到达的这个节点,但是这个节点内部的引用对象还没有被GC ROOT扫描过
- 黑色表示GC ROOT可以到达这个节点,并且内部的引用对象已经被GC ROOT扫描过了
但是使用三色标记法会存在多标和漏标的情况,例如下面的图示:
出现多标的情况:在将D引用的所有内部节点都扫描了之后,这时候D节点就被标记为黑色,而它的内部引用E就标记为灰色,此时如果将D中的内部引用E设置为null,那么本来应该是要回收E这个节点的,但是由于我们已经将E标记为灰色,那么就不会回收E,同时会去扫描E中的内部引用,此时就会将F,G标记为灰色,然后E就标记为黑色,那么多标了E,F,G这3个对象(本应该被回收,但是实际没有被回收的对象就称为浮动垃圾)
出现漏标的情况:在已经将中内部的所有引用对象都扫描了之后,此时会将D标记为黑色,之后即使重新回到D这个节点,由于标记成了黑色,所以不会再扫描其内部引用了,所以这时候我们在添加D的内部引用G,此时G就不会被标记了,从而出现了漏标的情况。
2. 5种引用
JVM中,主要有5种引用:
-
强引用: 我们平常使用的大多是强引用对象,例如
Object obj = new Object()
,这就是一个强引用。当所有的GC ROOT都没有强引用这个对象的时候,那么这个对象就会被回收。 -
软引用:这里主要是利用SoftReference引用对象,如下所示:
软引用的特点是:如果弱引用指向的对象已经没有被GC ROOT强引用了,那么在进行垃圾回收的时候,如果发现内存依旧是充足的,那么不会回收这个软引用对象,但是如果内存不足,就会将这个软引用对象回收。
同时软引用还可以配合引用队列来使用,因为我们将软引用对象A1回收之后,SoftReference这个软引用依旧是占用着内存,所以将其放入到引用队列中,然后通过遍历这个引用队列,之后将这个SoftReference引用所占用的内存释放。 -
弱引用:通过WeakReference引用指向对象,其实和软引用差不多,只是弱引用在进行垃圾回收的时候,不管内存是否充足,都会将这个弱引用对象回收,而软引用则是只有在内存不足的时候,才会将这个软引用对象回收。
同样的,弱引用也可以配合使用引用队列,通过使用引用队列,来将弱引用所占用的内存释放。
至于哪里使用到了弱引用,我们可以来看一下ThreadLocal
中的静态内部类ThreadLocalMap
,这个ThreadLocalMap继承了WeakReference
,然后在构造ThreadLocalMap的时候,会用ThreadLocal对象作为参数传入,然后就会调用super(threadLocal)
,从而使得ThreadLocal作为ThreadLocalMap的key,并且这个key是一个弱引用。 -
虚引用:需要配合引用队列使用的。我们将这个虚引用添加到引用队列中,然后ReferenceHandler线程会遍历整个引用队列,发现存在虚引用,就会调用相应的方法,在方法的内部通过
Unsafe
这个类调用freeMemory方法来释放内存。例如直接内存,就是通过虚引用对象Cleaner调用clean方法来释放内存的,而clean方法的内部,是通过Unsafe
调用freeMemory方法释放内存。 -
终结器引用:同样需要配合引用队列使用的。第一次GC的时候,并没有将对应的对象回收,而是将这个终结器引用添加到引用队列中,然后有一个优先级较低的Finalize线程遍历这个引用队列,然后发现存在终结器引用,就会找到这个终结器引用所指向的对象,通过这个对象调用
finalize
来释放内存,第二次GC才会将这个对象释放。
3. 垃圾回收算法
常见的垃圾回收算法主要有以下几种:
-
标记-清除法
所谓的标记清除法,就是说,第一次将从GC ROOT开始搜索,然后将活着的对象做个标记,第二次再从GC ROOT开始往下搜索,将没有标记的对象回收。
这样的好处是效率高,只需要将没有标记的对象回收,但是缺点就会导致出现大量的内存碎片。 -
复制法
所谓的标记复制法,就是将整个内存一分为二,然后每次添加对象的时候,只是添加到同一个内存块中,另一个内存块一直是空着的,当发现使用的内存块不足的时候,就会将活着的对象复制到另一个空着的内存中,然后当前使用的这一块内存全部清空。然后交换这2个内存块的指向。如下所示:
复制法的好处就是不会产生内存碎片,但是它的内存利用率低,因为只能使用的一半的内存,同时需要涉及到对象的复制。 -
标记-整理法
所谓的标记整理法,就是第一次从GC ROOT开始往下搜索,将活着的对象往内存的另一侧压缩,然后将最后一个压缩对象后面的内存清空即可。如下所示:
使用标记整理法好处就是不会像标记清除法那样产生大量的内存碎片,但是由于需要将对象压倒另一侧,此时就会涉及到对象的地址发生改变等问题,所以效率较低。
所以在jvm中,采用的是分代回收来进行垃圾回收:
所谓的分代,就是说,将整个内存区域划分为新生代和老年代。而在新生代中,还可以划分为Eden, from, to(比例默认是8:1:1)。对于不同的年代,采用的回收算法是不同的,新生代采用的是复制法,而对于老年代则采用的是标记清除或者标记整理算法。
新生代采用的是复制法,首先会尝试将对象添加到Eden中,但是如果发现Eden的内存不足,那么就会触发Minor GC,此时就会将Eden中活着的对象复制到from中,并且将这些活着的对象的寿命 + 1,然后将Eden中的内存清空,再将对象添加到Eden中。重复上面的操作,如果发现from的内存区域也不足,那么同样会将from中活着的对象复制到to区域,然后调整from,to的指向,从而让to保证是空的。同样的,当活着的对象从from复制到to的时候,也需要将活着的对象寿命 + 1.
之所以要将活着的对象寿命 + 1,是因为一旦它的寿命超过了某一个阈值,那么就会将这个对象晋升为老年代,需要将其放到老年代中。
值得一提的是,Minor GC会引起STW(stop the world),也即是进行垃圾回收的时候,会暂定其他所有线程继续运行,直到垃圾回收结束之后,才能够继续运行。因为新生代垃圾回收算法采用的是复制法,那么就会涉及到对象的地址变化问题,此时如果没有STW,那么就可能引起数据的混乱(线程在使用对象A,但是垃圾回收,需要将对象A复制到from中,垃圾回收之后,此时地址的指向不同了,就造成数据的混乱)。
同时,如果要添加的对象占用的内存太大了,新生代中无法分配内存给这个对象,那么这个对象直接晋升为老年代,也即直接分配到了老年代这个区域中。
老年代则采用的是标记清除法或者标记整理法。一旦发现老年代的内存也不足了,那么首先尝试进行一次Minor GC,进行之后,内存还是不足,就会进行Full GC操作。Full GC操作同样会引起STW的。