写点什么

架构师训练营第九周作业

用户头像
_
关注
发布于: 2020 年 11 月 20 日

jvm 垃圾回收原理

结构


包含了方法区,堆空间,栈,程序计数器,本地方法栈


方法区 (method area)

方法区和堆区域一样,是各个线程共享的内存区域,它用于存储每一个类的结构信息,例如运行时常量池,成员变量和方法数据,构造函数和普通函数的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。当开发人员在程序中通过 Class 对象中的 getName、isInstance 等方法获取信息时,这些数据都来自方法区。

方法区也是全局共享的,在虚拟机启动时候创建。在一定条件下它也会被 GC。这块区域对应 Permanent Generation 持久代。 XX:PermSize 指定大小。

Java 堆 (heap area)

被所有线程共享的一块存储区域,在虚拟机启动时创建,它是 JVM 用来存储对象实例以及数组值的区域,可以认为 Java 中所有通过 new 创建的对象的内存都在此分配。

heap 组成

由于 GC 需要消耗一些资源和时间的,Java 在对对象的生命周期特征进行分析后,采用了分代的方式来进行对象的收集,即按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短 GC 对应用造成的暂停.

heap 的组成有三区域/世代:(可以理解随着时间,对象实例不断变换 heap 中的等级,有点像年级)

新生代 Young Generation

Eden Space 任何新进入运行时数据区域的实例都会存放在此

S0 Suvivor Space 存在时间较长,经过垃圾回收没有被清除的实例,就从 Eden 搬到了 S0

S1 Survivor Space 同理,存在时间更长的实例,就从 S0 搬到了 S1

旧生代 Old Generation/tenured

同理,存在时间更长的实例,对象多次回收没被清除,就从 S1 搬到了 tenured


Perm

存放运行时数据区的方法区


java 虚拟机栈(java stacks)

与 PC 一样,java 虚拟机栈也是线程私有的。每一个 JVM 线程都有自己的 java 虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

PC 程序计数器(PC registers)

一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器, NAMELY 存储每个线程下一步将执行的 JVM 指令,如该方法为 native 的,则 PC 寄存器中不存储任何信息。Java 的多线程机制离不开程序计数器,每个线程都有一个自己的 PC,以便完成不同线程上下文环境的切换。

本地方法栈(native method stack)

与虚拟机栈的作用相似,虚拟机栈为虚拟机执行执行 java 方法服务,而本地方法栈则为虚拟机使用到的本地方法服务。

运行时常量池

其空间从方法区中分配,存放的为类中固定的常量信息、方法和域的引用信息。


classloader 有两种装载 class 的方式 (加载时机)

  1. 隐式:运行过程中,碰到 new 方式生成对象时,隐式调用 classLoader 到 JVM

  2. 显式:通过 class.forname()动态加载


判断对象是否可以被回收

1.引用计数法

所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.

引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。


2.可达性算法(引用链法)

该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。

在 java 中可以作为 GC Roots 的对象有以下几种:


虚拟机栈中引用的对象

方法区类静态属性引用的对象

方法区常量池引用的对象

本地方法栈 JNI 引用的对象


堆内存分代策略以及意义


策略

Java 虚拟机将堆内存划分为新生代、老年代和永久代,永久代是 HotSpaot 虚拟机特有的概念,它采用永久代的方式来实现方法区,其他的虚拟机实现没有这一概念,而且 HotSpot 也有取消永久代的趋势,在 JDK 1.7 中 HotSpot 已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、类信息、静态变量等数据(移植到方法区),与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。


新生代(Young)

新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集-般可以回收 70% ~ 95%的空间,回收效率很高。


老年代(OldGenerationn)

在新生代中经历了多次(具体看虚拟机配置的阀值)GC 后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行 GC 的频率相对而言较低,而且回收的速度也比较慢。


永久代(PermanentGenerationn)

永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java 虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。

Jdk1.6 及之前: 有永久代, 常量池 1.6 在方法区。

Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池 1.7 在堆。

Jdk1.8 及之后: 无永久代,常量池 1.8 在元空间。而元空间是直接存在内存中,不在 java 虚拟机中的,因此元空间依赖于内存大小。当然你也可以自定义元空间大小。


意义

有了内存分代,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行 GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差, 一般不进行垃圾回收,还可以根据不同年代的特点,采用不同的垃圾收集算法。分代垃圾收集大大提升了垃圾收集效率,这些都是 JVM 分代的好处。


垃圾回收算法


1.复制算法

复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一半的内存。


2.标记清除算法

是 JVM 垃圾回收算法中最古老的一个,该算法共分成两个阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,清除未被标记的对象。该算法的缺点是需要暂停整个应用,并且在回收以后未使用的空间是不连续,即内存碎片,会影响到存储。


3.标记整理算法

此算法结合了标记-清楚算法和复制算法的优点,也分为两个阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题,按顺序排放,同时解决了复制算法所需内存空间过大的问题。


4.分代收集

分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。


a.年轻代回收算法(核心其实就是复制算法)

HotSpot 将新生代划分为三块,-块较大的 Eden 空间和两块较小的 Survivor 空间,默认比例为 8: 1: 1。划分的目的是因为 HotSpot 采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在 Eden 区分配(大对象除外,大对象直接进入老年代) ,当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC。GC 开始时,对象只会存在于 Eden 区和 From Survivor 区,To Survivor 区是空的(作为保留区域)。


GC 进行时,Eden 区中所有存活的对象都会被复制到 To Survivor 区,而在 FromSurvivor 区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到阀值(默认为 15 ,新生代中的对象每熬过一轮垃圾回收年龄值就加 1 ,GC 分代年龄存储在对象的 header 中)的对象会被移到老年代中,没有达到阀值的对象会被复制到 To Survivor 区。


接着清空 Eden 区和 From Survivor 区,新生代中存活的对象都在 To Survivor 区。接着, From Survivor 区和 To Survivor 区会交换它们的角色,也就是新的 To Survivor 区就是上次 GC 清空的 FromSurvivor 区,新的 From Survivor 区就是.上次 GC 的 To Survivor 区,总之,不管怎样都会保证 To Survivor 区在一轮 GC 后是空的(其实这就是分代收集算法中的年轻代回收算法,稍后我们会看到)。


GC 时当 To Survivor 区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。


b.老年代回收算法(回收主要以标记-整理为主)

1)在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。


2)内存比新生代也大很多(大概比例是 1:2),当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。


c. 持久代(Permanent Generation)的回收算法

用于存放静态文件,如 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。在该区内很少发生垃圾回收,但是并不代表不发生 GC,在这里进行的 GC 主要是对持久代里的常量池和对类型的卸载。


条件:

1)该类所有的实例都已经被回收,即 Java 堆中不存在该类的任何实例;

2)加载该类的 ClassLoader 已经被回收;

3)该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,此处仅仅是“可以”,而并不是和对象一样,不使用了就必然回收!


用户头像

_

关注

还未添加个人签名 2018.09.17 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营第九周作业