【Java】JVM和垃圾回收的基本了解

目录

 

1.JVM介绍

1.1 JVM概念

2.Java运行时数据区域

2.1 线程私有的内存区域

2.2 线程共享区域

3.常量池的补充

4.垃圾回收

4.1 如何判断对象已死

4.1.1 可达性分析算法

4.1.2 引用计数算法

4.2 编程语言类型

4.3 Java的引用类型(了解)

4.4 JVM的GC内存划分

4.4.1 虚拟机栈:

4.4.2 方法区(jdk1.7)/元空间(jdk1.8)

4.4.3 堆

4.5 垃圾回收的过程

4.6 垃圾回收算法

4.6.1 标记-清除算法(Mark-Sweep算法:最基础的收集算法)

4.6.2 复制算法(Copying算法:新生代的收集算法)

4.6.3 标记-整理算法(Mark-Compact算法,老年代收集算法)

4.6.4 分代收集算法(Generational Collection算法,主流虚拟机的实现)

4.7 垃圾收集器

4.8 JVM参数


1.JVM介绍

1.1 JVM概念

JVM:是Java虚拟机;JVM通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪,JVM是一台被定制过的现实当中不存在的计算机。

2.Java运行时数据区域

  • 线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
  • 线程共享区域:Java堆、方法区、运行时常量池

2.1 线程私有的内存区域

线程私有介绍:

由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。就把类似这类区域称为“线程私有”的内存。

(1)程序计数器:一块比较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器;

(2)Java虚拟机栈:

虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时都会创建一个栈用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈在虚拟机中入栈和出栈的过程。

之前讲的栈区域就是此处的虚拟机栈,就是虚拟机栈中的局部变量表部分;

此区域一共会产生以下两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的深度(-Xxx设置栈容量),将会抛出StackOverFlowError异常;
  • 虚拟机在动态扩展时无法申请到足够的内存,会抛出OutOfMrnoryError异常;

(3)本地方法栈:本地方法栈与虚拟机栈的作用完全一样,他两的区别是本地方法栈为虚拟机使用的Native方法服务,而虚拟机栈为JVM执行的Java方法服务;

2.2 线程共享区域

(1)Java堆:

  • 在JVM启动时,所有的对象实例以及数组都要在堆上分配;
  • 如果在堆上没有足够的内存完成实例分配并且堆也无法再扩展,将会抛出OutOfMrnoryError异常;

(2)方法区/元数据区:

用于存储已被虚拟机加载的这类信息、常量、静态变量、及时编译器编译后的代码等数据;

(3)运行时常量池:

  • 编译期及运行期间产生的常量被放在运行时常量池中;
  • 常量包括:基本类型、包装类(包装类不管理浮点型,整型只会管理-128到127)和String;
  • 类加载时,会查询字符串常量池,以保证运行时常量池所引用的字符串常量池中是一致的。

3.常量池的补充

常量池可以分为Class文件常量池、运行时常量池、字符串常量池。

  • Class文件常量池:用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放;
  • 运行时常量池:它的一个重要特征就是具备动态性,Java语言并不要求常量在编译期间才能产生,运行期间也可能将新的常量放入池中,比较多的就是String类的intern()方法;
  • 字符串常量池:存储字符串对象,或是字符串对象的引用。

4.垃圾回收

4.1 如何判断对象已死

Java堆中存放着几乎所有的对象实例,垃圾回收在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经死去,判断死去的有如下几种算法:

4.1.1 可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链,当一个对象的GC Root没有任何引用链相连(用图论表示,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

Java/C#等语言就是使用可达性算法进行垃圾回收的。

4.1.2 引用计数算法

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

但是,主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题;

Python/ActionScript等语言都是基于引用计数法;

4.2 编程语言类型

编程语言选择垃圾回收的算法,一般和语言有关。动态语言、解释型语言一般是使用引用计数算法,而静态语言、编译型语言一般是使用可达性分析算法。

动态语言:

动态语言是指在运行期间才去做数据类型检查的语言,也就是说,在用动态类型的语言编程,永远也不用给任何变量指定数据类型,该语言会在第一次赋值给变量时,在内部将数据类型记录下来。

静态语言:

静态类型语言与动态类型语言刚好相反,它的数据类型是在编译期间检查的,也就说在写程序时要声明所有变量的数据类型;

编译型语言:

编译是指在应用程序执行之前,就将程序源代码“翻译”成目标代码(机器语言)。再次运行,可直接使用上一次翻译好的机器码,不需要重新编译;

解释型语言:

  • 翻译发生在程序运行时,即边翻译边运行。再次运行时,需要重新进行翻译;
  • 解释具有良好的动态性和可移植性。将解释器一直到不同的系统上,则程序不用改动就可以在移植解释器的系统上运行;
  • 同时解释器也有很大的缺点,比如执行效率低,占用空间大,因为不仅要给用户程序分配空间,解释器本身也占用了宝贵的系统资源。

常见编程语言的类型:

【Java】JVM和垃圾回收的基本了解_第1张图片

java的语言类型:

Java属于混合型语言(M),即有编译期编译的过程,也存在运行期解释和编译的过程;

Java的编译是通过javac将java文件编译成class字节码文件,在运行时,JVM通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢很多,为了提高效率,引入了JIT技术。

JIT即时编译器的作用及时将一些热点代码直接编译成机器码,提高运行效率。

运行期通过解释器(Interpreter)和即时编译器(Just In Time Compiler,JIT编译器)配合完成解释、编译工作。

 

4.3 Java的引用类型(了解)

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。

Java将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Week Reference)、虚引用(Phantom Reference)4种,这四种强度依次逐渐减弱。

