一篇与面试官和善交流的深刻了解JVM(JDK8)

文章目录

 

一、类加载机制

类加载过程分为 加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载 

一、加载
	在硬盘上查找并经过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象 等等,在加载阶段会在内存中生成一个表明这个类的java.lang.Class对象,做为方法区这个类的各类数据的访问入口 

二、验证 
	校验字节码文件的正确性 
    
三、准备
	给类的静态变量分配内存,并赋予默认值 

四、解析 
	将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,好比main()方法)替换为指向数据 所存内存的指针或句柄等(直接引用),这是所谓的静态连接过程(类加载期间完成),动态连接是在程 序运行期间完成的将符号引用替换为直接引用,下节课会讲到动态连接 

五、初始化 
	对类的静态变量初始化为指定的值,执行静态代码块
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

二、双亲委派机制(先找父亲加载,不行再由儿子本身加载)

2.一、类加载器java

一、根类加载器(**Bootstrap classLoader**):负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,好比rt.jar、charsets.jar等
    
二、扩展类加载器(**ExtClassLoader**):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
    
三、应用加载器(**AppClassLoader**):负责加载ClassPath路径下的类包,主要就是加载你本身写的那些类,负责加载用户自定义路径下的类包
  • 1
  • 2
  • 3
  • 4
  • 5

2.二、加载器初始化过程程序员

类运行加载全过程会建立JVM启动器实例sun.misc.Launcher。sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。在Launcher构造方法内部,其建立了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。
    
JVM默认使用launcher的`getClassLoader()`方法返回的类加载器`AppClassLoader`的实例来加载咱们的应用程序。
  • 1
  • 2
  • 3

2.三、双亲委派机制web

应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大致逻辑以下:
首先,检查一下指定名称的类是否已经加载过,若是加载过了,就不须要再加载,直接返回。
若是此类没有加载过,那么,再判断一下是否有父加载器;若是有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
若是父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。算法

2.四、为何要设计双亲委派机制?spring

一、沙箱安全机制:本身写的java.lang.String.class类不会被加载,这样即可以防止核心API库被随意篡改
二、避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的惟一性数据库

2.五、全盘负责委托机制bootstrap

“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入数组

2.六、自定义类加载器示例浏览器

自定义类加载器只须要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,因此咱们自定义类加载器主要是重写findClass方法。缓存

三、tomcat怎么破解类加载机制

一、commonLoader:Tomcat最基本的类加载器,加载路径中的class能够被Tomcat容器自己以及各个Webapp访问;

二、catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;

三、sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于全部Webapp可见,可是对于Tomcat容器不可见;

四、WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,好比加载war包里相关的类, 每一个war包应用都有本身的WebappClassLoader,实现相互隔离,好比不一样war包应用引入了不一样的spring版本,这样实现就能加载各自的spring版本;

五、模拟实现Tomcat的JasperLoader热加载

​ 原理:后台启动线程监听jsp文件变化,若是变化了找到该jsp对应的servlet类的加载器引用 (gcroot),从新生成新的JasperLoader加载器赋值给引用,而后加载新的jsp对应的servlet类,以前的那个加载器由于没有gcroot引用了,下一次gc的时候会被销毁

=>总结:每一个webappClassLoader加载本身的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。

四、内存模型

4.一、线程私有区域

程序计数器:是当前线程所执行的字节码的行号指示器,无OOM

虚拟机栈:是描述java方法执行的内存模型,每一个方法在执行的同时都会建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态连接、方法出口等信息。

  • 栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态连接

(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创

建,随着方法结束而销毁——不管方法是正常完成仍是异常完成(抛出了在方法内未被捕获的异

常)都算做方法结束。

本地方法栈:和 Java Stack 做用相似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为

Native 方法服务, 若是一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个

C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

4.二、线程共享区域

==堆-运行时数据区:==是被线程共享的一块内存区域,建立的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。因为现代 VM 采用分代收集算法, 所以 Java 堆从 GC 的角度还能够细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代

方法区/永久代(1.8以后元空间):用于存储被 JVM 加载的类信息**、常量静态变量、**即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即便用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就能够像管理 Java 堆同样管理这部份内存, 而没必要为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 所以收益通常很小)。

  • 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后存放到方法区的运行时常量池中。

在这里插入图片描述

直接内存

jdk1.4后加入NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,它能够使用native函数库直接分配堆外内存,而后经过一个存储在java堆中的DirectByteBuffer对象做为这块内存的引用进行操做。能够避免在Java堆和Native堆中来回复制数据

直接内存的分配不会受到Java堆大小的限制.避免大于物理内存的状况

五、对象的建立

在这里插入图片描述

一、类加载检查

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,而且检查这个符号引用表明的类是否已被加载、解析和初始化过。若是没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等
  • 1
  • 2

二、分配内存

在类加载检查经过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后即可彻底肯定,为对象分配空间的任务等同于把 一块肯定大小的内存从Java堆中划分出来。
 //如何划份内存?
 一、“指针碰撞”(Bump the Pointer)(默认用指针碰撞)
		若是Java堆中内存是绝对规整的,全部用过的内存都放在一边,空闲的内存放在另外一边,中	  间放着一个指针做为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段	 与对象大小相等的距离。
 二、“空闲列表”(Free List)
		若是Java堆中的内存并非规整的,已使用的内存和空闲的内存相互交错,那就没有办法简	  单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从	 列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
 //解决并发问题的方法
 一、CAS(compare and swap)
		虚拟机采用CAS配上失败重试的方式保证更新操做的原子性来对分配内存空间的动做进行同	 步处理。
	二、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
		把内存分配的动做按照线程划分在不一样的空间之中进行,即每一个线程在Java堆中预先分配一	  小块内存。经过­XX:+/­UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启			­XX:+UseTLAB),­XX:TLABSize指定TLAB大小。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3.初始化

内存分配完成后,虚拟机须要将分配到的内存空间都初始化为零值(不包括对象头), 若是使用TLAB,这一工做过程也能够提早至TLAB分配时进行。这一步操做保证了对象的实例字段在Java代码中能够不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  • 1

4.设置对象头

初始化零值以后,虚拟机要对对象进行必要的设置,例如这个对象是哪一个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局能够分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例。
  • 1
  • 2

在这里插入图片描述

5.执行方法

执行<init>方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不一样,这是由程序员赋的值),和执行构造方法。
  • 1

5.一、对象大小与指针压缩

5.1.一、对象大小

对象大小能够用 jol­-core 包查看

5.1.二、什么是java对象的指针压缩?

  1. jdk1.6 update14开始,在64bit操做系统中,JVM支持指针压缩
  2. jvm配置参数:UseCompressedOops,compressed­­压缩、oop(ordinary object pointer)­­对象指针
  3. 启用指针压缩:­XX:+UseCompressedOops(默认开启),禁止指针压缩:­XX:­UseCompressedOops

5.1.三、为何要进行指针压缩?

1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
2.为了减小64位平台下内存的消耗,启用指针压缩功能
3.在jvm中,32位地址最大支持4G内存(2的32次方),能够经过对对象指针的压缩编码、解码方式进行优化,使得jvm
只用32位地址就能够支持更大的内存配置(小于等于32G)
4.堆内存小于4G时,不须要启用指针压缩,jvm会直接去除高32位地址,即便用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,因此堆内存不要大于32G为好

六、对象的分配过程

在这里插入图片描述

6.一、栈上分配

咱们经过JVM内存分配能够知道JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,须要依靠GC进行回收内存,若是对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减小临时对象在堆内分配的数量,JVM经过逃逸分析肯定该对象不会被外部访问。若是不会逃逸能够将该对象在栈上分配内存,这样该对象所占用的内存空间就能够随栈帧出栈而销毁,就减轻了垃圾回收的压力。

==对象逃逸分析:==就是分析对象动态做用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如做为调用参数传递到其余地方中。

public User test1() {
  User user = new User();
  user.setId(1);
  user.setName("zhuge");
  //TODO 保存到数据库
  return user;
 }

 public void test2() {
  User user = new User();
  user.setId(1);
  user.setName("zhuge");
  //TODO 保存到数据库
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

很显然test1方法中的user对象被返回了,这个对象的做用域范围不肯定,test2方法中的user对象咱们能够肯定当方法结束这个对象就能够认为是无效对象了,对于这样的对象咱们其实能够将其分配在栈内存里,让其在方法结束时跟随栈内存一块儿被回收掉。
JVM对于这种状况能够经过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其经过标量替换优先分配在栈上(栈上分配),JDK7以后默认开启逃逸分析,若是要关闭使用参数(-XX:-DoEscapeAnalysis)
==标量替换:==经过逃逸分析肯定该对象不会被外部访问,而且对象能够被进一步分解时,JVM不会建立该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会由于没有一大块连续空间致使对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7以后默认开启。
==标量与聚合量:==标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是能够被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是能够被进一步分解的聚合量。

结论:栈上分配依赖于逃逸分析和标量替换

6.二、对象在Eden区分配(大部分状况,当 Eden 区没有足够空间进行分配时,出现Young GC)

大量的对象被分配在eden区,eden区满了后会触发Young GC,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到s0区,下一次eden区满了后又会触发Young GC,把eden区和s0区垃圾对象回收,把剩余存活的对象一次性挪动到另一块为空的s1区,由于新生代的对象都是朝生夕死的,存活时间很短,因此JVM默认的8:1:1的比例是很合适的,让eden区尽可能的大,survivor区够用便可,JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会致使这个8:1:1比例自动变化,若是不想这个比例有变化能够设置参数-XX:-UseAdaptiveSizePolicy

6.三、大对象直接进入老年代

大对象就是须要大量连续内存空间的对象(好比:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 能够设置大对象的大小,若是对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。
好比设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC ,再执行下上面的第一个程序会发现大对象直接进了老年代

为何要这样呢?
为了不为大对象分配内存时的复制操做而下降效率。

6.四、长期存活的对象将进入老年代

虚拟机给每一个对象一个对象年龄(Age)计数器。若是对象在 Eden 出生并通过第一次 Minor GC 后仍然可以存活,而且能被 Survivor 容纳的话,将被移动到 Survivor
空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增长1岁,当它的年龄增长到必定程度(默认为15岁,CMS收集器默认6岁,不一样的垃圾收集器会略微有点不一样),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,能够经过参数 -XX:MaxTenuringThreshold 来设置。

6.五、对象动态年龄判断

当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio能够指定),那么此时大于等于这批对象年龄最大值的对象,就能够直接进入老年代了,例如Survivor区域里如今有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则实际上是但愿那些多是长期存活的对象,尽早进入老年代。对象动态年龄判断机制通常是在young gc以后触发的。

6.六、老年代空间分配担保机制)

年轻代每次minor gc以前JVM都会计算下老年代剩余可用空间
若是这个可用空间小于年轻代里现有的全部对象大小之和(包括垃圾对象)
就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了) 的参数是否设置了
若是有这个参数,就会看看老年代的可用内存大小,是否大于以前每一次minor gc后进入老年代的对象的平均大小。
若是上一步结果是小于或者以前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一块儿回收一次垃圾,若是回收完仍是没有足够空间存放新的对象就会发生"OOM"。

固然,若是minor gc以后剩余存活的须要挪动到老年代的对象大小仍是大于老年代可用空间,那么也会触发full gc,full gc完以后若是仍是没有空间放minor gc以后的存活对象,则也会发生“OOM”

在这里插入图片描述

七、如何判断一个类是无用的类

  • 该类全部的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。

八、finalize()方法最终断定对象是否存活

1. 第一次标记并进行一次筛选。

筛选的条件是此对象是否有必要执行finalize()方法。

当对象没有覆盖finalize方法,对象将直接被回收。

2. 第二次标记

若是这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,若是对象要在finalize()中成功拯救 本身,只要从新与引用链上的任何的一个对象创建关联便可,譬如把本身赋值给某个类变量或对象的成员变量,那在第 二次标记时它将移除出“即将回收”的集合。若是对象这时候还没逃脱,那基本上它就真的被回收了。

注意:一个对象的finalize()方法只会被执行一次,也就是说经过调用finalize方法自我救命的机会就一次。

九、常见引用类型(四大引用)

一、强引用:普通的变量引用

二、软引用(SoftReference):将对象用SoftReference软引用类型的对象包裹,正常状况不会被回收,可是GC作完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。

  • 使用场景:浏览器的后退按钮

三、弱引用(WeakReference):将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差很少,GC会直接回收掉,不多用

四、虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

十、对象回收

什么叫对象回收?

堆中几乎放着全部的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

10.一、引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任什么时候候计数器为0的对象就是不可能再被使用的。
缺点:循环引用问题

10.二、可达性分析算法(gcroot)

将“GC Roots” 对象做为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其他未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等

在这里插入图片描述

十一、四大垃圾回收算法

11.一、分代收集理论

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不一样将内存分为几块。通常将java堆分为新生代和老年代,这样咱们就能够根据各个年代的特色选择合适的垃圾收集算法。
好比在新生代中,每次收集都会有大量对象(近99%)死去,因此能够选择复制算法,只须要付出少许对象的复制成本就能够完成每次垃圾收集。而老年代的对象存活概率是比较高的,并且没有额外的空间对它进行分配担保,因此咱们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。

11.二、标记-复制算法

它能够将内存分为大小相同的两块,每次使用其中的一块。当这一块的
内存使用完后,就将还存活的对象复制到另外一块去,而后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

在这里插入图片描述

11.三、标记-清除算法

算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收全部未被标记的对象(通常选择这种);也能够反过来,标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象 。它是最基础的收集算法,比较简单,可是会带来两个明显的问题:

  1. 效率问题 (若是须要标记的对象太多,效率不高)
  2. 空间问题(标记清除后会产生大量不连续的碎片)

在这里插入图片描述

11.四、标记-整理算法

根据老年代的特色特出的一种标记算法,标记过程仍然与“标记-清除”算法同样,但后续步骤不是直接对可回收对象回收,而是让全部存活的对象向一端移动,而后直接清理掉端边界之外的内存。

在这里插入图片描述

十二、常见oom

一、java.lang.StackOverflowError:

​ 报这个错误通常是因为方法深层次的调用,默认的线程栈空间大小通常与具体的硬件平台有关。栈内存为线程私有的空间,每一个线程都会建立私有的栈内存。栈空间内存设置过大,建立线程数量较多时会出现栈内存溢出StackOverflowError。同时,栈内存也决定方法调用的深度,栈内存太小则会致使方法调用的深度较小,如递归调用的次数较少。

二、java.lang.OutOfMemoryError: Java heap space

​ Heap size 设置 JVM堆的设置是指:java程序执行过程当中JVM可以调配使用的内存空间的设置。JVM在启动的时候会本身主动设置Heap size的值,其初始空间(即-Xms)是物理内存的1/64最大空间(-Xmx)是物理内存的1/4。可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。Heap size 的大小是Young Generation 和Tenured Generaion 之和。

三、java.lang.OutOfMemoryError:GC overhead limit exceeded

​ GC回收时间过长时会抛出的OutOfMemory。过长是指,超过98%的时间都在用来作GC而且回收了不到2%的堆内存。连续屡次的GC,都回收了不到2%的极端状况下才会抛出。假如不抛出GC overhead limit 错误会发生什么事情呢?那就是GC清理出来的一点内存很快又会被再次填满,强迫GC再次执行,这样形成恶性循环,CPU的使用率一直很高,可是GC没有任何的进展。

四、java.lang.OutOfMemoryError:Direct buffer memory

​ 写NIO程序常用到ByteBuffer来读取或者写入数据,这是一种基于通道与缓冲区的I/O方式。它能够使用Native函数库直接分配堆外内存,而后经过一个存储在java堆里面的DirectByteBuffer对象做为这块内存的引用进行操做。这样能在一些场景中提升性能,由于避免了java堆和Native堆中来回复制数据。

  • ByteBuffer.allocate(capability) :这种方式是分配JVM堆内存,属于GC管辖范围以内。因为须要拷贝,因此速度相对较慢;
  • ByteBuffer.allocateDirect(capability):这种方式是直接分配OS本地内存,不属于GC管辖范围以内,因为不须要内存拷贝因此速度相对较快。

可是若是不断分配本地内存,堆内存不多使用,那么JVM就不须要执行GC,DirectByteBuffer对象就不会被回收。这时候堆内存充足,可是本地内存已经用光了,再次尝试分配的时候就会出现OutOfMemoryError,那么程序就直接崩溃了。

五、java.lang.OutOfMemoryError:unable to create new native thread

​ 准确的说,这一个异常是和程序运行的平台相关的。致使的缘由:

  • 建立了太多的线程,一个应用建立多个线程,超过系统承载极限;
  • 服务器不容许应用程序建立这么多的线程,Linux系统默认的容许单个进程能够建立的线程数量是1024个,当建立多 线程数量多于这个数字的时候就会抛出此异常

如何解决呢?

  • 想办法减小应用程序建立的线程的数量,分析应用是否真的须要建立这么多的线程。若是不是,改变代码将线程数量降到最低;
  • 对于有的应用,确实须要建立不少的线程,远超过Linux限制的1024个 限制,那么能够经过修改Linux服务器的配置,扩大Linux的默认限制。

六、java.lang.OutOfMemoryError:MetaSpace

