Jvm

9 minute read

背景

之前写代码时,业务逻辑开发完成后都是直接java -jar *.jar命令大法启动,不过随着知识面的扩展,接触到jconsole等性能分析工具,尤其是偶然发现了项目中的问题后,慢慢对JVM地开始研究。今记录一下累积的相关知识,以备后记。

相关技术栈: Java JVM

JVM内存分区 运行时数据区

程序计数器:记录当前线程执行的位置,用于线程切换时保存和恢复执行位置,每条线程有自己独立的程序计数器,线程私有,是唯一没有规定任何OutOfMemoryError的区域

堆:存放new出来的对象实例,所有线程共享,是java虚拟机管理内存最大的一块 PS:JDK1.8已经移除永久代,取而代之是元空间区域(Metaspace),永久代使用堆内存空间,元空间使用物理内存,直接受到本机物理内存限制。

虚拟机栈:线程私有,由一个个栈帧组成,每个方法在执行的同时都会创建一个栈帧,存放局部变量表、操作数栈、动态链接、方法出口信息。每一个方法从调用直至执行完成,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 局部变量表存放了编译器可知的各种基本数据类型、对象引用等。局部变量表所需的内存空间在编译器完成分配,当进入方法时,方法需要在帧中分配多大的局部变量表空间是完全确定的。

本地方法栈:虚拟机执行native方法

方法区:存放已经加载的类信息、常量、静态变量、字节码

运行时常量池:用于存放编译期生成的各种字面量和符号引用。相比于class文件常量池,运行时常量池的重要特征是动态性,并非class文件中的常量池的内容能进入,运行时也能将新的常量放入池中。

PS:JDK1.7及之后版本将运行时常量池从方法区中移除出来,在Java堆开辟一块区域存放运行时常量池

直接内存:可以通过native函数直接分配堆外内存,避免在Java堆和native堆来回复制数据

其中,堆、方法区、直接内存为线程共享,栈、程序计数器为线程私有

对象创建与访问

对象创建

对象创建可以分为3个阶段,加载、链接、初始化,其中链接又可以细分为3个阶段,验证、准备、解析。

虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池定位到一个类的符号引用,检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,先进行类加载过程。

对象在内存中的存储可以分为3块:对象头、实例数据和对齐填充。

对象头包括两部分,一部分存储对象自身的状态信息,例如哈希码、GC分代年龄、锁状态信息等,另一部分存储类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。

实例数据存储对象真正的数据,例如类中定义的字段内容。

对齐填充起占位作用,因为JVM要求对象的起始地址必须是8字节的整数倍,意味着对象大小必须是8字节的整数倍。

对象访问

当使用一个对象的时候,是通过栈上的reference来去堆中查找。访问对象的方式主流有两种,句柄和直接指针。 https://blog.csdn.net/hbtj_1216/article/details/77599990 句柄是指堆中分出一块区域作为句柄池,栈上reference存储的是对象在句柄池的地址,句柄池中分别存储着对象的实例数据地址和类型数据地址。 直接指针是指栈上reference指向对象实例数据地址,这这个地址的内存中有指针指向方法区的对象类型数据。 目前hot spot就是采用这种对象访问方式。 句柄优缺点: 优点: reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变 缺点: 增加了一次指针定位的时间开销。 直接指针优缺点: 优点: 节省了一次指针定位的开销。 缺点: 在对象被移动时reference本身需要被修改。

虚拟机类加载机制

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

java语言里面,类型加载、连接和初始化都是在程序运行期间完成,Java天生可以动态扩展特性是依赖运行期动态加载和动态链接的特点实现的。

类从被加载和虚拟机内存中开始,到卸载出内存为止,整个生命周期包括加载、验证、准备、解析、初始化、使用、卸载7个阶段,其中验证、准备、解析3部分统称为连接。

虚拟机规定有且只有5种情况: 1、使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法时 2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要触发其初始化 3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,需要先触发其父类初始化 4、当虚拟机启动时,用户指定一个要执行的朱磊,虚拟机会初始化这个主类 5、当使用jdk1.7动态语言支持时,如果这个方法所对应的类还没有初始化,需要先触发其初始化

