JVM系列1 内存结构和垃圾回收

参考:
Java内存模型:https://www.jianshu.com/p/cc640f7e919b
GC收集器:https://www.jianshu.com/p/50d5c88b272d
Java垃圾回收:https://www.jianshu.com/p/424e12b3a08f
Java垃圾回收算法和垃圾收集器:https://www.jianshu.com/p/6dd6ac40b58e

一.内存结构

1.JVM内存
JVM内存结构布局

JVM内存组成:堆内存(Heap) + 方法区(Method Area) + 栈(Thread 1..N)

  • a.堆内存 Heap(或称GC堆)
    Heap = YoungGeneration + OldGeneration
    YoungGeneration = EdenSpace + FromSpace + ToSpace
    虚拟机启动时创建,存放对象(和对象的实例变量)
    线程共享
    逻辑上连续即可,物理上可处于不连续空间
    堆中没有内存完成对象实例分配,且堆无法再扩展时,抛出OutOfMemoryError
  • b.方法区 Method Area(或称Non-Heap)
    运行时常量池 + 字段和方法数据 + 代码
    存储类信息常量静态变量(类变量)、即时编译器编译后的代码
    线程共享
    逻辑上连续即可,物理上可处于不连续空间
    可选择不实现垃圾收集
    方法区无法满足内存分配需求时,抛出OutOfMemoryError
  • c.栈
    用于方法执行:本地方法栈 + 虚拟机栈 + 程序计数器
    详情见下文
    线程私有
2.JVM内存区域控制
JVM内存区域控制参数
  • -Xms 堆的最小空间大小
  • -Xmx 堆得最大空间大小
  • -XX:NewSize 新生代最小空间大小
  • -XX:MaxNewSize 新生代最大空间大小
  • -XX:PermSize 永久代最小空间大小
  • -XX:MaxPermSize 永久代最大空间大小
  • -Xss设置每个线程的栈大小

3.JVM运行时内存

JVM运行时内存

堆和方法区线程共享,具体信息见上文;
栈 = Java栈 + 本地方法栈 + 程序计数器,线程私有

  • Java栈(JVM栈)
    生命周期与线程相同
    描述Java方法执行的内存模型:方法从调用至完成 对应 栈帧在JVM栈入栈至出栈
    栈帧用于存储 局部变量表+操作栈+动态链接+方法出口 等信息
    局部变量表 = 基本数据类型+对象引用(指向对象起始地址) +returnAddress类型(指向一条字节码地址)
    局部变量表内存空间在编译期完成分配,在方法运行期不会改变
    当 线程请求的栈深度>虚拟机允许的栈深度 抛出StackOverflowError
    当 虚拟机栈扩展无法申请足够内存时 抛出OutOfMemoryError
  • 本地方法栈(Native Method Stack)
    与JVM栈类似
    JVM栈为虚拟机执行Java方法服务,本地方法栈为虚拟机执行Native方法服务
  • 程序计数器
    记录当前线程执行的字节码行号
    分支、循环、跳转、异常处理、线程恢复依赖程序计数器实现
    执行Native方法时,程序计数器值为空(Undefined)

4.注意事项

  • a.引用、实例和对象
    Class a = new Class();
    new了一个对象,是Class类的实例
    其中a该对象的引用,对象在堆中,引用在栈中
    操作对象实际上是通过引用操作对象
    多个引用可以指向同一对象
  • b.内存回收
    方法一旦结束,栈中局部变量立即销毁
    堆中对象需要等待虚拟机启动垃圾回收时,按规则销毁
  • c.类的成员方法
    仅有一套,由该类的对象共享
    当对象调用该方法时入栈,不调用就不入栈
  • d.堆和栈
    堆可动态分配内存大小,不用确定生存期,较栈更灵活但速度
    栈的速度快于堆,但需明确大小和生存期,灵活性较差
  • native方法
    Java调用非Java代码的接口
    native方法由非Java语言实现

