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


总体架构

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


类加载器

做用

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

加载过程

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

执行引擎

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


运行时数据区

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


方法区

方法区同堆同样,是全部线程共享的内存区域,为了区分堆,又被称为非堆。用于存储已被虚拟机加载的类信息、常量、静态变量,如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开发工程师。

相关文章
相关标签/搜索