对于静态字段,只有直接定义这个字段的类才会被初始化,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化不会触发子类的初始化。 当一个类在初始化时,要求其父类全部都已经初始化过了,但是当一个接口初始化时,并不要求其父接口全部完成初始化,只有真正使用父接口的时候,如引用接口中定义的常量,才会初始化。

加载:在加载阶段,虚拟机需要完成以下3件事情:

1、通过一个类的全限定名获取定义此类的二进制字节流

2、将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构

3、在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口

对于数组类而言,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的,但数组类与类加载器仍然有密切的关系,因为数组类的元素需要靠类加载器去创建。

加载阶段完成后,虚拟机外部的二进制字节流按照虚拟机所需的格式存储在方法区,然后在内存中实例化一个Class对象,它虽然是对象,但存储在方法区,作为程序访问方法区中类型数据的外部接口。

验证:文件格式验证、元数据验证、字节码验证、符合引用验证

准备:准备阶段是正式为类变量分配内存并设置类变量初始化的阶段,这时候进行内存分配的仅包括类变量(被static修饰),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。 如果类变量被final修饰,那在准备阶段就会直接被初始化为value值

解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。

虚拟机并未规定解析阶段发生的具体时间,虚拟机实现可以根据需要来判断到底是在类被被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用被使用前再去解析。

初始化:编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生,编译器收集的顺序是语句在源文件中出现的顺序所决定。

静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。

类加载器:通过一个类的全限定名来获取描述此类的二进制字节流。

对于任意一个类,都需要由加载它的类加载器和向类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

双亲委派模型:

从Java虚拟机的角度看,只存在两种不同的类加载器:一种是启动类加载器,C++语言实现,虚拟机自身的一部分;另一部分是其它类加载器,全由Java语言实现,独立于虚拟机外部,并且全部继承于java.lang.ClassLoader

从开发人员角度看,大部分Java程序会使用以下3种系统提供的类加载器。

启动类加载器(Bootstrap ClassLoader):启动类加载器无法被Java程序直接引用,用户编写自定义类加载器,如果需要把加载请求委派给引导类加载器,直接使用null即可

扩展类加载器(Extension ClassLoader):开发者可以直接使用扩展类加载器

