Edwin & Xinyu's Blog  

查无此人,查有此地

Java基础之GC那些事(一) 有更新!

最近开始准备基于docker做PaaS。考虑到未来会涉及各主流中间件,今天先简单聊聊最基本的Java GC, 权当复习。

要了解GC的本质,我们先要了解下JVM的结构。

整个JVM的架构如上图所示,主要分成3部分:
  • Class Loader Subsystem (类加载器) :主要负责动态类加载,在首次运行时进行类实例化。这里会涉及双亲委派,系统类加载等细节问题,后续有机会再进行讨论。

  • Runtime Data Area (运行区) :主要是JVM运行时的内存区,也是我们着重讨论的地方。

  • Execution Engine (执行引擎 ):主要负责class字节指令执行,也和OS原生方法相关(JNI)。

    运行区主要分为5个部分,分别为:

  • Method Area(方法区):线程共享区域。用于存放已加载的类,常量,静态变量等数据。注意:在JDK8之前hotspot使用永生代来实现方法区,取消永生代并不是取消方法区。

  • Heap Area(堆区域):线程共享区域。用于存放绝大部分对象实例及变量。注意:这里的绝大部分是由于逃逸分析等技术优化下支持栈上分配对象。其包含新生代(hotspot 进一步细分还包含Eden,From Survivor,To Survivor ),老年代(Tenured),永生代(hotspot,JDK8取消)。另外,在Eden区,存在空间极小(1%)的线程私有TLAB(Thread Local Allocate Buffer),用于存放临时小对象。使用其不需要线程同步,这就是为什么传言中Java中分配多个小对象性能要比一个大对象好。

  • VM Stack Area(VM栈区):线程私有区域。方法执行时存储局部变量,操作顺序,方法出口等,直到方法出栈。

  • Program Couter Registers(程序计数器):线程私有区域。用于存放当前执行的非native方法指令。

  • Native Method stacks(本地方法栈):线程私有区域。与VM Stack 类似。存储本地方法执行时存储局部变量,操作顺序,方法出口等,直到方法出栈。

    说了这么基础概念,那么Java new出来的新对象都是分配在堆上面的吗?

思考一下,试想Java对象在堆中分配,调用栈只保留了对象的指针。当对象生命周期结束,只能等待JVM GC来进行回收,当对象数量非常多,对象生命周期非常短暂之时,GC的压力是非常大的。**一般对Java而言,GC就意味着stop the world,大量的GC也间接影响了Java应用的性能。**

那么,如何减少分配堆内对象的数量?

接下来谈一个JVM优化的技术-逃逸分析。所谓逃逸,即判断某对象在除了自己方法引用外是否还被方法外其他变量引用。后面这种情况无法直接从该方法进行回收,即为对该方法逃逸。

如下面的例子,就是三种典型的引用逃逸。

public class EscapeDemo {

private EscapeOject obj;

public EscapeDemo(){

    this.obj = new EscapeOject();// 逃逸

}

public EscapeOject getNewObject() {

    return new EscapeOject();// 逃逸

}

public void doJob() {

    getNewObject().doJob(this);// 逃逸

}

public static class EscapeOject {

    public void doJob(EscapeDemo demo) {

    }

}

}

一旦判断对象不存在逃逸,就可以对对象进行优化:
  • 栈上分配:未逃逸对象直接分配在栈上无需入堆,最后随着线程的结束,对象随着栈一起自动销毁。

  • 同步消除:未逃逸对象不会逃逸出线程,则不会被其他线程访问到,这样该变量的线程同步操作可以擦除。

  • 标量替换:未逃逸对象不会被外部访问,如果这个对象是复合对象,则可能不再通过包装类创建该对象,而是直接在栈上创建其成员变量(子对象)。

    对象优化了,那么Java是怎么分配对象的呢?总结一下,基本就是如下流程所示:

好了,今天就到这里,下次我们再谈谈GC算法。

validate