​ 元空间的本质和永久代相似,都是对JVM规范中的方法区的实现。不过元空间与永久代之间最大的区别在于:元空间不在虚拟机中,而是使用的本地内存。所以,默认状况下,元空间的大小仅仅受到本地内存的限制 。

元空间存放了如下的内容:

  • 虚拟机加载的类信息;
  • 常量池;
  • 静态变量;
  • 即时编译后的代码

模拟MetaSpace空间溢出,咱们不断生成类往元空间里灌,类占据的空间老是会超过MetaSpace指定的空间大小的

查看元空间的大小:java -XX:+PrintFlagsInitial

1三、垃圾收集器

13.一、CMS(“标记-清除”算法, -XX:+UseConcMarkSweepGC(old)

定义:以获取最短回收停顿时间为目标的收集器

13.1.一、运做过程(5大步骤)

一、初始标记: 暂停全部的其余线程(STW),并记录下gc roots直接能引用的对象,速度很快。
二、并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长可是不须要停顿用户线程, 能够与垃圾收集线程一块儿并发运行。由于用户程序继续运行,可能会有致使已经标记过的对象状态发生改变。
三、从新标记: 从新标记阶段就是为了修正并发标记期间由于用户程序继续运行而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到增量更新算法作从新标记。
四、并发清理: 开启用户线程,同时GC线程开始对未标记的区域作清扫。这个阶段若是有新增对象会被标记为三色标记法里面的黑色不作任何处理
五、并发重置:重置本次GC过程当中的标记数据。

在这里插入图片描述

主要优势:并发收集、低停顿。可是它有下面几个明显的缺点:

  1. 对CPU资源敏感(会和服务抢资源);
  2. 没法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
  3. 它使用的回收算法-“标记-清除”算法会致使收集结束时会有大量空间碎片产生,固然经过参数==-XX:+UseCMSCompactAtFullCollection可让jvm在执行完标记清除后再作整理==
  4. 执行过程当中的不肯定性,会存在上一次垃圾回收还没执行完,而后垃圾回收又被触发的状况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收

CMS的相关核心参数

  1. -XX:+UseConcMarkSweepGC:启用cms
  2. -XX:ConcGCThreads:并发的GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:FullGC以后作压缩整理(减小碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC以后压缩一次,默认是0,表明每次FullGC后都会压缩一次
  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),若是不指定,JVM仅在第一次使用设定值,后续则会自动调整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减小老年代对年轻代的引用,下降CMS GC的标记阶段时的开销,通常CMS的GC耗时 80%都在标记阶段
  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
  9. -XX:+CMSParallelRemarkEnabled:在从新标记的时候多线程执行,缩短STW;

13.1.二、三色标记法

黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的全部引用都已经扫描过。 黑色的对象表明已经扫描过, 它是安全存活的, 若是有其余对象引用指向了黑色对象, 无须从新扫描一遍。 黑色对象不可能直接(不通过灰色对象) 指向某个白色对象。

灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用尚未被扫描过。

白色: 表示对象还没有被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 全部的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即表明不可达。

在这里插入图片描述

13.1.三、concurrent model failure(浮动垃圾)

在并发标记过程当中,若是因为方法运行结束致使部分局部变量(gcroot)被销毁,这个gcroot引用的对象以前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部份内存。这部分本应该回收可是没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是须要等到下一轮垃圾回收中才被清除。另外,针对并发标记(还有并发清理)开始后产生的新对象,一般的作法是直接所有当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。
  • 1

13.1.四、background/foreground collector

-XX:ConcGCThreads=4和-XX:+ExplicitGCInvokesConcurrent开启foreground CMS GC,CMS gc 有两种模式,background和foreground,正常的cms gc使用background模式,就是咱们平时说的cms gc;当并发收集失败或者调用了System.gc()的时候,就会致使一次full gc,这个fullgc是否是cms回收,而是Serial单线程回收器,加入了参数 -XX:ConcGCThreads=4 后,执行full gc的时候,就变成了CMS foreground gc,它是并行full gc,只会执行cms中stop the world阶段的操做,效率比单线程Serial full GC要高;须要注意的是它只会回收old,由于cms收集器是老年代收集器;而正常的Serial收集是包含整个堆的,加入了参数==-XX:+ExplicitGCInvokesConcurrent==,表明永久带也会被cms收集;

13.1.五、为何G1用SATB?CMS用增量更新?

SATB相对增量更新效率会高(固然SATB可能形成更多的浮动垃圾),由于不须要在从新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会作深度扫描,G1由于不少对象都位于不一样的region,CMS就一块老年代区域,从新深度扫描对象的话G1的代价会比CMS高,因此G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。

13.1.六、漏标-读写屏障(解决方案)

13.1.6.一、增量更新(Incremental Update)+写屏障

增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束以后, 再将这些记录过的引用关系中的黑色对象为根, 从新扫描一次。 这能够简化理解为, 黑色对象一旦新插入了指向白色对象的引用以后, 它就变回灰色对象了。

  • 写屏障实现增量更新

当对象A的成员变量的引用发生变化时,好比新增引用(a.d = d),咱们能够利用写屏障,将A新的成员变量引用对象D
记录下来:
void post_write_barrier(oop* field, oop new_value) {
remark_set.add(new_value); // 记录新引用的对象
}

13.1.6.二、原始快照(Snapshot At The Beginning,SATB)+写屏障

原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束以后,再将这些记录过的引用关系中的灰色对象为根, 从新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候从新扫描,这个对象也有多是浮动垃圾)

以上不管是对引用关系记录的插入仍是删除, 虚拟机的记录操做都是经过写屏障实现的。

  • 写屏障实现SATB

    当对象B的成员变量的引用发生变化时,好比引用消失(a.b.d = null),咱们能够利用写屏障,将B原来成员变量的引用
    对象D记录下来:
    void pre_write_barrier(oop* field) {
    oop old_value = *field; // 获取旧值
    remark_set.add(old_value); // 记录原来的引用对象
    }

