2

jvm由浅入深

 3 years ago
source link: http://cbaj.gitee.io/blog/2020/08/13/jvm%E7%94%B1%E6%B5%85%E5%85%A5%E6%B7%B1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。以上是百度对于jvm的介绍

jvm的组成

jvm主要由类加载器、运行时数据区、执行引擎,三部分组成。整体结构如下

20200813143159.png

运行时数据区主要包括:方法区、栈、堆、程序计数器,下面详细介绍下各个模块

  • 方法区是各个线程共享的内存区域,在虚拟机启动时创建

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

  • 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来

  • 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

  • 堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享
  • Java对象实例以及数组都在堆上分配

java虚拟机栈

  • 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程 的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建

  • 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧

  • 调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出

结合伪代码、进出栈图理解下栈

1
2
3
4
5
6
7
8
a(){
b();
}
b(){
c();
}
c(){
}
20200813151814.png

程序计数器

  • 程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器 执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换 后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)
  • 如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址
  • 如果正在执行的是Native方法,则这个计数器为空

本地方法栈

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。

程序运行过程

1、jvm启动后,会根据参数到指定目录下加载.class的类文件,类文件被加载到内存后,放在方法区

2、jvm创建一个主线程执行main方法,main方法中的参数方法内定义的变量被压栈到“java虚拟机栈

3、如果方法内存在实例,实例信息被存放在中,这个对象实例的引用存放在“java虚拟机栈”中

4、方法区中存放着类中可以执行的代码,而方法内的局部变量又存放在

这个比较好理解,如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元 素指向堆中的对象。

方法区指向堆

方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。

1
private static Object obj=new Object();

堆指向方法区

方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?

java对象内部布局

一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充

image-20200813194013146.png
20200813200151.png

1、大多数情况下对象分配在Eden中,如果Eden空间不足时,虚拟机会发起一次Minor Gc

2、新生代Gc(Minor Gc):在新生代发生的垃圾回收动作,因为java对象大多数有朝生夕灭的规律,所以Minor Gc发生的非常频繁,而且速度也很快

3.老年代Gc(Major Gc(清理老年代)、Full Gc( 是清理整个堆空间—包括年轻代和老年代)):Full Gc 往往会伴随着一次Minor Gc,当Minor Gc产生时会判断 a.老年代最大可用的连续空间是否大于新手代所有对象所占的空间 成立 进行Minor Gc b.a不成立 查看虚拟机参数是否允许担保失败 如果不允许Full Gc 如果允许,继续检查老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小 c.b成立 进行Minor Gc 如果不成立进行Full Gc

补充

1、大部分对象创建都是在Eden的,除了个别大对象外。

2、Minor GC开始前,to-survivor是空的,from-survivor是有对象的。

3、Minor GC后,Eden的存活对象都copy到to-survivor中,from-survivor的存活对象也复制to-survivor中。其中所有对象的年龄+1

4、from-survivor清空,成为新的to-survivor,带有对象的to-survivor变成新的from-survivor。重复回到步骤2

20200813233154.png

jvm的垃圾回收

通过可达性算法分析,通过一系列的GC root的节点作为起始节点向下查找,搜索所经过的路径被称为”引用链”,当一个对象到Gc root没经过任何引用链则证明此对象是不可用的

通过可达性算法存在的问题:逐个检查比较耗时,在可达性分析的时候必须保证一致性,停掉所有执行的java线程。即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

在java中,可作为GC Roots的对象有:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)中引用的对象;

标记-清除算法

标记-清除算法原理简单,但是效率不高、导致回收后的空间不连续,如果放入较大对象时,还需要进行另一次垃圾回收

将内存分为两块,每次只是用其中的一块,这一块快用完的时候,把存活的对象复制到另外一块,然后把一用完的空间回收掉。

标记-整理算法

和标记清理算法类似,只是后面不是直接回收,而是将存活的对象移到一端,然后进行回收边界以外的区域(老年代)

分代收集算法

一般把java堆分为新生代、老年代,新生代存活的对象较少使用复制算法,老年代使用复制算法、标记-清除算法

垃圾回收器

serial

单线程收集器,每次执行来及回收时需要停掉其他所有的工作线程,直到它收集完成。(默认的新生代回收器,简单高效)

parNew:

是serial多线程版本

Parallel Scavenge

精确的控制吞吐量(应用场景:高吞吐量为目标,减少垃圾回收的时间),有一个UserAdaptiveSizePolicy开关参数,这个参数打开后,不需要手动指定jvm各个空间的大小,会自动分析性能调节大小

serial old

是serial的老年代版本,针对老年代,采用标记整理算法

parallel old

是parallel的老年代版本,采用标记整理算法

基于标记:清除算法(产生碎片),并发收集,停顿时间短 (初始化标记:标记Gc roots直接关联到的对象
并发标记:进行Gc Roots Tracing的过程 重新标记:修正之前标记后程序运行发生变动的对象 并发清除:初始化标记和重新标记任然需要stop the world,整个过程最耗时的并发标记、并发清理都可以和用户线程同步进行,所以CMS是并发收集,低停顿,一款优秀的收集器 缺点

  • 对于cpu资源比较敏感,默认开启的垃圾收集线程是(cup个数+3)/4;如果cpu个数超过四个那么还好;

  • 无法清除浮动垃圾,由于它没有STW,所以会产生需要被回收的垃圾,被称为浮动垃圾;由于垃圾收集阶段没有停止用户线程的使用,因此cms收集器不能像其他垃圾回收器一样等到老年带快满的时候在进行垃圾回收,需要留有一定的空间给用户线程使用,默认情况下是在老年代空间到68%的时候, 触发发垃圾回收;如果预留的空间无法满足用户线程使用,会造成curent mode fail失败,这个时候虚拟机会采用后备预案,临时启用serial old对老年代重新回收,这样造成停顿时间很长

  • 基于标记清理算法实现会导致产生大量空间碎片,大对象分配的时候会触发一次fullGc,有一个UseCMSCpmpactAtFullCollection参数可以设置,在fullGc结束之后免费送一次碎片整理过程,这样会导致挺短时间较长,设计者还提供了一个参数CMSFullGcBeforeCompaction,这个参数用于执行多少次不压缩的fullGc之后,来一次压缩

整堆回收G1:并发与并行,可以充分利用多cpu,可以独立管理整个Gc堆,不需要与其他回收器组合使用。对于整个堆空间没有明显的物理界限,而是划分成多个region,G1可以建立一个可预测的停顿时间模型,跟踪各个Region里面垃圾堆积价值大小,自动维护一个优先列表,根据优先级优先回收价值最大的region,保证了再有限的时间内获得尽可能高的收集效率(化整为零的思路)。

G1化整为零的思想看着简单,但从sun实验室发表G1论文开始,近是10年才开发出G1的商用版。以一个细节为例堆分成多个region后,能否真的简单的以region为单位进行回收,很多时候会存在一个对象存在于a-region但引用关系可能存在于b-region,甚至于可以和整个堆上的任意对象发生关系,那么在回收的时候岂不是要扫描整个堆,这个问题不仅仅在G1中存在。虚拟机是使用 Remembered Set来避免全堆扫描G1每一个region对于Remembered Set,会将对象引用关系存在Remembered Set,所以可以避免全队扫描又不会遗漏。(初始标记、并发标记、最终标记、筛选回收)

20200814000058.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK