JAVA-虚拟机
2020-07-28 00:13:47 18 举报
AI智能生成
整理不易,免费分享,只求一个点赞!
作者其他创作
大纲/内容
java垃圾回收机制
垃圾回收算法
标记整理
标记清除
垃圾回收器
串行收集器
并行收集齐
并发收集器
G1收集器
参数配置
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
概念
G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。<br>在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。<br>
回收过程
G1 Young GC
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
G1 Mix GC
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区
在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)
global concurrent marking的执行过程分为五个步骤:<br>初始标记(initial mark,STW)<br><br>在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。<br><br>根区域扫描(root region scan)<br><br>G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。<br><br>并发标记(Concurrent Marking)<br><br>G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断<br><br>最终标记(Remark,STW)<br><br>该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。<br><br>清除垃圾(Cleanup,STW)<br><br>在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。<br>
三色标记算法
提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。<br><br>黑色:根对象,或者该对象与它的子对象都被扫描<br>灰色:对象本身被扫描,但还没扫描完该对象中的子对象<br>白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
过程
根对象被置为黑色,子对象被置为灰色。
继续由灰色遍历,将已扫描了子对象的对象置为黑色。
遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。
这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题
那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?
1.在插入的时候记录对象<br><br>2.在删除的时候记录对象
CMS
在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
G1
G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:<br><br>1,在开始标记的时候生成一个快照图标记存活对象<br>2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)<br>3,可能存在游离(改变的对象依旧是需要被回收的)的垃圾,将在下次被收集
G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区
调优
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
Young GC:选定所有新生代里的region。通过控制新生代的region个数来控制young GC的开销。<br><br>Mixed GC:选定所有新生代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。
需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间
-XX:G1HeapRegionSize=n
设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。
-XX:ParallelGCThreads=n
设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。<br><br>如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中 n 的值可以是逻辑处理器数的 5/16 左右。
-XX:ConcGCThreads=n
设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。
避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。
触发Full GC
G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求
原因
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。
晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:<br>a,增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。<br><br>b,通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。<br><br>c,也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。
巨型对象分配失败 <br>
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。
回收对象定位
可达性分析
引用计数法
内存模型
程序计数器
概念
程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。
为什么需要
我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。<br>
堆
概念
堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制
所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的
虚拟机栈/栈
概念
每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
平时说的栈一般指局部变量表部分
局部变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。<br><br>reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。<br>
需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。
方法区/永久代/元空间/元数据区/非堆
概念
元数据区取代了1.7版本及以前的永久代。元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虚拟机规范是把这个区域(方法区)描述为堆的一个逻辑部分的,但实际它应该是要和堆区分开的。一般情况下为了与堆进行区分,通常又叫“非堆”。<br>在HotSpot中,方法区≈永久代。不过1.7版本之后,我们使用的HotSpot就没有永久代这个概念了,而是在本地内存中使用 元空间 取代了 方法区。在1.7版本之前,“PermGen space”(永久代空间) 其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。
JDK 8 中永久代向元空间的转换的几点原因
1、字符串存在永久代中,容易出现性能问题和内存溢出。<br>2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。<br>3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。<br>4、Oracle 可能会将HotSpot 与 JRockit 合二为一。
jvm参数配置
-XX:MetaspaceSize=8m <br><br>-XX:MaxMetaspaceSize=50m
本地方法栈
本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。<br>
内存屏障(Memory Barrier)
CPU中,每个CPU又有多级缓存,一般分为L1,L2,L3,因为这些缓存的出现,提高了数据访问性能,避免每次都向内存索取,但是弊端也很明显,不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。<br>
为什么需要内存屏障
由于现代操作系统都是多处理器操作系统,每个处理器都会有自己的缓存,可能存在不同处理器缓存不一致的问题,而且由于操作系统可能存在重排序,导致读取到错误的数据,因此,操作系统提供了一些内存屏障以解决这种问题.<br><br>简单来说:<br>1.在不同CPU执行的不同线程对同一个变量的缓存值不同,为了解决这个问题。<br>2.用volatile可以解决上面的问题,不同硬件对内存屏障的实现方式不一样。java屏蔽掉这些差异,通过jvm生成内存屏障的指令。<br><br>对于读屏障:在指令前插入读屏障,可以让高速缓存中的数据失效,强制从主内存取。
内存屏障的作用
1.阻止屏障两侧指令重排序<br>2.强制把写缓冲区/高速缓存中的数据等写回主内存,让缓存中相应的数据失效。
volatile
当我们声明某个变量为volatile修饰时,这个变量就有了线程可见性,volatile通过在读写操作前后添加内存屏障。
可见性,对于一个该变量的读,一定能看到读之前最后的写入。<br>防止指令重排序,执行代码时,为了提高执行效率,会在不影响最后结果的前提下对指令进行重新排序,使用volatile可以防止,比如单例模式双重校验锁的创建中有使用到<br><br>注意的是volatile不具有原子性,如volatile++这样的复合操作
至于volatile底层是怎么实现保证不同线程可见性的,这里涉及到的就是硬件上的,被volatile修饰的变量在进行写操作时,会生成一个特殊的汇编指令,该指令会触发mesi协议,会存在一个总线嗅探机制的东西,简单来说就是这个cpu会不停检测总线中该变量的变化,如果该变量一旦变化了,由于这个嗅探机制,其它cpu会立马将该变量的cpu缓存数据清空掉,重新的去从主内存拿到这个数据。<br>
调优
XX属性
-XX:ParallelGCThreads
并行GC线程数量
-XX:ConcGcThreads
并发GC线程数量
-XX:MaxGCPauseMillis
最大停顿时间,单位毫秒,GC尽力保证回收时间不超过设定值
-XX:GCTimeRatio
垃圾收集时间占总时间的比值,取值0-100,默认99,即最大允许1%的时间做GC
-XX:SurvivorRatio
设置eden区大小和survivor区大小的比例,8表示两个survivor:eden=2:8,即一个survivor占年轻代的1/10
-XX:NewRatio
新生代和老年代的比,4表示新生代:老年代=1:4,即年轻代占堆的1/5
-verbose:gc,-XX:+PrintGC
打印GC的简要信息
-XX:+PrintGCDetails
打印GC详细信息(JDK9之后不再使用)
-XX:+PrintGCTimeStamps
打印GC发生的时间戳(JDK9之后不再使用)
-Xloggc:log/gc.log
指定GC log的位置,以文件输出
-XX:PrintHeapAtGC
每次GC后都打印堆信息
Parallel
-XX:+UseParallelGC 新生代使用并行垃圾收集器
-XX:+UseParallelOldGC 老年代使用并行垃圾收集器
-XX:ParallelGCThreads 设置用于垃圾回收的线程数
-XX:+UseAdaptiveSizePolicy 打开自适应GC策略
如果开启 AdaptiveSizePolicy,则每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。
CMS
-XX:+UseConcMarkSweepGC 新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:+UseParNewGC 新生代使用并行收集器,老年代CMS收集器默认开启
-XX:CMSInitiatingOccupanyFraction 设置触发GC的阈值,默认68%,如果内存预留空间不够,就会引起concurrent mode failure
-XX:+UseCMSCompactAtFullCollection Full GC后,进行一次整理,整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后进行一次碎片整理
-XX:+CMSClassUnloadingEnabled 允许对类元数据进行回收
-XX:+UseCMSInitiatingOccupanyOnly 表示只在达到阈值的时候才进行CMS回收
-XX:+CMSIncrementalMode 使用增量模式,比较适合单CPU
G1
-XX:G1HeapRegionSize 设置Region大小,默认heap/2048
-XX:G1MixedGCLiveThresholdPercent 老年代依靠Mixed GC, 触发阈值
gc日志说明
[GC (Allocation Failure) [PSYoungGen: 157174K->17392K(157184K)] 159834K->23426K(506880K), 0.0423352 secs] [Times: user=0.19 sys=0.01, real=0.04 secs]
[GC/FULL GC 垃圾收集器名称 [YoungGC新生代收集前的内存占用->YoungGC新生代收集后的内存占用(新生代总大小)] JVM堆YoungGC前的内存占用-> JVM堆YoungGC后的内存占用(JVM总大小),YoungGC耗时][YoungGC用户耗时,YoungGC系统耗时,YoungGC真实耗时]
Allocation Failure:<br>表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。
OOM排错
对象的访问定位
概念
java程序需要通过引用(ref)数据来操作堆上面的对象,那么如何通过引用定位、访问到对象的具体位置。
方式
对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式<br>1.句柄访问对象<br>2.直接指针访问对象。(Sun HotSpot使用这种方式)
句柄访问
简单来说就是java堆划出一块内存作为句柄池,引用中存储对象的句柄地址,句柄中包含对象实例数据、类型数据的地址信息。<br><br>优点:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不需要改动引用【ref】本身。<br>
直接指针
与句柄访问不同的是,ref中直接存储的就是对象的实例数据,但是类型数据跟句柄访问方式一样。<br><br>优点:优势很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【可能是出于Java中对象的访问时十分频繁的,平时我们常用的JVM HotSpot采用此种方式】<br>
对象的创建过程
1.类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一 个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。<br><br> new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化。
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。<br><br>这个步骤有两个问题:<br><br>1.如何划分内存。<br><br>2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
划分内存的方法:<br>“指针碰撞”(Bump the Pointer)<br><br>如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配 内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。<br><br><br><br>“空闲列表”(Free List)<br><br>如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录<br>
解决并发问题的方法:<br>CAS(compare and swap)<br><br>虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。<br><br>本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)<br><br>把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。<br>
TLAB的出现
优点
如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。<br><br>TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。<br><br>TLAB的本质其实是三个指针管理的区域:start,top 和 end,每个线程都会从Eden分配一块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。<br><br>TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。从这一点看,它被翻译为 线程私有分配区 更为合理一点<br><br>当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。
缺点
TLAB空间大小是固定的,但是这时候一个大对象,我TLAB剩余的空间已经容不下它了。(比如100kb的TLAB,来了个110KB的对象)<br><br>TLAB空间还剩一点点没有用到,有点舍不得。(比如100kb的TLAB,装了80KB,又来了个30KB的对象)<br><br>所以JVM开发人员做了以下处理,设置了最大浪费空间。<br><br>当剩余的空间小于最大浪费空间,那该TLAB属于的线程在重新向Eden区申请一个TLAB空间。进行对象创建,还是空间不够,那你这个对象太大了,去Eden区直接创建吧!<br><br>当剩余的空间大于最大浪费空间,那这个大对象请你直接去Eden区创建 <br><br>当然,又回造成新的诟病<br>Eden空间够的时候,你再次申请TLAB没问题,我不够了,Heap的Eden区要开始GC,<br>TLAB允许浪费空间,导致Eden区空间不连续,积少成多。以后还要人帮忙打理。
3.初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指 针来确定这个对象是哪个类的实例。<br><br> 初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
5.执行<init>方法
执行<init>方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法
0 条评论
下一页