13.1.6.三、并发标记时对漏标的处理方案

CMS:写屏障 + 增量更新
G1,Shenandoah:写屏障 + SATB
ZGC:读屏障

工程实现中,读写屏障还有其余功能,好比写屏障能够用于记录跨代/区引用的变化,读屏障能够用于支持移动对象的并发执行等。功能以外,还有性能的考虑,因此对于选择哪一种,每款垃圾回收器都有本身的想法。

13.1.七、promotion failed

这个异常发生在年轻带回收的时候;
在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下形成的,多数是因为老年带有足够的空闲空间,可是因为碎片较多,新生代要转移到老年带的对象比较大,找不到一段连续区域存放这个对象致使的,

13.1.八、过早提高和提高失败

在 Minor GC 过程当中,Survivor Unused 可能不足以容纳 Eden 和另外一个 Survivor 中的存活对象, 那么多余的将被移到老年代, 称为过早提高(Premature Promotion),这会致使老年代中短时间存活对象的增加, 可能会引起严重的性能问题。 再进一步, 若是老年代满了, Minor GC 后会进行 Full GC, 这将致使遍历整个堆, 称为提高失败(Promotion Failure)。

13.1.九、早提高的缘由

  1. Survivor空间过小,容纳不下所有的运行时短生命周期的对象,若是是这个缘由,能够尝试将Survivor调大,不然端生命周期的对象提高过快,致使老年代很快就被占满,从而引发频繁的full gc;
  2. 对象太大,Survivor和Eden没有足够大的空间来存放这些大象;

13.1.十、提高失败缘由

当提高的时候,发现老年代也没有足够的连续空间来容纳该对象。
为何是没有足够的连续空间而不是空闲空间呢?
老年代容纳不下提高的对象有两种状况:

  1. 老年代空闲空间不够用了;

  2. 老年代虽然空闲空间不少,可是碎片太多,没有连续的空闲空间存放该对象;

    解决方法

    1. 若是是由于内存碎片致使的大对象提高失败,cms须要进行空间整理压缩;
    2. 若是是由于提高过快致使的,说明Survivor 空闲空间不足,那么能够尝试调大 Survivor;
    3. 若是是由于老年代空间不够致使的,尝试将CMS触发的阈值调低

13.二、G1

定义:面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高几率知足GC,停顿时间要求的同时,还具有高吞吐量性能特征

13.2.一、运做流程

G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多能够有2048个Region。
通常Region大小等于堆大小除以2048,好比堆大小为4096M,则Region大小为2M,固然也能够用参数"-XX:G1HeapRegionSize"手动指定Region大小,可是推荐默认的计算方式。
G1保留了年轻代和老年代的概念,但再也不是物理隔阂了,它们都是(能够不连续)Region的集合。
默认年轻代对堆内存的占比是5%,若是堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,能够经过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增长更多的Region,可是最多新生代的占比不会超过60%,能够经过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟以前同样,默认8:1:1,假设年轻代如今有1000个region,eden区对应800个,s0对应100个,s1对应100个。
一个Region可能以前是年轻代,若是Region进行了垃圾回收,以后可能又会变成老年代,也就是说Region的区域功能
可能会动态变化。G1垃圾收集器对于对象何时会转移到老年代跟以前讲过的原则同样,惟一不一样的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的断定规则就是一个大对象超过了一个Region大小的50%,好比按照上面算的,每一个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,并且一个大对象若是太大,可能会横跨多个Region来存放。

Humongous区专门存放短时间巨型对象,不用直接进老年代,能够节约老年代的空间,避免由于老年代空间不够的GC开销。
Full GC的时候除了收集年轻代和老年代以外,也会将Humongous区一并回收。

G1收集器一次GC的运做过程大体分为如下4个步骤:

  • 初始标记(initial mark,STW):暂停全部的其余线程,并记录下gc roots直接能引用的对象,速度很快 ;

  • 并发标记(Concurrent Marking):同CMS的并发标记

  • 最终标记(Remark,STW):同CMS的从新标记

  • 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的==回收价值和成本进行排序,根据用户所指望的GC停顿时间(能够用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,==好比说老年代此时有1000个Region都满了,可是由于根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么经过以前回收成本计算得知,可能回收其中800个Region恰好须要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽可能把GC致使的停顿时间控制在咱们指定的范围内。这个阶段其实也能够作到与用户程序一块儿并发执行,可是由于只回收一部分Region,时间是用户可控制的,并且停顿用户线程将大幅提升收集效率。无论是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另外一个region中,这种不会像CMS那样回收完由于有不少内存碎片还须要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。(注意:CMS回收阶段是跟用户线程一块儿并发执行的,G1由于内部实现太复杂暂时没实现并发回收,不过到了Shenandoah就实现了并发收集,Shenandoah能够当作是G1的升级版本)

在这里插入图片描述

==G1收集器在后台维护了一个优先列表,每次根据容许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),好比一个Region花200ms能回收10M垃圾,另一个Region花50ms能回收20M垃圾,在回收时间有限状况下,G1固然会优先选择后面这个Region回收。==这种使用Region划份内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内能够尽量高的收集效率。