强引用:

指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用:

用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

弱引用:

    用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

虚引用:

也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

4.4 JVM的GC内存划分

4.4.1 虚拟机栈:

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

4.4.2 方法区(jdk1.7)/元空间(jdk1.8)

这个区域在GC中一般称为永久代(Permanent Generation);

永久代的垃圾回收主要回收两部分内容:废弃常量和无用的量。此区域进行垃圾收集的“性价比”一般比较低;

4.4.3 堆

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)

从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可细分为:

(1)新生代(Yong Generation):又可以分为Eden空间,From Survivor空间、To Survivor空间

  • 新生代的垃圾回收又称为Yong GC(YGC)、Minor GC;
  • 指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度非常快;

(2)老年代(Old Generation):

  • 老年代垃圾回收又称为Major GC;
  • 指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程);
  • Major GC的速度一般会比Minor GC慢10倍以上;

(3)Full GC:

  • 在不同的语义条件下,对Full GC的定义也不同,有时候指老年代的垃圾回收,有时候指全堆(新生代+老年代)的垃圾回收,还可能指由用户线程暂停(Stop-The-World)的垃圾回收(如GC日志中)。

【Java】JVM和垃圾回收的基本了解_第2张图片

4.5 垃圾回收的过程

 

垃圾回收的时机:虚拟机设置的参数+创建对象空间不足时;

【Java】JVM和垃圾回收的基本了解_第3张图片

 

  • 1.现在有一个新对象产生,那么对象一定需要内存空间,于是现在需要为该对象进行内存空间的申请;
  • 2.首先会判断Eden区是否有内存空间,如果此时有充足内存空间,则直接将新对象保存到Eden区;
  • 3.但是如果此时Eden区的内存空间不足,那么会自动执行Minor GC操作,将Eden区无用的内存空间进行处理,清理之后会继续判断Eden区空间是否充足?如果充足,则将新的对象直接在Eden区进行内存空间分配;
  • 4.如果执行Minor GC 之后的后Eden区空间仍然不足,那么这个时候会进行Survior区判断,
  • 则将Eden活跃对象保存在Survivor区,随后继续判断Eden区的空间是否足够,如果充足,则进行内存空间分配;
  • 5.如果此时Survivor区也没有内存空间了,则继续判断老年代,如果此时老年区的空间充足,则将Survivor区中的活跃对象保存到老年区,而后Survivor区应出现空余空间,随后Eden区将部分活跃对象保存在Survivor区中,最后在Eden区为新对象分配内存空间。
  • 6.如果这个时候老年代内存空间也满了,呢么这个时候将进行Major GC(Full GC)。然后再将Survivor区中活跃对象保存到老年代,从而腾出空间,然后再将Eden区的部分活跃对象保存到Survivor区,最后在Eden区为新对象分配内存空间;
  • 7.如果老年代执行Full GC之后依然空间不足,产生OOM(OutOfMemoryError)异常;

4.6 垃圾回收算法

4.6.1 标记-清除算法(Mark-Sweep算法:最基础的收集算法)

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

它的主要不足有两个:

  • 效率问题,标记和清除两个过程的效率都不高;
  • 空间问题,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多可能会导致在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次的垃圾回收动作;

【Java】JVM和垃圾回收的基本了解_第4张图片

 

4.6.2 复制算法(Copying算法:新生代的收集算法)

  • 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块;
  • 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存一次清理掉;
  • 这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效;
  • 算法的代价是将内存缩小为原来的一半,有点太高了;

【Java】JVM和垃圾回收的基本了解_第5张图片

 

4.6.3 标记-整理算法(Mark-Compact算法,老年代收集算法)

标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存;

