Tip-关于JVM和Hotspot,你也许有这么几个容易晕的问题

1,550 阅读9分钟

1.JVM的结构到底有哪些?

       快速过一遍JVM的内存结构,JVM中的内存分为5个虚拟的区域:



▪ 你的Java程序中所分配的每一个对象都需要存储在内存里。堆是这些实例化的对象所存储的地方。是的——都怪new操作符,是它把你的Java堆都占满了的!
▪ 它由所有线程共享
▪ 当堆耗尽的时候,JVM会抛出java.lang.OutOfMemoryError 异常
▪ 堆的大小可以通过JVM选项-Xms和-Xmx来进行调整

堆被分为:

▪ Eden区 —— 新对象或者生命周期很短的对象会存储在这个区域中,这个区的大小可以通过-XX:NewSize和-XX:MaxNewSize参数来调整。新生代GC(垃圾回收器)会清理这一区域。
▪ Survivor区 —— 那些历经了Eden区的垃圾回收仍能存活下来的依旧存在引用的对象会待在这个区域。这个区的大小可以由JVM参数-XX:SurvivorRatio来进行调节。
▪ 老年代 —— 那些在历经了Eden区和Survivor区的多次GC后仍然存活下来的对象(当然了,是拜那些挥之不去的引用所赐)会存储在这个区里。这个区会由一个特殊的垃圾回收器来负责。年老代中的对象的回收是由老年代的GC(major GC)来进行的。
方法区
▪ 也被称为非堆区域(在HotSpot JVM的实现当中)
▪ 它被分为两个主要的子区域
持久代
       这个区域会 存储包括类定义,结构,字段,方法(数据及代码)以及常量在内的类相关数据。它可以通过-XX:PermSize及 -XX:MaxPermSize来进行调节。如果它的空间用完了,会导致java.lang.OutOfMemoryError: PermGen space的异常。
代码缓存
       这个缓存区域是用来存储编译后的代码。编译后的代码就是本地代码(硬件相关的),它是由JIT(Just In Time)编译器生成的,这个编译器是Oracle HotSpot JVM所特有的。
JVM栈
▪ 和Java类中的方法密切相关
▪ 它会存储局部变量以及方法调用的中间结果及返回值
▪ Java中的每个线程都有自己专属的栈,这个栈是别的线程无法访问的。
▪ 可以通过JVM选项-Xss来进行调整
本地栈
▪ 用于本地方法(非Java代码)
▪ 按线程分配
PC寄存器
▪ 特定线程的程序计数器
▪ 包含JVM正在执行的指令的地址(如果是本地方法的话它的值则未定义)

2.为什么JVM规范里面从来没有出现过永久代这个词?

        根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分:

        绝大部分 Java 程序员应该都见过 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多或者加载的包较多的情况,容易出现永久代内存溢出。因此,你会看到Java8虚拟机规范和Java7并没有什么不同,但是所有人都在告诉你:Hotspot虚拟机舍弃了永久代。

3.JDK7中的永久代会回收吗?它回收哪些东西?

       一般的垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(FullGC)。如果仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。那么永久代能回收什么呢?一,废弃的字符串常量,二,不再被引用的class对象。正确的设置永久代大小对避免FullGC是非常重要的原因。所以引起full GC的原因不光是老年代满了或者超过临界值,也有可能是永久代满了或者超过临界值。

4.永久代已经没了。那么JVM规范中的方法区HotSpot是如何实现的?

       JDK8的Hotspot使用Metaspace(元空间)代替了永久代。相比较永久代,有如下的一些改变。这个改变在如下文章中有描述:
blogs.oracle.com/poonam/entr…

       其中有如下一段话:

元空间被直接接分配在本地内存。默认的元数据空间分配只受到本地内存的限制。我们可以使用一个新的MaxMetaspaceSize选项来设置元空间占用本地内存的大小。这个选项与MaxPermSize类似。当元空间使用量MetaspaceSize设置的值(32位的client模式默认是12M,32位的server模式是16M,64位的server模式则会更多)达到了垃圾收集器会收集那些不再使用的classloader和class会被回收。所有设置一个较大的MetaspaceSize值会延迟垃圾收集发生的时间。在触发一次垃圾收集以后和下一次垃圾收集之前,元空间的使用值值会随着使用不断增加。

       如果要想知道关于元空间更多的内容,可以访问这个链接,这个链接专门讲述了关于元空间一些更多的细节。

5.Hotpsot垃圾回收的原理是什么?

       hotspot的垃圾收集的判定主要是使用可达性分析算法。在目前主流的编程语言(java,C#等)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

仅仅是了解这些是不够的。在我的博客中,有一篇JAVA内存白皮书的翻译java内存管理白皮书,非常经典。

6.被判定为垃圾地对象一定会被回收吗?

       即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
       如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)

       如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
代码示例:

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes,i am still alive:)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize mehtod executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        // 对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,i am dead:(");
        }
        // 下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        // 因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,i am dead:(");
        }
    }
}

运行结果:

finalize mehtod executed!
yes,i am still alive:)
no,i am dead:(

       SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。因为finalize()方法已经被虚拟机调用过,虚拟机都视为“没有必要执行”(即意味着直接回收)。