被视为JDK1.7以上版本Java虚拟机的一个重要进化特征。它具有如下特色:

  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优点,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其余收集器本来须要停顿Java线程来执行GC动做,G1收集器仍然能够经过并发的方式让java程序继续执行。
  • 分代收集:虽然G1能够不须要其余收集器配合就能独立管理整个GC堆,可是仍是保留了分代的概念。
  • 空间整合:与CMS的“标记–清理”算法不一样,G1从总体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
    可预测的停顿:这是G1相对于CMS的另外一个大优点,下降停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能创建可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片断(经过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。

毫无疑问, 能够由用户指按期望的停顿时间是G1收集器很强大的一个功能, 设置不一样的指望停顿时间, 可以使得G1在不一样应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。 不过, 这里设置的“指望值”必须是符合实际的, 不能异想天开, 毕竟G1是要冻结用户线程来复制对象的, 这个停顿时间再怎么低也得有个限度。 它默认的停顿目标为两百毫秒, 通常来讲, 回收阶段占到几十到一百甚至接近两百毫秒都很正常, 但若是咱们把停顿时间调得很是低, 譬如设置为二十毫秒, 极可能出现的结果就是因为停顿目标时间过短, 致使每次选出来的回收集只占堆内存很小的一部分, 收集器收集的速度逐渐跟不上分配器分配的速度, 致使垃圾慢慢堆积。 极可能一开始收集器还能从空闲的堆内存中得到一些喘息的时间, 但应用运行时间一长就不行了, 最终占满堆引起Full GC反而下降性能, 因此一般把指望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。

在这里插入图片描述

13.2.二、Remembered Set(记录集)/Card Table(卡表)

在新生代作GCRoots可达性扫描过程当中可能会碰到跨代引用的对象,这种若是又去对老年代再去扫描效率过低了。
为此,在新生代能够引入记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围。事实上并不仅是新生代、 老年代之间才有跨代引用的问题, 全部涉及部分区域收集(Partial GC) 行为的垃圾收集器, 典型的如G一、 ZGC和Shenandoah收集器, 都会面临相同的问题。
垃圾收集场景中,收集器只需经过记忆集判断出某一块非收集区域是否存在指向收集区域的指针便可,无需了解跨代引用指针的所有细节。
hotspot使用一种叫作“卡表”(cardtable)的方式实现记忆集,也是目前最经常使用的一种方式。关于卡表与记忆集的关系,能够类比为Java语言中HashMap与Map的关系。
卡表是使用一个字节数组实现:CARD_TABLE[ ],每一个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”。
HotSpot使用的卡页是2^9大小,即512字节。

一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,不然为0。
GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。

卡表如何维护?

卡表变脏上面已经说了,可是须要知道如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为1。Hotspot使用写屏障维护卡表状态。

13.2.三、Collect Set

Collect Set(CSet)是指,在Evacuation阶段,由G1垃圾回收器选择的待回收的Region集合。G1垃圾回收器的软实时的特性就是经过CSet的选择来实现的。对应于算法的两种模式fully-young generational mode和partially-young mode,CSet的选择能够分红两种:

  1. 在fully-young generational mode下:顾名思义,该模式下CSet将只包含young的Region。G1将调整young的Region的数量来匹配软实时的目标;
  2. 在partially-young mode下:该模式会选择全部的young region,而且选择一部分的old region。old region的选择将依据在Marking cycle phase中对存活对象的计数。G1选择存活对象最少的Region进行回收。

13.2.四、young gc的完整流程

YoungGC并非说现有的Eden区放满了就会立刻触发,G1会计算下如今Eden区回收大概要多久时间,若是回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增长年轻代的region,继续给新对象存放,不会立刻作Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC

13.2.五、Mixed GC的完整流程

不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收全部的Young和部分Old(根据指望的GC停顿时间肯定old区垃圾收集的优先顺序)以及大对象区,正常状况G1的垃圾收集是先作MixedGC,主要使用复制算法,须要把各个region中存活的对象拷贝到别的region里去,拷贝过程当中若是发现没有足够的空region可以承载拷贝对象就会触发一次Full GC

13.2.六、Full GC

中止系统程序,而后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是很是耗时的。(Shenandoah优化成多线程收集了)

13.2.七、Marking bitmaps/TAMS

Marking bitmap是一种数据结构,其中的每个bit表明的是一个可用于分配给对象的起始地址。举例来讲:

img

bitmap

其中addrN表明的是一个对象的起始地址。绿色的块表明的是在该起始地址处的对象是存活对象,而其他白色的块则表明了垃圾对象。
G1使用了两个bitmap,一个叫作previous bitmap,另一个叫作next bitmap。previous bitmap记录的是上一次的标记阶段完成以后的构造的bitmap;next bitmap则是当前正在标记阶段正在构造的bitmap。在当前标记阶段结束以后,当前标记的next bitmap就变成了下一次标记阶段的previous bitmap。

TAMS(top at mark start)变量,是一对用于区分在标记阶段新分配对象的变量,分别被称为previous TAMS和next TAMS。在previous TAMS和next TAMS之间的对象则是本次标记阶段时候新分配的对象。如图:

img

previous TMAS 和 next TAMS

白色region表明的是空闲空间,绿色region表明是存活对象,橙色region表明的在这次标记阶段新分配的对象。注意的是,在橙色区域的对象,并不能确保它们都事实上是存活的。

13.2.八、Pause Prediction Model

停顿预测模型,经过用户设定的GC停顿时间(参数-XX:MaxGCPauseMillis),G1以衰减平均值为理论基础,计算须要回收的Region数量从而进行知足。

13.2.九、G1收集器参数设置

-XX:+UseG1GC:使用G1收集器

    -XX:ParallelGCThreads:指定GC工做的线程数量

    -XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区

    -XX:MaxGCPauseMillis:目标暂停时间(默认200ms)

    -XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)

    -XX:G1MaxNewSizePercent:新生代内存最大空间

    -XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代

    -XX:MaxTenuringThreshold:最大年龄阈值(默认15)

    -XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),好比咱们以前说的堆默认有2048个region,若是有接近1000个region都是老年代的region,则可能就要触发MixedGC了

    -XX:G1MixedGCLiveThresholdPercent(默认85%)  region中的存活对象低于这个值时才会回收该region,若是超过这个值,存活对象过多,回收的的意义不大。

    -XX:G1MixedGCCountTarget:在一次回收过程当中指定作几回筛选回收(默认8次),在最后一个筛选回收阶段能够回收一会,而后暂停回收,恢复系统运行,一会再开始回收,这样可让系统不至于单次停顿时间过长。

    -XX:G1HeapWastePercent(默认5%): gc过程当中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其余Region,而后这个Region中的垃圾对象所有清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会当即中止混合回收,意味着本次混合回收就结束了。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

