内容主要参考《深入理解Java虚拟机(第2版)》

Java和C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人想出来。

一 JVM运行时数据区

运行时数据区结构如下图:

深入解析java虚拟机hotspotpdf 深入理解java虚拟机笔记_初始化


1 程序计数器:内存较小,线程执行的字节码的行号指示器,线程私有。唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2 Java虚拟机栈:线程私有的,用于存贮局部变量表、操作数栈、动态链接、方法出口等信息。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。

3 本地方法栈:和2相似,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。

4 Java堆:内存中最大的一块,所有线程共享。作用是:存放实例对象。通过-Xms设置初始堆大小,-Xmx设置可扩展的最大堆大小。垃圾回收的主要区域。

细分:根据对象存活的周期不同将内存分为新生代和老年代。

新生代:可分为Eden空间:From Survivor空间:To Survivor空间,比例为8:1:1 ,可通过-Xmn设置新生代堆大小。新生代频繁的进行垃圾回收,回收的空间较大, 每次回收都有大批对象死去,只有少量存活。使用复制算法回收,具体过程:将Eden和From Survivor空间还存活的对象一次性的复制到另外一块To Survivor空间上,最后清理掉Eden和From Survivor空间。一般场景下,98%的对象可以回收,但不是每次回收都只有不多于10%的对象存活。所以当To Survivor空间空间不足时,需要老年代进行分配担保(参考分配策略)

老年代:对象存活率高,老年代只有进行Full GC的时候才进行回收,使用“标记-清理”或者“标记-整理”算法进行回收。

标记-清理算法:首先标出需要回收的所有对象,之后统一回收标记的对象。缺点:效率不高,内存碎片化。

标记-整理算法:首先标出需要回收的所有对象,之后让所有的对象向一端移动,然后清理掉边界以外的内存,避免了内存碎片化。

以上就包含了垃圾收集算法:标记-清理算法、复制算法、标记-整理算法、分代收集算法。

5.方法区:线程共享的区域。存贮已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。称为“永久代”。无法满足分配需求是,抛出OOM异常。

6.运行时常量池:方法区的一部分。用于存放编译器生成的各种字面量和符号引用。

永久代的垃圾回收效率比较低,主要回收两部分内容:废弃常量和无用的类。

1)废弃常量:没有引用的常量,可以被回收。

2)无用的类满足:

    类的所有实例都被回收;

    加载类的ClassLoader已经被回收;

    该类对应的java.lang.class对象没有在任何地方引用,无法再任何地方通过反射访问类的方法;

7.直接内存:不是虚拟机运行时数据区的一部分,不会受到Java堆大小的限制,但是会受到本机总内存大小以及处理器寻址空间的限制。

二 Java对象

一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况。具体可参考 :详解Java类的生命周期

对象基本上都是在jvm的堆区中创建,在创建对象之前,会触发类加载(加载、连接、初始化),当类初始化完成后,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被jvm垃圾收集器回收。对象的生命周期只是类的生命周期中使用阶段的主动引用的一种情况(即实例化类对象)。而类的整个生命周期则要比对象的生命周期长的多。

创建过程:

1)为对象分配空间等于将一块确定大小的内存从Java堆中划分出来。有两种方式:

指针碰撞:假设Java堆内存是绝对规整的,所有用过的内存放在一边,空闲的在另一边,中间有一个指针作为分界指示器,那么所谓分配内存就是将指针移动和对象大小相等的距离。

空闲列表:如果Java堆不是规整的,虚拟机需要维护一个列表,记录哪些内存块可用,在分配的时候找到足够大的空间划分给对象实例,并更新列表上的记录。

TLAB:(Thread Local Allocation Buffer):每个线程预先分配一小块内存,防止多线程分配对象时候内存冲突。

2)虚拟机将分配到的内存空间初始化为零值。

3)虚拟机对对象进行必要的设置,例如对象是哪儿个类的实例、如何找到类的元数据信息、对象的哈希值、对象的GC年代等。

4)执行<init>方法,按照程序员的意愿初始化对象,这样一个真正的对象才算完全可用。

分配策略:

1)大多数情况下,对象在新生代的Eden区中分配,Eden不够时,发起一次MinorGC。

2)大对象(大量连续内存空间的Java对象)直接进入老年代,虚拟机提供一个参数-XX:PertenureSizeThreshold,大于该阈值的西乡直接在老年代中分配。

3)长期存活的对象将进入老年代。

4)空间分配担保:在发生MinorGC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代的对象总和,如果大于,那么MinorGC可以确保是安全的。如果不成立,那虚拟机会查案HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次MinorGC,尽管有风险。否则改为进行一次Full GC。

对象回收

有两种方法判断对象是否仍在存活:

1)引用计数法:给对象添加一个引用计数器,每当一个地方引用它时,计数器值就加1,引用失效时,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。简单,但是无法解决相互循环引用的问题。

2)可达性分析算法:通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜多所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链的时候,证明对象不可用。

即是在可达性分析算法中不可大的对象,也并非“非死不可”,一个对象要真正宣告死忙,至少要经理两次标记过程:如果对象不可达,那它第一次标记并且进行一次筛选,筛选条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机掉用过,虚拟机将这两种情况都视为“没有必要执行”。

如果被判定为有必要执行finalize()方法,那么对象将会被放置在一个叫F-Queue的队列之中,之后虚拟机自动执行finalizer。