4.6.4 分代收集算法(Generational Collection算法,主流虚拟机的实现)

  • 一般是把Java堆分为新生代和老年代;
  • 在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;
  • 而老年代中对象存活率高,没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法;

4.7 垃圾收集器

【Java】JVM和垃圾回收的基本了解_第6张图片

常用的垃圾回收器搭配方式:

(1)用户体验:ParNew + CMS

(2)性能要求高/吞吐量:parallel Scavenge + Parallel Old

根据什么条件来决定吞吐量/用户体验:

前提:运行同样的代码,设置不同的参数(垃圾回收间隔时间)——垃圾回收间隔时间越长,垃圾越多,回收越长,用户线程停顿的时间越长;

【Java】JVM和垃圾回收的基本了解_第7张图片

 

收集器有:

(1)Serial收集器(新生代收集器,串行GC)

  • 特性:单线程、复制算法、stop The World(用户线程暂停,STW)

(2)ParNew收集器(新生代收集器,并行GC)

  • 特性:多线程、复制算法、stop The World(STW)
  • 它是Serial的多线程版本,可以同时执行垃圾回收线程;
  • 搭配CMS收集器,在用户体验优先的程序中使用;

(3)Paraller Scavenge收集器(新生代收集器,并行GC)

  • 特性:多线程、复制算法、可控制的吞吐量、自适应的调节策略;
  • 应用于“吞吐量优先”的收集器;

(4)Serial Old收集器(老年代收集器,串行GC)

  • 特性:单线程、“标记-整理”算法;

(5)Paraller Old收集器(老年代收集器,并行GC)

  • 特性:多线程、“标记-整理”算法;

(6)CMS收集器(老年代收集器,并发GC)

  • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器;
  • 特性:用户体验优先(并发收集、低停顿);“标记-清除”算法
  • 执行步骤/流程:

1. 初始标记(CMS initial mark) 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快, 需要“Stop The World”。

2. 并发标记(CMS concurrent mark) 并发标记阶段就是进行GC Roots Tracing的过程。(根据gc引用链查找引用对象的过程)

3. 重新标记(CMS remark) 重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生 变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。

4. 并发清除(CMS concurrent sweep) 并发清除阶段会清除对象。

耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作(即第2、3步并发执行)

  • 缺点:

1.CPU资源敏感:用户体验优先,吞吐量越低,要求系统性能越高;

2.无法处理浮动垃圾(第四个阶段,用户线程并发执行产生的垃圾),可能出现“Concurrent Mode Failure”导致另一次Full GC,停顿时间就越长(另外的GC方案)

3.空间碎片问题:标记-清除算法导致,在survivor对象升级到老年代,创建大对象分配在老年代,可能因空间不足触发另一次full gc,停顿时间比较长;

(7)G1收集器(全区域的垃圾回收器)

  • 内存划分不同:把heap划分为很多很多的region块,然后并行的对其进行垃圾回收;
  • 算法:整体是标记-整理算法,局部是复制算法;
  • 年轻代垃圾收集:赋值算法。Eden+Survivor区复制到另一块Survivor区;

4.8 JVM参数

jvm参数,即为配置jvm虚拟机的参数,都在调用java命令开启虚拟机的时候指定。

 

【Java】JVM和垃圾回收的基本了解_第8张图片

 

比较重要参数如下:
-Xmssize
设置堆的初始化大小。如设置堆空间初始化大小为6m:-Xms6291456 或 -Xms6144k 或 -Xms6m
-Xmxsize
设置堆的最大值。如设置堆空间的最大值为80m:-Xmx83886080 或 -Xmx81920k 或 -Xmx80m
-Xmnsize
设置年轻代的大小(初始化及最大值)。如设置256m大小的年轻代:-Xmn256m 或 -Xmn262144k 或-Xmn268435456
-XX:NewSize
设置年轻代的初始化大小。
-XX:MaxNewSize
设置年轻代的最大值。
-XX:PermSize=size
设置永久代的大小(jdk1.7方法区)
-XX:MaxPermSize=size
设置永久代的最大值(jdk1.7方法区)
-XX:MetaspaceSize=size
设置永久代的大小(jdk1.8元空间)
-XX:MaxMetaspaceSize=size
设置永久代的最大值(jdk1.8元空间)
-XX:SurvivorRatio=ratio
设置Eden区与Survivor区的空间比例,默认为8,即Eden=8,Survivor From=1,Survivor To=1。
-XX:MaxTenuringThreshold=threshold
对象晋升到老年代的年龄阈值
-XX:PretenureSizeThreshold=size
在老年代分配大对象的阈值,单位只能使用byte,如3m,只能写为3145728
-XX:+PrintGCDetails
打印gc详细信息

你可能感兴趣的:(Java)