13.2.十、G1垃圾收集器优化建议( -XX:MaxGCPauseMills=50ms)

​ 假设参数 -XX:MaxGCPauseMills 设置的值很大,致使系统运行好久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。
​ 那么存活下来的对象可能就会不少,此时就会致使Survivor区域放不下那么多的对象,就会进入老年代中。

​ 或者是你年轻代gc事后,存活下来的对象过多,致使进入Survivor区域后触发了动态年龄断定规则,达到了Survivor区域的50%,也会快速致使一些对象进入老年代中。
​ 因此这里核心仍是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc事后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.

13.2.十一、什么场景适合使用G1

  1. 50%以上的堆被存活对象占用
  2. 对象分配和晋升的速度变化很是大
  3. 垃圾回收时间特别长,超过1秒
  4. 8GB以上的堆内存(建议值)
  5. 停顿时间是500ms之内

13.三、ZGC

定义:具备实验性质的低延迟垃圾收集器

13.3.一、主要目标

  • 支持TB量级的堆。咱们生产环境的硬盘尚未上TB呢,这应该能够知足将来十年内,全部JAVA应用的需求了吧。
  • 最大GC停顿时间不超10ms。目前通常线上环境运行良好的JAVA应用Minor GC停顿时间在10ms左右,Major GC通常都须要100ms以上(G1能够调节停顿时间,可是若是调的太低的话,反而会拔苗助长),之因此能作到这一点是由于它的停顿时间主要跟Root扫描有关,而Root数量和堆大小是没有任何关系的。
  • 奠基将来GC特性的基础。
  • 最糟糕的状况下吞吐量会下降15%。这都不是事,停顿时间足够优秀。至于吞吐量,经过扩容分分钟解决。另外,Oracle官方提到了它最大的优势是:它的停顿时间不会随着堆的增大而增加!也就是说,几十G堆的停顿时间是10ms如下,几百G甚至上T堆的停顿时间也是10ms如下。

13.3.二、color poin(颜色指针)

Colored Pointers,即颜色指针,以下图所示,ZGC的核心设计之一。之前的垃圾回收器的GC信息都保存在对象头中,而ZGC的GC信息保存在指针中。

在这里插入图片描述每一个对象有一个64位指针,这64位被分为:

  • 18位:预留给之后使用;
  • 1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能经过finalizer才能访问;
  • 1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示须要GC的
    Region集合);
  • 1位:Marked1标识;
  • 1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC;
  • 42位:对象的地址(因此它能够支持2^42=4T内存):

为何有2个mark标记?

每个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,全部引用都变成未标记。
GC周期1:使用mark0, 则周期结束全部引用mark标记都会成为01。
GC周期2:使用mark1, 则期待的mark标记10,全部引用都能被从新标记。
经过对配置ZGC后对象指针分析咱们可知,对象指针必须是64位,那么ZGC就没法支持32位操做系统,一样的也就没法支持压缩指针了(CompressedOops,压缩指针也是32位)。

颜色指针的三大优点:

  1. 一旦某个Region的存活对象被移走以后,这个Region当即就可以被释放和重用掉,而没必要等待整个堆中全部指向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。
  2. 颜色指针能够大幅减小在垃圾收集过程当中内存屏障的使用数量,ZGC只使用了读屏障。
  3. 颜色指针具有强大的扩展性,它能够做为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便往后进一步提升性能。

读屏障
以前的GC都是采用Write Barrier,此次ZGC采用了彻底不一样的方案读屏障,这个是ZGC一个很是重要的特性。
在标记和移动对象的阶段,每次「从堆里对象的引用类型中读取一个指针」的时候,都须要加上一个Load Barriers。那么咱们该如何理解它呢?看下面的代码,第一行代码咱们尝试读取堆中的一个对象引用obj.fieldA并赋给引用o(fieldA也是一个对象时才会加上读屏障)。若是这时候对象在GC时被移动了,接下来JVM就会加上一个读屏障,这个屏障会把读出的指针更新到对象的新地址上,而且把堆里的这个指针“修正”到本来的字段里。这样就算GC把对象移动了,读屏障也会发现并修正指针,因而应用代码就永远都会持有更新后的有效指针,并且不须要STW。
那么,JVM是如何判断对象被移动过呢?就是利用上面提到的颜色指针,若是指针是Bad Color,那么程序还不能往下执行,须要「slow path」,修正指针;若是指针是Good Color,那么正常往下执行便可:

在这里插入图片描述

❝ 这个动做是否是很是像JDK并发中用到的CAS自旋?读取的值发现已经失效了,须要从新读取。而ZGC这里是以前持有的指针因为GC后失效了,须要经过读屏障修正指针。❞
后面3行代码都不须要加读屏障:Object p = o这行代码并无从堆中读取数据:o.doSomething()也没有从堆中读取数据;obj.fieldB不是对象引用,而是原子类型。
正是由于Load Barriers的存在,因此会致使配置ZGC的应用的吞吐量会变低。官方的测试数据是须要多出额外4%的开销:

在这里插入图片描述

那么,判断对象是Bad Color仍是Good Color的依据是什么呢?就是根据上一段提到的Colored Pointers的4个颜色位。

当加上读屏障时,根据对象指针中这4位的信息,就能知道当前对象是Bad/Good Color了。

PS:既然低42位指针能够支持4T内存,那么可否经过预定更多位给对象地址来达到支持更大内存的目的呢?答案确定是不能够。由于目前主板地址总线最宽只有48bit,4位是颜色位,就只剩44位了,因此受限于目前的硬件,ZGC最大只能支持16T的内存,JDK13就把最大支持堆内存从4T扩大到了16T。

