深入理解jvm内存模型以及gc原理

1,735 阅读7分钟

整体架构

Jvm = 类加载器 + 执行引擎 + 运行时数据区域


类加载器

作用

类加载器是将编译好的class文件加载到内存中,并进行验证、初始化等步骤,形成能被jvm直接使用的类型。

加载过程

可分解为5个步骤:加载–>连接–>初始化–>使用–>卸载。
  • 加载:把class文件以二进制字节流的形式存储到方法区中,并在堆中创建对应的class对象。
  • 连接:连接过程又分为3步,验证、准备、解析。
① 验证:验证文件格式、元数据、字节码是否符合规范。
② 准备:为成员变量分配内存并初始化值。
③ 解析:解析是虚拟机将常量池的符号引用替换为直接引用的过程。
  • 初始化:初始化过程主要包括执行构造方法,初始化静态变量、静态块。

执行引擎

其作用是将class字节码转变成机器能识别的码,然后在jvm中创建方法栈去执行方法。


运行时数据区

运行时数据区包含方法区、堆、虚拟机栈、本地方法栈、程序计数器。如下图:


方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。


 堆

简单的说就是对象的存储区,它是被所有线程共享的一块区域堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。堆回收算法使用的复制算法效率高没有碎片利用率低分为三个区 eden、S0、 S1 按照8:1:1的默认值。

 程序计数器

我们知道对于一个处理器,在一个确定的时刻都会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。

虚拟机栈

虚拟机栈指的是java方法执行的内存概念模型,每个方法执行时都会在栈内存里面创建一个栈帧,栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成,都会对应一个栈帧在虚拟机中入栈到出栈的过程。

本地方法栈

本地方法栈与虚拟机栈功能相似,是为虚拟机使用的Native方法服务。有的虚拟机可能会把这两个栈合二为一。

程序计数器

我们知道对于一个处理器,在一个确定的时刻都会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。


GC

目前比较常用的gc回收是年代回收法,JVM将堆分成了二个大区新生代、老年代和持久代。新生代和老年代的内存区域是在堆上也是gc回收的主要区域,默认情况新生代与老年代比例为1:2,该值可以通过参数–XX:NewRatio 设定。持久代是在方法区,持久代存放一些一般不需要被回收的对象,持久代一般情况不会触发GC。

新生代

新生代又分为Eden和Survivor区,而Survivor由S0和S1组成。,新生代默认分配是eden:S0:S1为8:1:1,该值可以通过参数–XX:SurvivorRatio 来设定。新生代采用的是复制回收算法,当第一次产生对象是在eden区分配空间,当eden区空间满了时候,会在S0区域分配空间,当S0空间满了时候会触发Minor GC,这时候会把eden区和S0区存活对象复制出来放在S1区,然后直接清空eden区和S0区,新生代就是这么反复的进行垃圾回收。

老年代

老年代用于存放经过多次Minor GC之后依然存活的对象,在年代回收法对象有个年龄的概念,在新生代每进行一次Minor GC仍然存活的对象年龄都会加1,当对象年龄达到一定的值就会进入老年代区域, 默认的值是15 ,可以通过参数-XX:MaxTenuringThreshold 来设定。还有一种情况是当对象特别大时候不需要达到设定值会直接进入老年代。老年代由于对象比较稳定所以老年代采用标记整理算法进行Full GC,此算法会减少内存碎片带来的效率损耗,下面会重点介绍一下本算法。

垃圾收集算法

①.(标记-清除)算法
这是最基础的算法,标记-清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。这种算法的缺点是会产生内存碎片而且效率也不高。下图是此算法的执行过程。


②.(复制)算法
为了解决(标记-清除)算法的缺陷,复制算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。 很显然,复制算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么复制算法的效率将会大大降低。我们的新生代GC算法采用的是这种算法。复制算法执行过程如下图:

③.(标记-整理)算法
因为复制算法效率低,清除算法会产生内存碎片,所以又产生了了(标记-整理)算法。该算法标记阶段和(标记-清除)一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后回收被标记的对象,此算法的好处是效率高,同时不会产生内存碎片。标记-整理算法执行过程如下图:



内存泄漏

在我们平时写代码时候很容易发生gc没有及时回收的情况,这时就会发生内存泄漏情况,下面介绍一个内存泄漏的例子。


Public class test{
Public static Map<int, Object> map = new HashMap<int, Object>();
Public void insert(){
for (int i=1;i<100; 
i++){ Object o=new Object()
map.put(i,o);
}
}
}


在这个例子中,由于map是静态的,所以gc不会回收,当执行insert方法时候,进入for循环,声明o对象,然后放进map里面,执行完此方法o对象原本可以被gc回收,但是由于map是静态的所以不会被回收,这样就会导致内存泄漏,所以我们在写代码时候一定要谨慎使用常量和静态变量这类型的变量,可能在不经意间造成内存泄漏情况。

总结


jvm以及gc都是我们写代码和设计程序时经常能涉及到的知识,深入的学习一下jvm和gc能提高对jvm调优的能力,也可以让自己写出更优雅的代码。提高自己的java水平。


Thanks!



作者简介

程东旭,民生科技有限公司,用户体验技术部Firefly移动金融开发平台Java开发工程师。