JVM你知道多少?

前言

博主在寒假的时间,学车的空余时间又学了学周志明老师的深入理解Java虚拟机,结合之前的写下了这篇笔记,希望和大家一块学习,同时巩固自己记忆!

JVM内存模型

众所周知,jvm分五个模块:
虚拟机栈、本地方法栈、程序计数器、方法区、和堆。
其中五大内存模块可分为两种:线程共享和线程私有
线程共享: 方法区、堆
线程私有: 虚拟机栈、程序计数器、本地方法栈
图示:
在这里插入图片描述

1. 程序计数器(记录当前线程)

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前 线程的行号指示器(类似于CPU里的PC)
对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一 条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位 置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影 响,独立存储。 注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地 址。如果为native【底层方法】,那么计数器为空。而且这块内存区域是虚拟机规范 中唯一没有OutOfMemoryError的区域。

2. Java栈(虚拟机栈)

同计数器也为线程私有,生命周期与线程相同,就是我们平时说的栈,描述的是 Java方法执行的内存模型。 每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链 接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从 入栈到出栈的过程。虚拟机栈中的局部变量表中还存放了编译期的可知的八个基本数据类型,这里也是一个小的考点哦:
整型: byte,short,int,long 浮点型: float,double 字符型:char boolean型:boolean
以及对象的引用和returnAddress类型
在这里插入图片描述
这里要再提一点就是:对象访问的方式

  1. 指针直接访问实例数据
    在这种方式中,JVM栈中的栈帧中的本地变量表中所存储的引用地址就是实例数据的地址。通过这个引用就能直接获取到实例数据的地址。
    除此之外,其实引用所指向的对内存中的对象数据有两部分组成,一部分就是这个对象实例本身,另一部分是对象类型在方法区中的地址。
  2. 使用句柄间接访问实例数据
    JVM会在堆中划分一块内存来作为句柄池,JVM栈中的栈帧中的本地变量表中所存储的引用地址**是这个对象所对应的句柄地址,而非对象本身的地址。**句柄池中的一个个对象地址有两部分组成,一部分就是对象数据在堆内存中实例池中的地址,另一部分就是对象类型在方法区中的地址。

在这里插入图片描述

3. 本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方 法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,这里个人理解的是:前者调用外部的代码,本地方法栈调用的是JDK自带的代码。本地方法栈可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

4. 堆

周老师在这里皮了一下,说这个是 “垃圾堆” 这个比喻很形象,所谓垃圾堆即指GC频繁发生的区域,即对象多的地方。在这个区域所有线程是共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆(“垃圾堆”),又由于现在收集器常使用分代算法,Java堆中还可以 细分为新生代和老年代,再细致点还有Eden(伊甸园)空间等等,如下图所示。 根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要 逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。 当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误 (OutOfMemoryError)
图示:
在这里插入图片描述

5.方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。 用于存储已被虚拟机加载的版本、字段、方法、接口和常量池(存储字面量和符号引用)、常量、静态变量,如 static 修饰的变量加载类的时候就被加载到方法区中。这里就解释了之前的博客里面的

String name=new String("HXZ");

强引用类型。
为什么就创建一或者两个对象了:
1 个的情况: 如果字符串池中已经存在了"HXZ"这个对象,那么直接在创建一个对象放入堆中,返回 name 引用。
2 个的情况: 如果字符串池中未找到"HXZ"这个对象,那么分别在堆中和字符串池中创建一个对象,但是如果遇到了new关键字,则还是会在内存(不是字符串池)中创建一个对象,然后将对象返回给引用name。字符串池中的比较都是采用equals方法。String的equals的方法是重写了Object超类13个方法中的equals的方法故能进行字符串内容的比较,重写equals方法必须重写hashcode方法(why?比如我去酒店,登记的前台好比哈希算法,名字是对比好比 equals 方法,身份证号的对比好比 hashcode 方法只有equals 和 hashcode 都满足的时候才能确保是同一个对象)。
这里还是强调一点,就是在jdk1.8及以后方法区的字符串池被挪到了堆里,不在方法区了,方法区的永久代也被元空间所取代。

6.直接内存