应用程序类加载器(Application ClassLoader):这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,一般也称为系统类加载器,它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,一般这个是程序默认的类加载器

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,所有的类加载器 最终都应该传送到顶层的启动类加载器,只有当父加载器自己无法完成这个加载请求(在它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去加载。

垃圾回收

对象判定

gc回收的是已经死亡的对象,如何判断一个对象是否存活,有两种方式,引用计数和GC Roots对象是否可达。

引用计数方式是清除引用计数为0的对象,当一个对象的某个引用超过生命周期(出作用域)或被设置新值,引用数减1(Java没有选择这种方式)

可达性分析是指通过一系列GC Roots对象作为起点,向下搜索,当一个对象与GC Roots没有任何引用链相连时,该对象标记为不可达

GC Roots对象包括以下四种:

1、栈中引用的对象

2、本地方法栈中引用对象

3、方法区中类静态属性引用的变量

4、方法区中常量引用的变量

在java中,可作为GC Roots的对象有: 1.虚拟机栈(栈帧中的本地变量表)中引用的对象; 2.方法区中的类静态属性引用的对象; 3.方法区中常量引用的对象; 4.本地方法栈中JNI(即一般说的Native方法)中引用的对象

对象之间的引用根据强弱程度可分为以下4类 https://juejin.cn/post/6844904085091516430 1、强引用 new出来的对象 2、软引用 可用SoftReference类来实现软引用,系统即将发生内存溢出异常之前,会将此类对象回收,如果内存充足,不会回收 3、弱引用 可用WeakReference类来实现弱引用,无论当前内存是否足够,用软引用相关联的对象都会被回收掉 4、虚引用 可用PhantomReference类来实现虚引用,为一个对象设置虚引用的唯一目的是:能在这个对象在垃圾回收器回收时收到一个系统通知 Java设计这四种引用的主要目的有两个: 可以让程序员通过代码的方式来决定某个对象的生命周期; 有利于垃圾回收。 虚引用有什么用呢?在NIO中,就运用了虚引用管理堆外内存。

当一个对象第一次被标记为可回收的时候,jvm会判断此对象是否有必要执行finalize方法,如果当前对象finalize方法被重写,且之前没调用过,允许将这个对象放入队列中,等待被执行finalize方法,但由于线程优先级较低,不保证finalize方法一定被执行;如果在finalize方法中能和GC Roots对象关联上,则不会被回收。

回收算法

1、标记-清除 回收死亡的对象,优点不需要移动对象,缺点会产生碎片 2、复制 内存分为两块,把存活的对象复制到另一块 优点解决碎片问题,缺点可以内存减少,容易触发gc 3、标记-整理 将存活对象往某一端移动,清理另一端空间 优点解决碎片问题,缺点仍需要移动对象,一定程度上降低效率

HotSpot虚拟机垃圾回收实现

垃圾回收主要的作用区域是在堆上,商业虚拟机一般采用的回收算法是分代收集。我用jconsole连接远程服务器上的Java进程,发现有如下几个分区:

Young Generation: 其中分为Eden和两个Survivor,-XX:SurvivorRatio=8可以设置Eden和Survivor的比例,JVM会根据gc的频率自动调整比例,一般默认即可

Old Generation: 存放MinorGc后年轻代活着的对象

Metaspace: 方法区,jdk1.8之后改为Metaspace,存储类的元数据信息,例如字段、静态属性、方法、常量

Code Cache: 存放编译后的代码,即字节码

Compressed Class Space:32bit的压缩版本 https://www.zhihu.com/question/268392125

参与垃圾回收的区域包括Young Generation和Old Generation:

新生成的对象一般分配在Eden区,当Eden区占满时,触发Minor GC,采用复制算法将Eden和一块Survivor上活着的对象复制到另一块Survivor,如果另一块Survivor不能存放当前Minor GC存活的对象,则将存活对象移动到Old Generation,如果Old Generation不能放下存活对象,则触发Major GC。

Minor GC触发条件:Eden区满

Major GC触发条件:1、显式调用System.gc,但不必然执行 2、老年代空间不足 3、方法区空间不足

内存分配的几个规则:

1、对象有限在Eden分配

2、大对象(大量连续空间内存)直接进入老年代,目的是为了避免在Eden区和Survivor区重复发生内存复制

3、长期存活的对象进入老年代,每经历一次Minor GC,对象年龄加1

4、动态对象年龄分配,如果Survivor区中相同年龄的对象所占空间大于Survivor的一半,大于等于该年龄的对象直接进入老年代

5、空间分配担保 一般允许担保失败,避免频繁的进行Major GC

垃圾收集器

1、Serial收集器 新生代收集器,单线程收集器,进行垃圾收集时,必须暂停其它所有工作线程 虚拟机运行在client模式的默认新生代收集器 适合单cpu

2、ParNew收集器 新生代收集器,Serial收集器的多线程版本,Server模式下的首选新生代收集器,只有Serial和ParNew收集器能和CMS收集器配合工作

3、Parallel Scavenge收集器,又称为吞吐量优先收集器,新生代收集器,复制算法,多线程收集,目标是达到可控制的吞吐量,提供两个参数用于精确控制吞吐量,控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis 直接设置吞吐量大小的-XX:GCTimeRatio

此外还有一个重要参数-XX:+UseAdaptiveSizePolicy,GC自适应调节策略,虚拟机默认会动态调整参数以提供最合适的停顿时间或吞吐量,只需把基本数据设置好,然后设置优化目标,-XX:MaxGCPauseMillis或-XX:GCTimeRatio。

自适应调节策略是Parallel Scavenge和ParNew的一个重要区别。

4、Serial Old收集器 Serial收集器的老年代版本,单线程收集器,标记-整理算法,主要给Client模式的虚拟机使用

5、Parallel Old收集器 Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法,它的出现意味着有了比较名副其实的吞吐量优先收集器,即Parallel Scavenge+Parallel Old,注重吞吐量及CPU资源敏感

6、CMS收集器 老年代收集器,基于标记-清除算法,新生代默认采用ParNew收集器,目标是尽可能缩短垃圾收集时用户线程的停顿时间

1、初始标记 stw,仅标记GC Roots能直接关联到的对象,速度很快

2、并发标记

3、重新标记 stw,修正并发标记阶段因用户线程运行改动的一部分,比初始标记稍长一些,远比并发标记时间短

4、并发清除

CMS有3个明显缺点:

1、虽然不会导致用户线程停顿,但会因为占用一部分线程导致用户程序变慢,总吞吐量降低

2、无法处理浮动垃圾,CMS并发清除阶段仍然会有垃圾产生,所以需要预留一部分空间,jdk1.6设定老年代占用92%开始启动,如果CMS预留的内存无法满足程序需要,将会临时启用Serial Old收集器进行老年代的垃圾回收

3、由于基于标记-清除,意味着会有碎片产生,不过CMS收集器提供参数-XX:+UseCMSCompactAtFullCollection(默认开启)进行碎片合并整理,还有另外一个参数-XX:CMSFullGCsBeforeCompaction设置多少次不压缩的GC后来一次压缩的

PS: Parallel Scavenge无法与CMS配合

7、G1收集器 jdk1.7u4才开始商用,具备以下特点

1、并行与并发 充分利用多CPU缩短stw的时间,本来需要停顿java线程的GC动作,G1仍能通过并发方式让程序继续执行

2、分代收集 仍然保留分代概念,不需要需其它收集器配合就能管理整个堆,能够采用不同的方式去处理新创建的对象和已经存活一段时间的对象获取更好的收集效果

3、空间整合 不会产生碎片,总体基于标记-整理,局部(Region之间)基于复制算法

4、可预测的停顿 这是G1相对于CMS的另一大优势,能让使用者明确指定一个长度M毫秒的时间片内消耗在垃圾收集的最长时间

G1将内存划分多个Region,新生代和老年代不再物理隔离,是一部分Region的集合,之所以停顿时间可预测,因为可以避免整个堆的卡机收集,会有计划的回收部分region。

PS: jdk1.8默认 UseParallelGC ParallelGC 默认的是 Parallel Scavenge(新生代)+ Parallel Old(老年代)

JVM调优

调优目标是根据程序的预测目标,通过分析GC日志,不断调整各项参数,使程序满足预设的吞吐量和降低服务的响应时间。

PS: jvm进行垃圾收集时,虚拟机虽然会对direct memory进行回收,但不会主动触发,只能等待老年代满之后Major GC才能清理

性能监控命令

jps -v 输出进程启动时的JVM参数

jstat 虚拟机统计信息监视工具

jinfo java配置信息工具

jmap java内存映像工具

jstack java堆栈跟踪工具

进程无缘无故消失了,怎么排查?

1,linux的OOM killer杀死; 2,JVM自身故障; 3,JVM的OOM导致进程退出;

Linux 内核有个机制叫OOM killer(Out-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉。 可以去下面这个文件里翻,系统报错日志: /var/log/messages egrep -i ‘killed process’ /var/log/messages 也可以去内核日志里头查询 dmesg | grep java

JVM自身故障 当JVM发生致命错误导致崩溃时,会生成一个hs_err_pid_xxx.log这样的文件,该文件包含了导致 JVM crash 的重要信息,我们可以通过分析该文件定位到导致 JVM Crash 的原因,从而修复保证系统稳定。

JVM的OOM 堆内存溢出 一般情况下,JVM的GC会进行回收,是不会因为OOM导致JVM进程退出的。要真说OOM导致退出的情况,那很有可能就是内存泄漏,由于内存占用越来越大,且无法正常回收(例如有强依赖等),结果。。。。 不过这种JVM的OOM导致的异常,很好排查。注意两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump_${YYYYMMDD}.hprof; 然后去对应目录找dump快照文件,接下来借助可视化工具分析就行,很容易定位问题。

方法区(运行时常量池)和元空间溢出 方法区和堆一样,是线程共享的区域,包含Class文件信息、运行时常量池、常量池。 运行时常量池和常量池的主要区别是具备动态性,也就是不一定非要是在Class文件中的常量池中的内容才能进入运行时常量池,运行期间也可以可以将新的常量放入池中,比如String的intern()方法。

验证一下String.intern() 设置-XX:MetaspaceSize=50m -XX:MaxMetaspaceSize=50m 元空间大小 1.8版本之前方法区存在于永久代(PermGen),1.8之后取消了永久代的概念,转为元空间(Metaspace),如果是之前版本可以设置PermSize MaxPermSize永久代的大小。

https://xie.infoq.cn/article/74d7449272c21dd9f8d706957

直接内存

https://blog.csdn.net/weixin_45505313/article/details/105310477 直接内存主要被 Java NIO 使用,某种程度上也就是指DirectByteBuffer对象占用的堆外内存。DirectByteBuffer对象创建时会通过Unsafe类接口直接调用操作系统的malloc分配内存,然后将内存的起始地址和大小保存下来,据此就可以直接操作内存空间

Full GC一般发生在年老代垃圾回收或者代码调用System.gc的时候 堆外内存并不直接控制于JVM,这些内存只有在DirectByteBuffer回收掉之后才有机会被回收 而 Young GC 的时候只会将年轻代里不可达的DirectByteBuffer对象及其直接内存回收,如果这些对象大部分都晋升到了年老代,那么只能等到Full GC的时候才能彻底地回收DirectByteBuffer对象及其关联的堆外内存。 因此,堆外内存的回收依赖于 Full GC 场景八:堆外内存 OOM https://tech.meituan.com/2020/11/12/java-9-cms-gc.html

https://tech.meituan.com/2018/10/18/netty-direct-memory-screening.html

jvm调优 https://juejin.cn/post/6844903802378665997 多数的 Java 应用不需要在服务器上进行 GC 优化; 多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题; 在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合); 减少创建对象的数量; 减少使用全局变量和大对象; GC 优化是到最后不得已才采用的手段; 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多。

GC调优目的 将转移到老年代的对象数量降低到最小;减少 GC 的执行时间。 1、将新对象预留在新生代 2、大对象进入老年代 -XX:PretenureSizeThreshold 可以设置直接进入老年代的对象大小。 3、合理设置进入老年代对象的年龄 -XX:MaxTenuringThreshold 设置对象进入老年代的年龄大小 4、设置稳定的堆大小,堆大小设置有两个参数:-Xms 初始化堆大小,-Xmx 最大堆大小。 5、注意: 如果满足下面的指标,则一般不需要进行 GC 优化: MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。 https://juejin.cn/post/6844903802378665997

-XX:+HeapDumpOnOutOfMemoryError 堆内存溢出时保留快照

dump文件实战 https://blog.csdn.net/AlbertFly/article/details/78686408

主动触发gc Java的公有API可以主动调用GC的有两种办法

System.gc(); Runtime.getRuntime().gc(); Java的GC是由JVM自行调动的,在需要的时候才执行,上面的指令只是告诉JVM尽快GC一次,但不会立即执行GC。

参考

《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)》

https://juejin.im/post/5b7d69e4e51d4538ca5730cb

https://www.cnblogs.com/paddix/p/5309550.html

jdk8 jvm分区 https://www.choupangxia.com/2019/10/22/interview-jvm-gc-02/ jvm 内存模型 https://www.choupangxia.com/2019/10/18/jvm%E4%B9%8B%E5%86%85%E5%AD%98%E7%BB%93%E6%9E%84%E8%AF%A6%E8%A7%A3/

jdk8取消了永久代,新增了元空间? 表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。 当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

更深层的原因还是要合并HotSpot和JRockit的代码,JRockit从来没有所谓的永久代,也不需要开发运维人员设置永久代的大小,但是运行良好。 同时也不用担心运行性能问题了,在覆盖到的测试中, 程序启动和运行速度降低不超过1%,但是这点性能损失换来了更大的安全保障。

Java7及以前版本的Hotspot中方法区位于永久代中。同时,永久代和堆是相互隔离的,但它们使用的物理内存是连续的。

永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。

但在Java7中永久代中存储的部分数据已经开始转移到Java Heap或Native Memory中了。比如, 符号引用(Symbols)转移到了Native Memory; 字符串常量池(interned strings)转移到了Java Heap; 类的静态变量(class statics)转移到了Java Heap。

Java8,HotSpots取消了永久代,那么是不是就没有方法区了呢?当然不是,方法区只是一个规范,只不过它的实现变了。

在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。 同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。

本地内存(Native memory),也称为C-Heap,是供JVM自身进程使用的。 当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC。

jvm gc http://www.choupangxia.com/2019/10/20/interview-jvm-gc-01/

jvm参数格式 对于参数的格式可以这样理解:

-: 标准VM选项,VM规范的选项。 -X: 非标准VM选项,不保证所有VM支持。 -XX: 高级选项,高级特性,但属于不稳定的选项。

切记:方法区只是一个规范,永久代是方法区的实现,元空间也是方法区的实现

JVM中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生随线程而灭。 栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理。它们的内存分配和回收都具有确定性。

cms垃圾收集器 cms的目标是最短回收停顿时间,因为垃圾回收线程和用户线程可以并发执行,达到降低收集停顿时间的功能 cms基于标记清除算法,仅作用于老年代,收集过程分为4个阶段: 1、初始标记 stw 2、并发标记 3、重新标记 stw 4、并发清除 cms缺点? 吞吐量低、浮动垃圾、内存碎片 cms之所以只用于老年代,是因为标记清除产生的碎片对于新生代是难以接受的 线程并非在任意地方都会停顿下来gc,只有安全点、安全区域才会gc 安全点的定义是Java的堆栈不再有变化

g1垃圾收集器 garbage first https://zhuanlan.zhihu.com/p/52841787 从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,不会产生碎片 垃圾回收的作用区域是region,避免全堆扫描,g1会尽可能将堆划分为2018个region,g1会自动调整 实现可预测的停顿时间 g1引入了一种新的gc roots的集合Remembered Sets Rsets, YGC是否需要扫描整个老年代? RSet记录了其他Region中的对象引用本Region中对象的关系 查找新生代对象,是否有gc roots执行它需要全堆扫描,g1引入了card table卡表,每个卡的标识位代表是否有指向新生代对象的引用,如果有,标识位置为脏卡 在进行Minor GC的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成所有脏卡的扫描之后,Java虚拟机便会将所有脏卡的标识位清零。 卡表能用于减少老年代的全堆空间扫描,这能很大的提升GC效率。

cms vs g1: G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。 Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽量划分2048个左右、同等大小的Region 这个数字既可以手动调整,G1也会根据堆大小自动进行调整。

g1能够实现可预测的停顿时间的原因是什么? G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。 G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Regions作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。

什么情况下适合从cms或parallelOld变为g1? 官方建议当场景满足以下特性时,可以更换成g1 1、实时数据占用了超过半数的堆空间; 2、对象分配率或“晋升”的速度变化明显; 3、期望消除耗时较长的GC或停顿(超过0.5——1秒)。

jmm: java memory model java内存模型是Java对于在多线程并发情况下对于共享变量读写的规范,主要是为了解决多线程可见性、原子性的问题,解决共享变量的多线程操作冲突问题。 所有的变量都存储在主内存中,每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中变量的一份拷贝) JMM的两条规定 1、线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写; 2、不同的线程之间无法直接访问其他线程工作内存中的变量,线程变量值的传递需要通过主内存来完成。

Updated:

Leave a comment