二、垃圾收集

1.垃圾收集内存(回收哪里?)
  • a.概念
    堆内存 + 方法区(即持久代,可选择不进行垃圾收集)
    根据对象生命周期长短,分为新生代Young、老年代Old、持久代Permanent(即方法区,可选)
    不同代采取不同的垃圾回收算法。
    JVM初始分配内存由-Xms指定,默认为物理内存的1/64且小于1G。
    JVM最大分配内存由-Xmx指定,默认为物理内存的1/4且小于1G。
    默认当空余堆内存小于40%时,JVM将增大堆直到-Xmx的最大限制。
    默认当空余堆内存大于70%时,JVM将减小堆直到-Xms的最小限制。
  • b.结构
    Heap: Young + Tenured
    Young: Eden(存放新生对象) +Survivor(=From + To)(存放每次垃圾回收后存活的对象)
    Old: 存放生命周期长的对象
    下图中 Virtual = 堆最大内存 - 堆初始内存
    随程序运行,Young/Tenured/Permanent将逐渐使用保留的Virtual空间。
堆内存
2.垃圾对象判定(回收哪些?)
  • 判断对象是否存活的算法
    1)引用计数法:给对象添加一个引用计数器,每当有一个地方引用它,计数器值加1,当引用失效,计数器值减1,任何计数器为0的对象就是不可能再被使用。
    优点:实现简单,判定效率高
    缺点:无法解决对象间循环引用问题(两个互相引用的对象再无其他引用),故Java未使用该算法
    2)可达性分析算法:以一系列名为“GC Roots”的对象作为起始点,向下搜索,搜索经过的路径称为引用链,当一个对象到GC Roots没有任何引用链连接时,即为不可达对象,可被回收
    GC Roots对象 = 虚拟机栈(栈帧中的本地变量表)中引用的对象+方法区中类静态属性引用的对象+方法区中的常量引用的对象+本地方法栈中JNI的引用对象
可达性算法
  • 引用的扩展定义
    Java传统的引用定义无法描述内存充裕情况与对象是否抛弃的关系,JDK1.2后Java扩展了引用的定义:
    1)强引用:程序代码中普遍存在,类似Object obj = new Object()这类引用。垃圾收集器永远不会回收存在强引用的对象
    2)软引用:描述还有用但并非必需的对象。软引用关联的对象在系统要发生内存溢出异常之前,会被列入回收范围进行第二次回收,如果回收软引用对象后内存依然不足,才会抛出内存溢出异常
    3)弱引用:也用来描述非必需对象,强度比软引用更弱。弱引用关联的对象在下次GC时,无论内存是否充足,都将被回收;
    4)虚引用:最弱的引用关系,是否有虚引用存在并不影响对象的生存时间,也无法通过虚引用获取对象实例。为某个对象设置虚引用仅意味在着该对象被GC时能收到一个系统通知。
  • 对象死亡的标记过程
    在可达性分析算法中不可达的对象,在真正回收前会筛选此对象是否有必要执行finalize()方法。如果对象没有覆盖finalize()方法或finalize()方法已被虚拟机调用过,将被GC回收;如果对象覆盖finalize()方法且finalize()方法未被虚拟机调用过,对象可在finalize()方法中实现自救(重新与引用链建立关联)。
    演示代码:a.对象可在GC时自救;b.自救机会仅有一次
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();
        // 因为Finalizer方法优先级很低,暂停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();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}