直接内存并不是虚拟机规范定义的数据区的一部分,也不是虚拟机运行时数据区 的一部分。但是这部分内存也被频繁的使用,而且也可能导致 OutOfMemoryerror。
调用native方法时,如要操作native方法的数据时,需要来回不停地复制,而直 接内存可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆 中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场合中 显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

JVM垃圾回收

前面讲到堆是Java虚拟机是虚拟机所管理的内存里面最大的一块,所有的对象几乎都在这里分配内存。《深入理解Java虚拟机》这本书里还提到了随着时间的推移,这种情况也在随之改变:JIT编译器 的发展和
针对不同区域JVM采用了不同的GC,不同的GC是通过不同的算法实现的。在JDK8中,按照回收区域的不 同,把GC分为工作在新生代的普通GC(minorGC)和工作在堆全局空间的全局GC(Full GC)。 新生代和老年代占比空间为1:2,且采用了不同的算法,所以minor GC 的速度要比Full GC快很多。

在这里插入图片描述

垃圾判断标准
1. 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象
之间相互循环引用的问题。尽管该算法执行效率很高。

2. 可达性分析算法(根索法)

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
在这里插入图片描述

圾回收算法
1. 标记清除

先假设堆中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后堆中没有打标记的对象都是可以被回收的。这种算法虽然不需要多余的内存空间 “周转” 对象,但是会导致内存碎片化,如图所示:
在这里插入图片描述

2. 复制算法

HotSpot 把新生代分为三个部分:Eden区和两个Survivor区(From区和To区),(上方有图,这里不放了)默认比例8:1:1。对象 创建时会被放在Eden区,当Eden区触发GC(minor GC),GC会对Eden和Survivor区进行垃圾回收,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。而幸存下来的对象会被 “复制” 到Survivor1区(To区),然后清空Eden和From区,最后将To和From 交换,让刚才被清空的From作新的To区,让刚才保存对象的To区作新的From区,以保证下一次GC可以扫 描到这些对象。这个过程中涉及到了一个 “复制” 的操作,就是 “复制算法” 的产物。顺带一提:当一个对 象在多次GC后依然无法被回收,在From区和To区来回复制,每复制一次“年龄”加1,一旦“年龄”达到 MaxTenuringThreshold的值(阈值,默认为15)就会被移动到老年代。
如图所示:
在这里插入图片描述
“复制算法” 的优缺点:
优点:1、由于“复制算法”采用了复制—清空的方法,所以不会导致内存空间的碎片化。
缺点:1、由于复制算法需要另外的空间来 “周转” 这些幸存的对象,所以内存消耗比较大。2、如果存在“极端情况”,比如大量的对象循环引用而导致无法回收的幸存对象占比很大,假设 为80%,那么就需要将这些数量庞大的对象都复制遍,并将所有的引用地址重置一遍,这回耗费比较多 的时间。所以复制算法的最佳工作环境就是这一块的对象存活率比较低。

3. 标记整理

标记整理其实就是在标记清除后加了一个 “整理” 操作,将分散的数据整理到一块连续的内存空间。就是慢,但慢工出细活。
在这里插入图片描述
优点:减少了地址空间的占用。
缺点:时间效率问题。

4.分代回收

一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清除”或者“标记—整理”算法来进行回收

GC垃圾回收器

如图所示:
在这里插入图片描述
按照新生代和老年代的划分,可将GC回收器分成两部分,

1. Serial 垃圾收集器(单线程、复制算法) (新生代)

Serial(英文连续)是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1之前新生代唯一的垃圾 收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工 作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限 定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial 垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。

2. ParNew 垃圾收集器(Serial+多线程) (新生代)

ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃 圾收集之外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也 要暂停所有其他的工作线程。
ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限 制垃圾收集器的线程数。【Parallel:平行的】 ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多java 虚拟机运行在Server模式下新生代的默认垃圾收集器。

3. Parallel Scavenge 收集器(多线程复制算法、高效) (新生代)

Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃 圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码 的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)), 高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而 不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个 重要区别。

4. Serial Old 收集器(单线程标记整理算法 ) (老年代)

Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法, 这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器。 在Server模式下,主要有两个用途:

  1. 在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。
  2. 作为年老代中使用CMS收集器的后备垃圾收集方案。 新生代Serial与年老代Serial Old搭配垃圾收集过程图:
    在这里插入图片描述
