JVM划重点:引用类型、垃圾回收算法和内存划分

一、Java四种引用类型

   每种编程语言都有操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”(Reference)。在 Java 中一切都被视作对象,但是我们操作的标识符实际上是对象的一个引用。

   Java 将引用分为【强软若虚】四类:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)等,这 4 种引用的强度依次减弱。

强引用:不会被回收

   用的最广。我们平时写代码时,new一个Object存放在堆内存,然后用一个引用指向它,这就是强引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

软引用:仅在内存不足时,随时有可能被回收

   如果一个对象只具有软引用,则内存空间足够时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。(备注:如果内存不足,随时有可能被回收)。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

弱引用:随时有可能被回收

   弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。

   每次执行GC的时候,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

虚引用:随时有可能被回收

   “虚引用”顾名思义,就是形同虚设,与其它几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收

   虚引用主要用来跟踪对象被垃圾回收器回收的活动。

二、Java中的内存划分

  Java程序在运行时,需要在内存中分配空间。为了提高运算效率,就对数据进行了不同空间的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。

1、程序计数器(线程私有)

  每个线程拥有一个程序计数器,在线程创建时创建,指向下一条指令的地址, 执行本地方法时,其值为undefined。

2、虚拟机栈(线程私有)

  每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。局部变量表存放的是:编译期可知的基本数据类型、对象引用类型。

  每个方法被调用直到执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

  在Java虚拟机规范中,对这个区域规定了两种异常情况:

(1)如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)

(2)虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现OOM

3、本地方法栈

(1)本地方法栈与java虚拟机栈作用非常类似,其区别是:虚拟机栈是为虚拟机执行java方法服务的,而本地方法栈则为虚拟机使用到的Native方法服务。

(2)Java虚拟机没有对本地方法栈的使用和数据结构做强制规定,Sun HotSpot虚拟机就把java虚拟机栈和本地方法栈合二为一。

(3)本地方法栈也会抛出StackOverFlowError和OutOfMemoryError。

4、堆内存(线程共享)

(1)堆是java虚拟机所管理的内存区域中最大的一块,java堆是被所有线程共享的内存区域,在java虚拟机启动时创建,堆内存的唯一目的就是存放对象实例,几乎所有的对象实例都在堆内存分配。

(2)堆是GC管理的主要区域,从垃圾回收的角度看,由于现在的垃圾收集器都是采用的分代收集算法,因此java堆还可以初步细分为新生代和老年代。

(3)Java虚拟机规定,堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。在实现上既可以是固定的,也可以是可动态扩展的。如果在堆内存没有完成实例分配,并且堆大小也无法扩展,就会抛出OutOfMemoryError异常。

5、方法区(线程共享)

(1)用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据。

(2)Sun HotSpot虚拟机把方法区叫做永久代(Permanent Generation),方法区中最终要的部分是运行时常量池。

三、Java对象在内存中的状态

  可达的/可触及的:Java对象被创建后,如果被一个或多个变量引用,那就是可达的;即从根节点可以触及到这个对象。其实就是从根节点扫描,只要这个对象在引用链中,那就是可触及的。

  可恢复的:Java对象不再被任何变量引用就进入了可恢复状态。在回收该对象之前,该对象的finalize()方法进行资源清理。如果在finalize()方法中重新让变量引用该对象,则该对象再次变为可达状态,否则该对象进入不可达状态

  不可达的:Java对象不被任何变量引用,且系统在调用对象的finalize()方法后依然没有使该对象变成可达状态(该对象依然没有被变量引用),那么该对象将变成不可达状态。

  当Java对象处于不可达状态时,系统才会真正回收该对象所占有的资源。

四、判断对象死亡的两种常用算法

1、引用计数算法

  给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

  但是,主流的java虚拟机并没有选用引用计数算法来管理内存,其中最主要的原因是:它很难解决对象之间相互循环引用的问题。

2、根搜索算法(jvm采用的算法)

  假设有若干个根对象,当任何一个根对象(GC Root)到某一个对象均不可达时,则认为这个对象是可以被回收的。

概念

  这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。

  那么问题又来了,如何选取GC Roots对象呢?在Java语言中,可以作为GC Roots的对象包括下面几种:
(1) 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2) 方法区中的类静态属性引用的对象(一般指被static修饰的对象,加载类的时候就加载到内存中)。
(3) 方法区中常量引用的对象。
(4) 本地方法栈中JNI(Native方法)引用的对象。

案例分析

  如下图所示,为一个GC Roots的引用链的例子:


GC Roots引用链

  举例分析,看下面的图:


GC Roots引用链示例

  根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

  从上图,reference1、reference2、reference3都是GC Roots,可以看出:

  • reference1-> 对象实例1;
  • reference2-> 对象实例2;
  • reference3-> 对象实例4;
  • reference3-> 对象实例4 -> 对象实例6;

可以得出对象实例1、2、4、6都具有GC Roots可达性,也就是存活对象,不能被GC回收的对象。而对于对象实例3、5直接虽然连通,但并没有任何一个GC Roots与之相连,这便是GC Roots不可达的对象,这就是GC需要回收的垃圾对象。

五、垃圾回收算法

1、标记-清除算法

  标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;

  清除阶段:清除所有未被标记的对象。

  缺点:会产生内存碎片。

2、复制算法(新生代的GC)

  将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,然后清除正在使用的内存块中的所有对象。

3、标记-整理算法(老年代的GC)

  标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。

  整理阶段:将所有的存活对象压缩到内存的一端,之后,清理边界外所有的空间。

4、分代收集算法

  存活率低:少量对象存活,适合复制算法。在新生代中,每次GC时都发现有大批对象死去,只有少量存活(新生代中98%的对象都是“朝生夕死”),那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。

  存活率高:大量对象存活,适合用标记-清理/标记-整理。在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法进行GC。

六、Java虚拟机之垃圾回收器

  上面有7类垃圾回收器,分为两块,上面为新生代(Young generation)回收器,下面是老年代(Tenured generation)回收器。如果两个回收器之间存在连线,就说明它们可以搭配使用。

  • GC主要分二类:新生代GC和老年代GC;
  • 新生代GC包括:串行GC、并行GC、并行回收GC;
  • 老年代GC包括:老年代串行GC、老年代并行GC、CMS;
  • G1比较特殊,同时支持新生代和老年代。
分代收集理论

  分代收集(Generationl Collection)建立在三个分代假说之上:

  弱分代假说(Weak Generationl Hypothesis):绝大多数对象都是朝生夕灭的。

  强分代假说(Strong Generationl Htpothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

  跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

Java Minor GC、Major GC和Full GC之间的区别?

  Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。

  Major GC清理Tenured区,用于回收老年代,出现Major GC通常会出现至少一次Minor GC。

  Full GC是针对整个新生代、老生代、元空间(metaspace,自jdk 8取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

1、Serial回收器(串行GC)

  它是一个新生代回收器,单线程执行,使用复制算法。它在进行垃圾回收时,必须暂停其它所有的工作线程(用户线程)。是JVM client模式下默认的新生代回收器。对于限定单个CPU的环境来说,Serial回收器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程回收效率。温馨提示,它进行垃圾收集时,必须暂停其它所有的工作线程(Stop-The-World:将用户正常工作的线程全部暂停掉),直到它收集结束。

2、ParNew回收器(并行GC)

  并行GC是Serial回收器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial回收器一样。

  它是运行在server模式下的首选新生代回收器,除了串行GC外,目前只有它能与CMS回收器配合工作。CMS回收器是一个被认为具有划时代意义的并发回收器,因此如果有一个垃圾回收器能和它一起搭配使用让其更加完美,那这个回收器必然也是一个不可或缺的部分了。

3、Parallel Scavenge回收器(并行回收GC)

  scavenge [ˈskævɪndʒ] 清除污物,打扫。并行GC的plus版,更加关注吞吐量。目标是达到一个可控制吞吐量的回收器。

  并行回收GC也是一个新生代回收器,它也是使用复制算法的回收器,又是并行多线程回收器。

  停顿时间和吞吐量不可能同时调优。我们一方面希望停顿时间少,另外一方面希望吞吐量高,其实这是矛盾的,鱼与熊掌不可兼得。因为:在GC的时候,垃圾回收的工作总量是不变的,如果将停顿时间减少,那频率就会提高;既然频率提高了,说明就会频繁的进行GC,那吞吐量就会减少,性能就会降低。

  吞吐量:CPU用于用户代码的时间/CPU总消耗时间的比值,即=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)。比如,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

  jdk8中,JVM使用的默认的垃圾回收器是UseParallelGC,即新生代并行回收GC和老年代(Ps MarkSweep)组合。

4、Serial Old回收器(老年代串行GC)

  Serial Old是Serial回收器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

5、Parallel Old回收器(老年代并行GC)

  老年代并行GC是并行GC的老年代版本,使用多线程和标记-整理算法。

6、G1回收器

  G1(Garbage First)回收器是当今回收器发展的最前沿成果之一,自JDK 7提供的一个新回收器,基于“标记-整理”算法实现,故不会产生内存碎片。还有一个特点之前的回收器进行收集的范围都是整个新生代或老年代,而G1将收集整个Java堆(包括新生代,老年代)。

  充分利用多CPU、多核环境下的硬件优势来缩短垃圾收集的停顿时间。G1 回收器是针对性地对Java堆内存区域进行垃圾收集,而非每次都对整个 Java 堆内存区域进行垃圾收集。

  使用的垃圾收集算法:

  • 对于新生代:复制算法;
  • 对于老年代:标记 - 整理算法。
7、CMS回收器(并发GC)

  CMS回收器(Concurrent Mark Sweep:并发标记清除)基于标记-清除算法,是一种以获取最短回收停顿时间为目标的回收器。适合应用在互联网或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。

  CMS回收器的优点:并发收集、低停顿,但是CMS还远远达不到完美。

  三个显著缺点:

  • CMS回收器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
  • CMS回收器无法处理浮动垃圾。
  • 会产生大量碎片。

七、Java堆内存划分

  Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。

  在 Java 中,堆被划分成两个不同的区域:年轻代和老年代。年轻代又被划分为三个区域:Eden、From Survivor、To Survivor。 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

1.年轻代

  年轻代用来存放新近创建的对象,尺寸随堆大小的增大和减小而相应的变化,默认值是保持为堆大小的1/15,可以通过 -Xmn 参数设置年轻代为固定大小,也可以通过 -XX:NewRatio 来设置年轻代与年老代的大小比例,年青代的特点是对象更新速度快,在短时间内产生大量的“死亡对象”。

  年轻代的特点是产生大量的死亡对象,并且要是产生连续可用的空间,所以使用复制清除算法和并行收集器进行垃圾回收。对年轻代的垃圾回收称作初级回收 (minor gc)。

2.老年代

  Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。

  现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉”了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。 另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

3.永久代

  永久代是Hotspot虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西。在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间。

  永久代或者“Perm Gen”包含了JVM需要的应用元数据,这些元数据描述了在应用里使用的类和方法。注意,永久代不是Java堆内存的一部分。永久代存放JVM运行时使用的类。永久代同样包含了Java SE库的类和方法。永久代的对象在full GC时进行垃圾收集。

八、类加载机制

  虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

Reference

posted @ 2021-08-29 16:05  楼兰胡杨  阅读(183)  评论(0编辑  收藏  举报