3.垃圾回收(如何收)
  • a.GC算法
    1)标记清除 mark-sweep: 收集器先标记活跃对象,再清除其他不活跃对象
    缺点:a.效率低;b.产生大量内存碎片,在分配较大对象时因无连续内存而提前触发GC
    2)复制 copying: 收集器把堆分为A、B空间,将空间A的活跃对象复制到空间B,再一次性回收空间A
    优点:每次均对半区内存回收,内存分配不必考虑内存碎片等情况,实现简单,运行高效
    缺点:内存缩小为原来的一半
    3)标记整理 mark-compact: 收集器先标记活跃对象,将其合并在较大内存区,再清除不活跃对象
  • b.分代收集算法
    1)新生代:复制算法。仅需通过少量存活对象的复制即完成收集
    2)老年代:标记整理算法。对象存活率高、没有额外空间进行分配担保
  • c.GC收集器类型
    1)串行: GC线程单独运行
    2)并行: 多条GC线程并行,此时用户线程未运行
    3)并发: 用户线程和GC线程同时执行
    并发收集会很短暂地暂停所有线程来标记对象,而清除过程与应用程序并发执行
    串行标记清除在老年代满之后停止应用程序开始执行
  • d.常见GC收集器


    HotSpot虚拟机的垃圾回收器
  • e.Minor GC 流程
    1)GC开始前,对象只存在Eden区和Survivor的From区
    2)GC开始,Eden区存活对象移到Survivor的To区,Survivor的From区中对象根据年龄进入To区或老年代
    3)GC完成,Eden区和From区被清空,Survivor中的From区和To区互换角色
    4)Minor GC重复上述过程,直到To区满,此时,将所有对象移入老年代
4.其他
  • a.内存泄漏内存溢出
    1)内存泄漏(Memory Leak)
    程序中一些对象无法被GC回收,始终占据内存,导致可用内存减少
    2)内存溢出(Memory Overflow)
    程序运行过程中无法申请到足够的内存。常见于老年代或永久代垃圾回收后仍无内存空间容纳新的Java对象的情况
  • b.Young设置大小对GC的影响
    Young的复制收集必须停止所有应用程序的线程,是GC主要的暂停时间瓶颈
    Young设置较大时,可以让更多的对象自行死去,不必频繁GC;一旦GC,停顿时间较长
  • c.内存申请过程
    1)JVM试图为相关Java对象在Eden初始化一块内存区域
    2)当Eden空间足够,内存申请成功。否则进行下一步
    3)JVM试图释放Eden中所有不活跃对象,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
    4)作为Eden及Old的中间交换区域,若Old区空间足够,Survivor区将区中对象移入Old区,否则保留在Survivor区
    5)当Old区空间不足时,JVM将在Old区进行完全GC
    6)完全GC后,若Survivor和Old仍然无法存放从Eden复制来的对象,从而导致JVM无法在Eden区为新对象创建内存区域,则出现out of memory错误
  • d.Minor GC & Full GC
    1)新生代GC(Minor GC):发生在新生代的GC,非常频繁速度
    2)老年代GC(Major GC / Full GC):发生在老年代的GC。一般比Minor GC10倍以上
  • e.吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
    CPU运行用户代码的时间与CPU总消耗时间的比值

三.解释执行和编译执行

1.解释执行
  • 逐句翻译源代码,每翻译一句就提交给计算机执行一句,并不会形成目标程序
  • 缺点是*运行速度慢,比如程序中存在循环语句时,循环体内的语句会被多次翻译,从而影响速度
2.编译执行
  • 先需要编译源代码,并生成目标文件,计算机再执行该目标文件
  • 编译过程较复杂(对代码进行语法分析、优化并分配内存等),但一旦形成目标文件就一劳永逸,不必重新编译,所以执行速度较快
3.Java的执行
  • JVM内嵌的解释器将由javac命令编译的字节码(.class)文件解释为最终的机器码
  • 虚拟机发现某个方法或代码块运行特别频繁时,会把这些代码认定为热点代码
  • 为了提高热点代码的运行效率,JVM会使用及时/动态编译器(JIT,Just In Time Compiler)将这些代码编译成机器码,并进行各种层次的优化
  • 所以Java结合了解释执行和编译执行

海日生残夜,江春入旧年

你可能感兴趣的:(JVM系列1 内存结构和垃圾回收)