13.3.三、运做过程

在这里插入图片描述

  1. 并发标记(Concurrent Mark):与G1同样,并发标记是遍历对象图作可达性分析的阶段,它的初始标记(Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不一样的是, ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。
  2. 并发预备重分配(Concurrent Prepare for Relocate):这个阶段须要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描全部的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
  3. 并发重分配(Concurrent Relocate):重分配是ZGC执行过程当中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每一个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,若是用户线程此时并发访问了位于重分配集中的对象,此次访问将会被预置的内存屏障(读屏障)所截获,而后当即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
    1 ZGC的颜色指针由于“自愈”(Self‐Healing)能力,因此只有第一次访问旧对象会变慢, 一旦重分配集中某个Region的存活对象都复制完毕后,
    2 这个Region就能够当即释放用于新对象的分配,可是转发表还得留着不能释放掉, 由于可能还有访问在使用这个转发表。
  4. 并发重映射(Concurrent Remap):重映射所作的就是修正整个堆中指向重分配集中旧对象的全部引用,可是ZGC中对象引用存在“自愈”功能,因此这个重映射操做并非很迫切。ZGC很巧妙地把并发重映射阶段要作的工做,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历全部对象的,这样合并就节省了一次遍历对象图的开销。一旦全部指针都被修正以后, 原来记录新旧对象关系的转发表就能够释放掉了。

13.3.四、存在的问题,怎么解决

ZGC最大的问题是浮动垃圾。ZGC的停顿时间是在10ms如下,可是ZGC的执行时间仍是远远大于这个时间的。假如ZGC全过程须要执行10分钟,在这个期间因为对象分配速率很高,将建立大量的新对象,这些对象很难进入当次GC,因此只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。
ZGC没有分代概念,每次都须要进行全堆扫描,致使一些“朝生夕死”的对象没能及时的被回收。

解决方案
目前惟一的办法是增大堆的容量,使得程序获得更多的喘息时间,可是这个也是一个治标不治本的方案。若是须要从根本上解决这个问题,仍是须要引入分代收集,让新生对象都在一个专门的区域中建立,而后专门针对这个区域进行更频繁、更快的收集。

13.3.五、安全点与安全区域

安全点就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是肯定的,这样JVM就能够安全的进行一些操做,好比GC等,因此GC不是想何时作就当即触发的,是须要等待全部线程运行到安全点后才能触发。
这些特定的安全点位置主要有如下几种:

  1. 方法返回以前
  2. 调用某个方法以后
  3. 抛出异常的位置
  4. 循环的末尾

大致实现思想是当垃圾收集须要中断线程的时候, 不直接对线程操做, 仅仅简单地设置一个标志位, 各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就本身在最近的安全点上主动中断挂起。 轮询标志的地方和安全点是重合的。

安全区域又是什么?

Safe Point 是对正在执行的线程设定的。
若是一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。所以 JVM 引入了 Safe Region。
Safe Region 是指在一段代码片断中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。

13.3.六、ZGC参数

在这里插入图片描述

13.3.七、ZGC触发时机

ZGC目前有4中机制触发GC:

一、定时触发,默认为不使用,可经过ZCollectionInterval参数配置。
二、预热触发,最多三次,在堆内存达到10%、20%、30%时触发,主要时统计GC时间,为其余GC机制使用。
三、分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽以前触发GC(耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间)。
四、主动触发,(默认开启,可经过ZProactive参数配置) 距上次GC堆内存增加10%,或超过5分钟时,对比距上次GC的间隔时间跟(49 * 一次GC的最大持续时间),超过则触发。

1四、如何选择垃圾收集器

  1. 优先调整堆的大小让服务器本身来选择

  2. 若是内存小于100M,使用串行收集器

  3. 若是是单核,而且没有停顿时间的要求,串行或JVM本身选择

  4. 若是容许停顿时间超过1秒,选择并行或者JVM本身选

  5. 若是响应时间最重要,而且不能超过1秒,使用并发收集器

  6. 4G如下能够用parallel,4-8G能够用ParNew+CMS,8G以上能够用G1,几百G以上用ZGC

在这里插入图片描述

JDK 1.8默认使用 Parallel(年轻代和老年代都是)
JDK 1.9默认使用 G1

1五、各类命令(例如100%cpu的排查、死锁的检查)

15.一、100%CPU的排查

1 、 使用top命令查看cpu占用资源较高的PID

二、经过jps 找到当前用户下的java程序PID(jps -l 可以打印出全部的应用的PID)

三、使用 pidstat -p

四、找到cpu占用较高的线程TID

五、将TID转换为十六进制的表示方式

六、经过jstack -l(使用jstack 输出当前PID的线程dunp信息)

七、 查找 TID对应的线程(输出的线程id为十六进制),找到对应的代码

15.二、死锁的检查

方法1、使用 jps + jstack

​ 在windons命令窗口,使用 jps -l (找到运行的程序的PID)

​ 使用jstack -l PID(上面的)

方法二:使用jconsole

方法三:使用Java Visual VM

1六、JIT(即时编译器)

JIT是一种提升程序运行效率的方法。一般,程序有两种运行方式:静态编译与动态解释。静态编译的程序在执行前所有被翻译为机器码,而动态解释执行的则是一句一句边运行边翻译。

1七、逃逸分析

逃逸分析是指在某个方法以内建立的对象,除了在方法体以内被引用以外,还在方法体以外被其它变量引用到;这样带来的后果是在该方法执行完毕以后,该方法中建立的对象将没法被GC回收,因为其被其它变量引用。正常的方法调用中,方法体中建立的对象将在执行完毕以后,将回收其中建立的对象;故因为没法回收,即成为逃逸。

相关文章
相关标签/搜索