5. Parallel Old 收集器(多线程标记整理算法)(老年代)

Parallel Old收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在JDK1.6 才开始提供。 在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只 能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞 吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge 和年老代Parallel Old收集器的搭配策略。 新生代Parallel Scavenge和年老代Parallel Old收集器搭配运行过程图:
在这里插入图片描述

6. CMS 收集器(多线程标记清除算法) (老年代)

CMS收集器:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。

7. G1垃圾回收器

G1垃圾收集器也是以关注延迟为目标、服务器端应用的垃圾收集器,被HotSpot团队寄予取代CMS的使命,也是一个非常具有调优潜力的垃圾收集器。虽然G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制,但单纯地以类似前三种的过程描述显得并不是很妥当。事实上,G1收集与以上收集器有很大不同:

G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);
G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;
G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

补充

1. 目前web应用中的垃圾收集器
  • 老年代CMs收集器(新生代使用ParNew)
    优点:并发收集、低停顿
    缺点:产生大量空间碎片、并发阶段会降低吞吐量

  • G1收集器(新生代和老年代均使用)

  • GC还是原来的CMS,并非G1.
    如果需要使用G1还是需要添加参数 -XX:+UseG1GC

2. 吞吐优先与响应优先

web项目和大数据运算类项目的区别。

  • 响应时间优先的并发收集器
    如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
    例如:XX:+UseConcMarkSweepGC -XX:+UseParNewGC(Parnew+CMS)
  • 吞吐量优先的并行收集器
    如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和纯后台处理等。例如:-XX:+UseParallelGC -XX:+UseParallelOldGC

并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。

3. Minor GC 和 Full GC

新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。

老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的。

要尽量避免Full GC,很影响性能。

4. Full Gc触发条件

下边这些情况可能会触发JVM进行Full GC:

  • System.gc()方法的调用:此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,

  • 老年代代空间不足

  • 永生区空间不足:VM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区。Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。方法区是逻辑上的,在HotSpot虚拟机上物理上属于堆。

  • CMS GC时出现promotion failed和concurrent mode failure。
    promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在
    执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。

  • 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间
    这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之
    前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。

  • 堆中分配很大的对象:所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。

  • Jvm crash 或者OutOfMemory应该如何解决?

  • 多线程并发编程中,线程锁定挂起该如何解决?
    jstack,jConsole

  • 要尽量避免full gc

  • 如何设置垃圾回收器?
    某个范例如下:
    java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0 -XX:+UseG1GC
    -XX:NewRatio=4 :设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
    -XX:SurvivorRatio=4 :设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
    -XX:MaxPermSize=16m: 设置持久代大小为16m。
    -XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不 经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行 多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
    -XX:+UseG1GC:使用G1垃圾回收器

5. 对象内存布局

在hotSpot虚拟机中,对象在内存中的布局可以分成对象头、实例数据、对齐填充三部分:

  • 对象头:它主要包括对象自身的运行行元数据,比如哈希码、GC分代年龄、锁状态标志等;同时还包含一个类型指针,指向类元数据,表明该对象所属的类型
  • 实例数据:它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
  • 对齐填充:它不是必要存在的,仅仅起着占位符的作用
6. 为什么新生代存在两个survivor 区?

简单来讲就是:为了减少内存碎片
新生代采用的复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生

7. 一个对象真正不可用,要经历两次标记过程:
  1. 首先进行可达性分析,筛选出与GC Roots没用引用链的对象,进行第一次标记
  2. 第一次标记后,再进行一次筛选,筛选条件是是否有必要执行finalize()方法。若对象有没有重写finalize()方法,或者finalize()是否已被jvm调用过,则没必要执行,GC会回收该对象
  3. 若有必要执行,则该对象会被放入F-Queue中,由jvm开启一个低优先级的线程去执行它(但不一定等待finalize执行完毕)。
  4. Finalize()是对象最后一次自救的机会,若对象在finalize()中重新加入到引用链中,则它会被移出要回收的对象的集合。其他对象则会被第二次标记,进行回收

参考

周志明《深入理解Java虚拟机》
https://www.jianshu.com/p/2f4a8e04c657
https://blog.csdn.net/canot/article/details/51037938
https://www.jianshu.com/p/d09207c047ec

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值