《深入理解JVM》(上)

Java包括什么
  1. 编程语言
  2. 跨平台的部署支持
  3. 内存管理、访问控制
  4. 热点代码探测
  5. 完善的程序接口 第三方软件库
    JCP(Java社区规定)
  6. Java程序设计语言(JDK)
  7. 各硬件平台的实现(JDK JRE)
  8. class文件格式
  9. Java类库API(JDK JRE)
  10. 第三方类库jdk结构图
Java个各种版本
  1. JavaSE(标准版)
  • 桌面
  1. JavaME
  • 手机APP
  1. JavaEE
  • 企业开发
Java发展史

在这里插入图片描述

大事件
  • 2006.11 Java开源
  • 2009.4 oracle收购sun
  • 2019 oracle声明放弃对之前jdk放弃维护 readhat公司继续对他进行维护
Java虚拟机家族
  1. classic
  • 纯解释的运行Java代码
  • sunwjit 无法与解释权交替工作
  • 句柄的形式操作对象
  1. Exact VM
  • 准确内存管理(某一个位置数值的具体类型是知道的)
  • 知道任意位置对象类型 可以直接进行对对象进行操作
  1. hotspot vm(武林盟主)
  • sun/oracleJDK OpenJDk的默认虚拟机
  • Longview Techenologies(小公司为self语言开发)开发
  • 准确位置分配技术 热点代码探测技术
  1. Embedded VM
  • JavaMe
  1. BEA JRockit(世界上最快的虚拟机)/J9
  • JRockit以及不在发展(服务端 不包含解释器)
  • j9依然活跃
  1. Azul VM
  2. Apache Harmony
  • 自己的api 类库 虚拟机
  • jdk差点产生隔代
    8.Graal VM()
  • Run Programs Faster AnyWhere
  • 跨语言全栈虚拟机
编译器的类型
  1. c1
  • 编译时间短 优化程度低
  1. c2
  • 编译时间长 优化程度高
    2.Graal
  • Java语言编写的
  • 替代C2
向Native迈进

微服务架构下单个服务无需面对 GB 甚至TB的内存 无需追求24小时稳定运行
启动时间较长 需要在运行一段时间后才可以达到最优
比起服务 函数运行的时间会更短
每次运行一个小的程序就需要大量的jre(几百兆)
substrate vm 上在graal vm 0.20 版本出现的极小型的运行环境

  • substrate VM代替hotspot
    • 在运行之前对于可达代码进行分析 将所有用到的代码保存快照
    • 后期的运行结果不需要重新加载整个jvm为graal 的run programs faster anywhere 内存的节约
hotspot的优缺点
  • hotspot 长期以来的进化和升级功能逐渐增多 体积变大
  • 模块之间的纠缠严重 J9这方面不存在这个问题
  • 现在已经开发了大量的借口
  • jdk9 Java语言级别的编译接口 在Java虚拟机外添加处理成为可能
  • jdk10 重新构建了垃圾收集器的接口

JVM运行时数据区

在这里插入图片描述

  1. 程序计数器
  • 当前代码执行的行数 当先线程的行号指示器
  • 程序分支、循环、跳转、异常处理、线程恢复
  • 线程的调度 一个CPU管理多个线程 所以程序计数器是cpu私有的
  • 没有规定OutOfMemoryError
  1. 虚拟机栈
  • 线程私有
  • 方法调用 机会创建一个栈帧用于储存局部变量表、操作数栈和动态链接 方法出口
  • 局部变量表储存空间以卡槽(具体的大小有jvm决定) long double 2个卡槽 int byte char short float 引用类型都是在卡槽当中
  • stackoverflowError
  1. 本地方法栈
  • 本地方法的调度
  1. Java堆
  • 最大的一块共享的内存空间 几乎所有的对象都在这进行内存分配(栈上分配、标量替换)
  • -Xmx -Xms 堆大小进行规定
  • 在共享的区域里 可以划分出多个线程私有的分配缓冲区
  • OutofMemory Error
  • 老年代、新生代的分代方式发生变化
  1. 方法区
  • 线程共享的一片区域

  • 类型信息、常量、静态变量

  • 原先的版本使用永久代实现jvm方法区

  • J9 jRocket等并没有采用这个实现

  • jdk7 字符串常量池和静态变量移出 jdk 8完全放弃永久代该用元空间 剩余的信息主要为类信息转移到堆中

  • 运行时常量池

    • 存放符号引用传到 转变为直接引用也放在常量池当中
  • outOfMemoryError

  1. 直接内存
  • NIO 可以使用native方法直接对内存进行操作
对象分配到内存的方式
  • 是否被加载和初始化
  • 对象分配空间之前大小就是确定的(类加载完成)
  • 如果空间的分配是规整的就可以使用指针碰撞的
  • 如果不是连续的区间就要使用空闲列表
  • 分配的方式有jvm的实现决定
  1. jvm 堆是否规整取决于垃圾回收器是否有整理的功能
  2. 当使用Serial、ParNew压缩垃圾回收器的时候采用指针碰撞
  3. CMS基于标记清除的就只可以使用空闲列表里
  • 对象分配的原子性
    1. 正在创建对象过程第二个对象使用原来的指针
    2. 解决办法
      1. 分配空间同步 CAS加失败重试保证操作原子性
      2. 另外可以为每一个线程在堆分配一块内存 就在TLAB(Thread Local Allocation Buffer) -XX:+/-UseTLAB
  • 分配到的空间(不包括对象头) 全部初始化为0
  • 如果使用TLAB 可以在分配之前完成这一步
  • 构造函数() 方法按照程序猿意愿进行初始化
对象内部的布局
  • 对象头
    • 运行时数据、Hash GC 分代年龄 锁状态 32位(32bit ) 64(64bit)(Mark Word)
    • 类型指针 确定是哪个类的实例 不一定保留 可以通过其它方式知道
  • 实例数据
    • 继承或者自己定义各种类型的都有记录
    • long/double int short char byte/boolean oops
    • 父类会分配在前面
    • -XX:CompactFileds 可以将子内容插入到父内容
  • 填充数据
    • hotspot 虚拟机要求必须是8字节的整数倍
对象定位
  • 使用栈上的reference 指向堆中的对象引用 具体的方式没有声明
  • 访问方式
    • 句柄访问 记录实例数据的地址(对象) 类型数据的地址(类)
    • 直接指针 直接指向对象的地址 对象指向类数据(hotspot 使用)
OutOfMemoryError
  1. 堆溢出
	-Xms20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
	while(true){
		list.add(new Object);
	}
	//java.lang.outOfMemoryError: Java heap space
	

通过查看GCRoot引用链查看导致无法回收的原因

  1. 栈溢出
	-Xoss(本地方法栈)存在但是调整没有任何效果
	-Xss128k
	1.如果允许扩容:OutOfMemoryError
	2.不允许扩容:StackOverflowError(hotspot)
	while(true){
		dfs();
	}
	//java.lang.StackOverflowError
	//创建大量的局部变量也可以导致内存溢出
	//栈帧太大 或者是 虚拟机栈容量过小都会在不支持扩容的情况下 StackoutofMemoryError
	//jdk1.2 classic -oss 
	//多线程的情况下hotspot也会产生内存溢出但是主要是由于物理内存导致的,可以说分配的空间越大越容易产生溢出
	//操作系统对于每一个进程的大小都有限制 window32 2gb 分给方法区和堆后剩余的留下来个栈 每一个线程越大越容易导致内存溢出
	//Java线程在win中说直接映射到无力处理机器上的 越大的内存越容易导致溢出
	//可以通过减少内存来达到解决溢出的情况

-Xss 32位128k 64 >=180k linux 228k

  1. 方法区溢出
	String::intern()
	//如果常量池中有这个数据有返回引用
	//如果没有就创建引用
	//在原先使用永久代实现方法区时候可以使用 -XX:PermSize -XX:MaxPermSize
	//jdk6 不停的创建就会出现 java.lang.OutOfMemoryError:PermGen space
	//jdk7以上常量池放在堆中 //java.lang.OutOfMemoryError:
	String str1=new StringBuilder("计算机").append("科学").toString();
	str1.intern()==str1//jdk7 true jdk6 false
	//jdk7 都在堆空间 返回第一次的应用
	//一个在堆空间 一个在方法区
	String str1=new StringBuilder("ja").append("va").toString();
	str1.intern()==str1 //jdk7 false jdk6 false
	//第一次并不是 stringBuilder所创建的所以不同
	//一个堆 一个在方法区
  • 其它方法导致的溢出 类描述 入口
	//CGLIb 直接操作字节码生成大量的动态类
	//在JDK8后永久代退出历史的舞台
	//方法区主要保存类的描述信息
	-XX:MaxMetaspaceSize=-1
	-XX:MinMetaspaceSize
  1. 本机内存的直接溢出
	-XX:MaxDirectMemorySize=10M
	Unsafe.class.getDeclaredfileds()[0];
	//在使用Dump文件很小的时候 但是方式溢出(NIO)考虑直接内存的使用

垃圾回收策略

  • 垃圾回收的出现早于Java语言的出现
  • 1960Lisp第一门动态垃圾回收语言
  • 程序计数器、Java虚拟机栈、本地方法栈伴随线程出现而出现 消失而消失不需要药考虑垃圾的收集
  • 但是方法区 堆明显是需要的
对象已死
  1. 引用计数法

    1. python使用 对象中添加一个引用计数器 无法很好的结局循环利用(需要大量的处理和判断)
  2. 可达性分析算法

    1. GCRoot 做完起始点 ,寻找引用链 如果被如果对象没有被任何引用链引用的话说明对象不可达 GCRoot的选取
  • 虚拟机栈中的引用 参数 局部变量表 零时变量
  • 方法区 静态变量
  • 方法区 字符串常量池
  • 本地方法栈引用对象
  • jvm内部引用
  • 锁持有的对象
  • 反应jvm内部情况 JMXBean
    垃圾收集器都具有局部收集的功能 但是 必须考虑其他区域的引用情况
引用的类型
  • 强引用
    Object obj=new Object();
    //任何情况下都可能被回收
    
  • 软引用
    //有用但并非必须
    //在内存溢出之前进行二次回收
    
  • 弱引用
    //生存到下一次垃圾收集发生为止
    
  • 虚引用
    //引用是否纯在与他没有关系
    //也无法通过它访问对象
    
判断对象生存还是死亡
  1. 没有与GCRoot关联后 进行一次筛选标记 如果finalize() 没有被重写或者执行过就标记为没有必要执行
  2. 标记为有必要执行 放在F-Queue 队列中但并不保证一定会被执行 最后一次挽救自己机会 例如引用tihs
	@Override
	public viod finalize() throws Throwable{
		super.finalize();
		SVAE=this;
	}
	//SAVE=null;
	//第一次会被救赎 但是第二次的时候就不会再执行了
回收方法区
  • Java规范并没有要求一定要对方法区进行回收
  • 有没有实现 或者没有完全实现方法区回收的垃圾回收器 (JDK11 ZGC)
  • 回收类
    1. 所有实例被回收
    2. 类加载器被回收
    3. 对应的 java.lang.Class 没有被其他地方啊所利用
  • 关于是否要对类信息进行回收(HotSpot虚拟机采用的方法)
    1. -XnoClassgc 是否对类进行回收
    2. -verbose:class 对类记载的过程进行监控

在大量的产生新类的框架(反射 、 动态代理、CGLib) 对于方法区的清理是必须的

垃圾收集算法

目前大多数商业虚拟机的收集器 都是按照分代收集的(分代假说)

  1. 弱分代 朝生夕死亡 (高频回收)
  2. 强分代 熬过多次垃圾回收的对象 难以回收(低频回收)
  • 导致Minor GC Major GC Full GC
  • 回收一个区域的时候对象可能被另外一个区域索引 分代收集的经验法则
  1. 跨代引用假说 跨代引用占同代引用的少数(相互引用的两个对象应该有一样的生命周期)
    • 新生代创建记忆集记录那片区域老年代有引用GCRoot的选择就不用是整个
GC分类
  • Partial GC
    1. MinorGC/YoungGC 新生代GC
    2. MajorGC/OldGC 老年代收集器 CMS
    3. MixedGc 混合回收器 G1
  • Full Gc

垃圾回收算法

1.标记-清除算法(Mark-Sweep)

  • 先对需要/不需要回收对象进行标记,然后对标记/未标记的对象进行回收
  • 缺点
    1. 标记时间长 浪费时间
    2. 产生大量的空间碎片标记-清除算法

2.标记-复制算法

  • 空间平均分 将存活对象复制到另外一边

  • 缺点

    1. 浪费了一半的空间在这里插入图片描述
  • 大多数新生代采用

  1. 研究表明大多数的对象(98%) 朝生夕死 因此不需要1:1 分配
  2. 8:1:1 Eden:Survivor0:Survivor1
  3. 当不满足80%的时候就会使用逃生门

3标记-整理算法

  • 先进行标记 但不是直接删除而是移动到到另外一边标记整理算法
  • 吞吐量关心的Parallel Scavenge使用的标记整理
  • 基于低延迟的CMS却是说基于标记删除
  • 向一边移动而不是对半分 复制到另外一边
GCRoot 的枚举
  • 所有的虚拟机在GCRoot枚举的过程都是STW
  • 可达性分析算法的实现可用于用户线程并发执行
  • 现在主流的JVM都是准确的垃圾回收 有办法直接得到那些地方是引用
    • oopMAp 的数据结构知道哪些地方引用

安全点

  • 可以改变引用的地方有很多不能每一个地方都存放OOPmap 在特定的地方记录引用 这个特定的地方叫做安点
  • 让所有的线程都到安全点进行GC
    1. 抢先式中断
      • 系统进行中断,如果没有到达安全点继续运行直到安全点
    2. 主动式中断
      • 不直接对线程操作 而是设置一个标志位 所有的线程去访问这个标志位 在最近的安全点进行GC
      • 轮训的过程比较频繁 必须高效
      • 解决办法(内存保护陷阱)
        test %eax 0x160100 机器指令
        需要暂停的时候将 0x160100设置为不可读
        test就会出现自陷异常 在之前注册的异常处理器中实现线程的等待
        

保证在执行不会等待较长的时间

安全区域

处理程序不执行的情况

  • 当线程没有分配CPU的时间(sleep Block) 无法响应虚拟机的中断请求
  • 安全区域是一段时间内引用关系不会发生改变
  • 需要枚举完成后才可以离开安全区

记忆集和卡表

解决跨代引用问题

  • 为了避免整个老年代都作为GCRoot的扫描范围引入了记忆集
  • 对象 所有分区收集都需要使用到
  • 记录非收集区域指向收集区域的抽象结构
    • 字长进度 精确到机器字长
    • 对象精度 精确到对象
    • 卡精度 精确到一块内存区域
  • 卡表是一种具体的实现(最简单的形式 一个字节数组)
    • 每一个元素对应一块内存(卡页)
    • 一个卡页中有一个或多个对象就是变脏
    CARD_TABLE[THIS ADDRESS>>9]=0
    //变脏标识为1
    //没有则标识0
    

写屏障

什么时候维护卡表

  • 将维护卡表放在每一个赋值操作中
  • 引用类型字段赋值的AOP操作 (在赋值的前后一个环形通知)在写操作的前后是一个环形 Hotspot 使用的是写后屏障
void oop_field_store(oop* filed,oop new_value){
	*filed=new_value;
	//写后屏障,在这完成卡表更新
	post_write_barrier(filed,new_value);
}
  • 写后屏障浪费时间 还有伪共享的问题
  • 缓冲行
    • 现在中央处理器以缓存行作为单位存储单元
  • 缓存行64字节
    • 一个卡表一个字节 64个卡表在一起 32kb(64*512字节)多个线程同时操作这一个区域就会导致伪共享
    • 解决办法不采用无条件的读后屏障只有没有标记的时候才将他标记为脏
    if(CARD_TABLE[this address>>9]!=0)
    	CARD_TABLE[this address>>9]=0
    
并发的可达分析
  • 通过并发标记判断是否使用过 三种颜色
    1. 白色 未被垃圾收集器访问过 分析结束后还是白色就是垃圾
    2. 黑色 已经访问过并且所有对象引用都已经访问过(不能直接指向白色对象)
    3. 灰色 引进访问 但是不是所有的对象引用都访问过
      并发修改的过程导致黑色直接指向白色 然而被黑色指向的不用扫描导致对象失了
      必须同时产生才会导致对象消失
      • 插入一条或多条黑色到白色的引用
      • 删除所有灰色到该白色的引用
    • 解决办法
      1. 增量更新 (新的白色指向 记录下来将这些黑色为根重新扫描一遍)
      2. 原始快照 记录灰色段点重新扫描 (不管断不断都按之前的扫描)

经典的垃圾收集器

hotspot虚拟机经典垃圾收集器
垃圾收集器没有好坏只有适应的场景的不同

1.Serial 收集器
  • 最古老的单线程的垃圾回收器 只可以使用一条线程进行垃圾回收 垃圾回收时候一直到垃圾回收结束STW(Stop The World) 后天JVM控制 用户不可控制
  • 在这里插入图片描述
  • 各种JVM垃圾回收器(serial、Paralle、CMS 、G1)都无法彻底消除STW
  • 默认客服端下的收集器
  • 额外消耗内存最小的。单核或者核心数较小的垃圾回收器 不用线程的交互专心的垃圾回收提高效率
  • 10~100ms 的延迟 在客户端是一个很好的选择
2.ParNew
  • Serial的多线程版本 除同时使用多条线程(并行)回收外(参数控制、收集算法、STW)都是一模一样的
  • 在这里插入图片描述
  • JDK7之前是服务端首选的垃圾回收器
    • 除了Serial 之外 ParNew是唯一一款与CMS(第一款并发虚拟机)配合使用的垃圾回收器
    -XX:+UseConcMarkSweepGC
    -XX:+/-UseParNewGC
    
    • CMS的出现成就了ParNew
    • G1作为CMS的替代者(全堆收集器)
    • 取消了 Serial+CMS / Serial old+parNew
并行与并发
  • 并行:多条垃圾回收器之间的关系
  • 并发:垃圾收集线程与用户线程之前的关系
3.Paralle Scavage
  • 新生代基于标记复制算法的并行 多线程垃圾回收器(与ParNew相似)
  • CMS(关注停顿时间和响应时间 快速的响应客户) Paralle Scavage(可控的吞吐量 时间固定执行更多的运算)
    • 吞吐量=运行用户代码的时间/用户代码时间+垃圾回收的时间
    • -XX:MaxGCPauseMilis=(int) //最大停顿时间
      	//停顿时间是以吞吐量为代价的
      	//单位停顿的时间少了清除的空间就少了,吞吐量就下降了
      -XX:MaxGCRatio=(0<int<100)//直接设置吞吐量大小
      	//垃圾收集时间占总时间的比例 (吞吐量的倒数)
      	//1/(1+x) 19 1/20 5%
      -XX:+UseAdaptiveSizePolicy
      	//-Xmn、-XX:SurvivorRatio Eden/Survivor的比例  -XX:PretennureSizeThreshold 晋升老年代大小 自动调节
      	//只需要调剂好-Xmn -XX:MaxPauseMillis -XX:GCTimeRatio
      
    • 吞吐量优先的垃圾收集器
4. Serial Old
  • 标记整理算法的单线程垃圾回收器
  • Serial 的老年版本 JDK5之前配合parallel Scavage
  • CMS失败的后备方案
5. Paralle Old
  • Paralle Scavage的老年代版本
  • 并行垃圾回收器
  • 在它出现之前(JDK<1.6)只有Serial Old可以和他配合使用拖累了Paralle Scavage 的速度
  • ParNew+CMS(吞吐量)的组合要比 Paralle Scavage 好
  • 注重吞吐量 或 处理器资源较为稀缺使用
  • 在这里插入图片描述
6. CMS
  • 最短停顿时间为目标的垃圾回收器(B/S架构追求低停顿)
  • 标记-清除 算法
    1. 初始标记(STW)标记一下与GCRoot关联的对象
    2. 并发标记 从GCRoot开始并发标记整个对象图
    3. 重新标记 (SWT) (增量更新:导致变动那一部分重新标记)
    4. 并发清除 并发清除垃圾
    5. CMS垃圾回收器的运行过程
    6. 并发低延迟垃圾回收器(第一款并发垃圾回收器)
  • 三个缺点
    1. 资源非常敏感(所有并发设计程序)
      • 咋用一部分线程导致程序变慢降低吞吐量 (处理器线程+3)/4 线程用于垃圾回收 处理器小于4核的时候非常吃力
      • 解决办法:使用增量的垃圾回收器(i-CMS) 交替执行线程(7 过时 9 移除)
    2. 无法处理浮动垃圾(并发收集的过程中产生的垃圾无法处理)
      • 失败后进行一次FUll GC(STW)
      • 收集的过程也还在运行 必须预留空间给程序继续运行 (JDK5 68%)(JDK6 92%)
      • 	-XX:CMSInitiatingOccu-pancyFraction //提高促发回收的百分比
        
      • 但是如果预期值太高就会并发回收失败从而导致FULLGC
    3. 基于标记-清除算法
      1. 在垃圾分配的时候会产生大量的碎片 如果存在大对象即使还有很多零碎的空间也不得不FULLGC
      -XX:+UseCMSCompactAtFullCollection //在FULLGC的时候开启碎片整理(<JDK9默认开启) 在ZGC 和shenandoah之前是无法并发处理的必然STW
      -XX:CMSFullGCsBefore //在执行若干次不整理的FULLGC后进行一次FUll GC
      
      永久代调优: 如果永久代要进行垃圾回收,就会进行Full GC,CMS默认不会处理永久代中的垃圾需要通过参数-XX:+CMSPermGenSweepingEnabled开启对方法区的收集,开启后会有专门的一组线程对永久代进行垃圾回收,同时还需要开启另一个参数-XX:+CMSClassUnloadingEnabled,使得在垃圾收集时可以卸载不用的类。
7.Garbage First
  • 简称G1
  • 开创了面向局部收集的基于Region的垃圾回收器

JDK6 Update14 Early Access(开发人员实验使用) JDK7 Update4移除了Experimental
JDK8 Update40 G1提供类卸载的支持 成为全功能的垃圾回收器

  • 他的出现是为了替代JDK5的CMS垃圾回收器
  • JDK9开始G1成为替代Paralle Scavage+Paralle的组合 CMS沦落为不推荐
-XX:+UseConcMarkSweepGC //JDK9下使用CMS垃圾回收器
//会发出已经淘汰了的警告
1.由于HotSPot历史版本原因
	1.CMS与内存管理、执行、编译、监控 都有千丝万缕的关系并不符合职责分工的原理
	2.JDK10 提出统一接口 将内存的回收等行为分开CMS也为基于这套接口等实现 这算是为CMS退出历史舞台的铺垫了
  • G1的目标是做一款停顿时间模型(在一定时间程度(m ms)的范围了垃圾回收器的时间为 n ms)的收集器 Java软实时的垃圾回收器 实现方法:
  • 回收的范围再不是也代为范围 而是垃圾最多 回收利益最大的Region(Mix Gc)
  • 也分代 但是不在坚持固定大小的分代 每一个Region可以扮演不同的区 垃圾回收器根据分区进行回收 对所有的收集都有好处
  • Humongous Region 大对象的存储Region 超过Region容量的一半就可以认为是大对象
-XX:G1HeapReagionSize=(1M~32M)//超过Region的区域可以放在几个连续的区间看作是一个Region
  • 分代不在固定 而是不需要连续的动态集合
  • 收集可以建立能预测的时间模型 是因为在后台建立一个优先列表按照优先程度进行收集达到用户设置的时间停顿
-XX:MaxGCPauseMilis //指定停顿时间

Region分布示意图

  • 化整为零需要解决的问题
    1. 记忆集复杂很多(G1本质是hash标 key region其实地址 value 卡表索引 双向 传统卡表是为指向谁 这种结构还记录了谁指向我)
    2. 采用原始快照的算法(SATB)
    3. Region设计了TAMS(Top at Mark Start)指针 并发收集新对象的分配必须在这两个地址上 收集的过程默认它上存活的
    4. 为了时间可控调整时间(衰减均值)
      • 记录每个Region的回收耗时
      • 脏卡数量的平均 2者平均值 标准差
      • 什么是衰减均值
        最新的平均状态(越新的状态越能反应回收的价值)
  • 不关注写屏障记忆集的操作 关键步骤
    1. 初始标记 标记与GCRoot直接关联的对象 修改TAMS为新对象分配(短暂暂停)
    2. 并发标记 扫描整个堆对象图 重新处理TAMS在并发标记的改变 (并发执行)
    3. 最终标记 处理少量SATB (暂停)
    4. 筛选回收 按照维护优先级 将存活对象复制(导致暂停)到空的Region 然后清除 (暂停)
  • 并非纯粹的追求低延迟(除并发标记都暂停)而是延迟可控下尽可能高的吞吐
    G1收集示意图
  • 特点在给出规定延迟时间后 取得吞吐量喝延迟之间的平衡 期望值在几十到200 都合理 但是记住延迟十靠牺牲吞吐量得来的 如果回收的时间过于不合理就会导致FUllGC 得不偿失
  • G1开始追求的是垃圾收集敢得上对象分配而不是将堆空间清理干净
  • G1 与 CMS (追求可控低延迟)
    1. G1优点 可控最大停顿时间 分Region 动态收益确定回收
    2. CMS 标记-删除 G1 整体 标记-整理 Region之间 标记-复制 因此G1不会产生空间碎片
    3. G1产生的内存占用 执行额外负载都要大于CMS
    4. G1所有Region都有记忆集(占堆空间>20%)CMS只有唯一一份卡表(处理老年代到新生代的引用)
    5. CMS 采用写后屏障维护卡表
    6. G1除使用写后屏障处理卡表 为了实现SATB(原始快照)还需要写前屏障跟踪并发指针的变化情况 避免在最终标记停顿时间过长 但是却是额外开销 G1不得不使用类似消息队列来进行屏障的处理。6~8G内存是按作者经验是G1 与CMS的大概分界线 大内存下G1 表现会大于CMS

完美难以实现,但是我们可以把它客观的描述出来

低延迟的垃圾回收器
  • 不可能三角
    1. 内存占用 吞吐量 延迟
    2. 一款优秀的垃圾收集器最多满足其中两项
    3. 内存越大 - 吞吐量越高 -延迟越大
      CMS 使用标记清除算法避免整理的STW 但是大量的碎片避免不了FULL GC 最终STW
      G1 回收粒度更小 整理出现停顿但是避免过长的停顿
      各回收器之前的关系

Shenanhoah,ZGC 只有初始标记 和最终标记 有短暂的停止 但与堆空间的大小无关 (可以在任意大小的堆空间进行回收)(ZGC只可以在4TB以内的堆)

7. Shenandoah收集器
  1. OracleJDK12 不包含
  2. Redhat开发的任何堆内存下停顿时间10ms以内的垃圾回收
    1. 并发的垃圾标记
    2. 并发的垃圾删除后的整理
  3. 更像G1的继承人
    • 相似的内存布局初始标记
    • 并发标记阶段都有高度的相似
    • G1就是合并了Shenandoah部分代码才有了多线程版本的逃生门FULL GC
  • 与G1 相同点 采用Region 有Humongous Region

  • (目前)不使用分代收集 不是没有意义而是基于工作量的考虑将它放在低优先级

  • 屏蔽G1中浪费大量空间的记忆集 采用连接矩阵 全局数据结构解决Region之间的关系

    • 降低了记忆集的消耗
    • 解决伪共享的问题
    • 二维表 在相互引用之间打上✅
    • 连接矩阵
  • 垃圾收集的过程

    1. 初始标记 与GCRoot直接关联的对象 (STW)
    2. 并发标记 遍历对象图
    3. 最终标记 G1 一样STAB扫描 统计高价值的Region (STW)
    4. 并发清除 清除连一个垃圾都没有的对象
    5. 并发回收 不冻结线程移动对象 为了解决原先的指针还指向老对象的位置 引入读屏障(Brook Pointers)转发指针解决
    6. 初始引用更新 之前指向老地址的指针指向新的地址 建立线程集合点 确保回收线程已经分配给他对象移动任务(非常短的停顿)(STW)
    7. 并发引用更新 真正开始引用更新 不沿对象图搜索 按照 物理地址线性搜索
    8. 最终引用更新 解决堆中的引用更新 还有GCRoot引用更新 (最后一次停顿)(STW)
    9. 并发清除 所有的Region都没有对象了Shenandoah垃圾收集器工作过程
转发指针 (Brooks Pointer)
  1. 之前通过并发标记通过移动对象原有的内存上设置保护陷阱 一旦访问 就会出发异常处理 将指针指向新的对象 (会导致用户态和核心态的转换)
  2. Brook采用在对象前面统一添加一个字段 未移动时指向自己
  3. 所有对象的访问会产生一次额外的转向开销
mov r13,QWORD PTR [r12+r14*8-0x8]
  • 对象的移动只需要稍微改变一下指针的值
  • 在并发操作的过程修改必须是在新的对象那个上 如果2 发生在13之间修改就会发生在老对象上
    1. 收集器线程复制了新的对象
    2. 用户线程更新了字段
    3. 收集线程更新抓发指针到新的引用值为副本
  • 必须采用同步措施 让收集线程和用户线程只有一个可以成功
  • 对象移动的关键性不得不采用读写屏障解决
  • 为了实现Brook Pointer 转发指针不得不采用读写屏障其中加了转发处理
  • 第一次采用读写屏障
  • Redhat 公司为了提高Shenandoah 的收益范围 将它Backport到JDK11 甚至JDK8
7. ZGC垃圾回收器
  • Z Garbage Collector
  • JDK11中具有实验性质的垃圾回收器
  • 都是追求在对吞吐量影响不大的情况下将延迟控制在10ms以内
  • ZGC 像是PGC C4的同胞兄弟
  • 基于Region(Page、ZPage)分布、(暂时)不设置分代、使用了读屏障、染色指针内存多重映射 实现并发的标记整理算法
  • Region 动态性 动态的创建和销毁 动态的区分容量的大小 容量可以分类
    1. 小型Region 固定2MB 放置小于256KB的小对象
    2. 中型Region 固定32MB 用于存放(256kb<=4MB)的对象
    3. 大型Region 容量不固定 可以动态改变必须是2MB的整数倍 存放4M以上的对象 他的大小完全可以小于中型号Region 但是>4MZGC的内存分布
  • Shenandoah使用了(转发指针和读屏障实现并发整理) ZGC同样使用了读屏障
  • 使用了染色指针(Tag Pointer)
    1. 之前要需要在对象头储存一些对象头的额外信息JVM或者收集器使用(hash码、分代、锁)
    2. 通过指针或者对象无关的的地方得到对象的信息(对象是否移动过)
      • 对象不能访问
      • 追踪式算法只和指针打交道而已对象本身无关(对象打上三种颜色标记)

    对象只有引用关系能够确定它能否存活 对象上的其它属性都无法影响判断结果

  • hotspot虚拟机几种实现标记的的方案
    • 对象头(Serial)
    • 独立数据结构 (G1 Shenandoah使用相当于堆内存的1/64 BitMAp)
    • ZGC染色指针(直接将少量信息放在指针上的技术)是最纯粹、最直接 直接把标记信息放在引用对象的指针上
      可达性分析算法说是遍历图还不如说是遍历引用图来标记引用
  • 为什么指针也可以存数据
    1. 理论16EB 但基于需求 性能(地址转换的叶表多) 成本(消耗晶体管) 目前64位最大内存 256TB 64Linux (47)128TB进程 46(64TB )物理地址空间 64windwos 44位(16TB)的运行空间
    2. Linux46位指针可用 将高4位取出储存4个标记信息 通过这些标识位可以直接看出对象的三色标记状态,是否进入重新分配集(就是对象是否被移动过)、是否只可以通过finalize方法进行访问 当然这进一步压缩了原来只有的46指针 这就是为什么ZGC管理内存不超过4TB (2的42次)
      染色指针示意图
      1EB=1024PB 1PB=1024TB
  • 不支持32位操作系统
  • 不支持压缩指针
-XX:+UseCompressedOops
  • ZGC的设计者对于染色指针的3大优势
    1. 主要对象被移动后就可以立即释放和重用Shenandoah垃圾回收器必须等到引用更新阶段才可以重新利用(在极端(所有对象都存活)的情况下)需要1:1
    2. 可以大幅度的减少内存屏障目前没有用到任何写屏障 只是使用了读屏障 (一部分是染色指针、一部分现在不存在分代 )内存屏障纯在性能问题
    3. 染色指针可以作为一种拓展的数据结构 对象标记、重定位(开发没有使用的高18位可以提高利用 也可以解决4TB的瓶颈)
  • 面对问题
    1. JVM是一个普通的进程 随便改动定义内存中指针的其中几位操作系统是否支持
    2. Solaris/SPARC 本身支持虚拟地址掩码 设置之后可以直接忽略标志位
    3. x86 采用内存映射技术
      • 在原来x86 相同所有的进程使用一块地址空间 进程不能隔离相互污染后只可以恢复系统
      • Intel80386开始保护模式 386处理器32条地址寻址线都有效 将物理地址分成大小相同的片“页” 这些片映射到物理内存 地址和物理地址是一对多的关系
      • Linux/x86-64 多个虚拟地址映射到一片物理地址 ZGC在虚拟内存看到要比堆空间大把染色指针看作地址分段符 经过多重映射转换后就可以寻址了多重映射指针
      • 并不是为了额外的好处而是染色指针的产物
ZGC垃圾回收的过程
  1. 并发标记 前后也需要经过初始标记和最终标记(短暂的暂停) ZGC的标记上在指针上 (G1 Shenandoah 是在对象上 标记阶段更新 染色指针Marked0 Marked1)
  2. 并发预备重分配 统计需要收集的Region 组成重分配集(Relocation Set) JDK12 开始的类卸载也是在这个阶段开始的
    • 与Collection Set 的区别
    • ZGC是全堆扫描,避免了记集 真是确定会复制里面的对象到其它Region而不是只是清除对应的Region
  3. 并发重分配 重分配集合存活对象复制到新的Region 为重分配集合没一个维护一张转发表记录老对象到新对象的转发过程
    自愈:由于染色指针的支持 可以直接知道一个对象是否处于重分配集 如果位于重分配集合就会被内存屏障所截获 并且使用转发表修正更新指针
    由于染色指针的纯在对象复制完成之后这些Region就可以立刻重新分配 (转发表还得留下来)这些剩下的对象可以自愈
  4. 并发重新映射 修理整个堆中的指向重分配对象的指针 (但由于自愈)并不迫切 合并到了下一次并发标记省异常对象图遍历 之后转发表就可以清楚

短暂到暂停只与GCRoot有关 与堆大小无关

分析各GC之间
  • G1使用读屏障维护卡表 卡表占用大量堆内存 才可以处理跨代引用指针 得以实现增量平均回收垃圾

  • ZGC没有记忆集 连分代也没有连CMS那种只记录新生代和老年代代堆记忆集也没有 完全没有用到写屏障

  • 由于ZGC不采用分代收集 整个并发收集过程产生的浮动垃圾(回收的区间持续小于浮动垃圾就会导致占满)
    NUMA 非统一内存访问架构:
    摩尔定律的逐渐失效 处理器往多核心发展 北桥芯片的内存管理也被集成每个处理器核心所在的裸晶都有自己的内存 要访问其它核心的内存就必须使用Inter-Connect 通道完成 要比访问本地内存慢很多 在NUMA架构下ZGC会请求当前线程所在的处理器本地申请内存 ZGC之前只有Parallel Scavenge 支持NUMA 内存分配

ZGC 延迟 吞吐量 秒掉G1 吞吐量达到 Parrallel Scavenge 99% Parralle Scavenge是吞吐量最高的垃圾回收器

选择合适的垃圾收集器

Epsilon收集器
  1. 不能够进行垃圾收集的收集器(JDK11)(RedHat)
  2. 垃圾收集 对象分配 解释器、编译器的协作 (堆管理和对象的分配是必须的)
  3. JDK10开始提出垃圾收集器提供统一的接口
  4. 在微服务的趋势下一个程序可能只需要运行很短的时间 根本无需垃圾收集这个时候无操作的垃圾收集器就显示得格外重要
  • 选择垃圾收集器的重要因素
  1. 关注的内容 快速运算出结果 关注吞吐量 B/s 关注低延迟 客服端嵌入式 垃圾收集的内存占用不可忘记
  2. 基础设施 Linux Solaris Windows
  3. JDK 的发行商是哪个
  • 回收DEmo
  1. 预算充足 没有太多的调优经验 一套商用的技术软件硬件支持是不错的 Azul Vega Zing VM都是这方面的代表就可以使用传说中的C4垃圾回收器
  2. 不能使用商业解决方案 关注低延迟 能够掌握软件型号可以使用比较新的ZGC
  3. 如果在windows下就与ZGC无缘了 Shenandoah是不错的选择
  4. 比较落后的系统 4-6 G以下CMS 以上使用G1
日志处理框架
  • JDK9 之前没有统一的框架处理 日志开关 、日志级别、循环日志大小、输出格式、重定向都要单独处理
  • JDK9 后这种混乱的局面得到解决 所有的参数都归到-Xlog参数上
-Xlog[:[selector][:[output]][:[decorators]][:[output-options]]]
  • Selector 由标签(TAG) 和(Level)日志级别组成 tag 表示需要哪些模块输出 gc表示垃圾收集器 收集器日志只是大多数中的一种
    add,age,alloc,annotation,aot,arguments,attach,barrier,
    biasedlocking,blocks,bot,breakpoint,bytecode …

  • 日志级别 从高到低 Trace Debug Info Warning Error Off 上 默认级别为info 默认的规格格式和Log4j SLF4J 一样 可以使用decorator 来指定日志的内容

    • time 当前的时间日期 uptime jvm运行时间 pid 进程ID tid 线程ID level 级别 tags 日志输出的标签 如果不指定默认 uptime level tags
演示使用GC日志框架的过程
  1. JDK9之前查看GC信息使用-XX:+PrintGC 之后使用 -Xlog:gc:
	//查看GC信息
	java -Xlog:gc GCTest
	//详细GC信息
	-XX:PrintGCDetails
	java -Xlog:gc* GCTest
	//查看GC前后堆方法区的变化
	-XX:+PrintHeapAtGC
	java -Xlogs:gc+heap=debug GCTest
	//查看线程并发时间与停顿时间
	-XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime
	-Xlogs:safepoints
	//查看Ergonomic机制(自动设置堆空间各分代区域的大小、收集目标内容 Parallel 收集器开始)
	-XX:+PrintAdaptiveSizePolicy
	-Xlogs:gc+ergo*=trace
	//查看对象年龄分布
	- XX:+PrintTenuringDistribution
	- Xlogs:gc+age=trace

  • 日志参数的变化
    JDK9前日志参数 JDK 9后配置形式
    G1 PrintHeapRegions | Xlog: gc+region=trace
    G1 PrintRegionLivenessInfo | Xlog:gc+liveness=trace
    G1 SummarizeConcMark | Xlog:gc+ marking=trace
    G1 SummarizeRSetStats | Xlog:gc+remset*=trace
    GCLogFileSize, NumberOfGCLogFiles, UseGCLog Xlog:gc* :file=< file>::filecount=< count> ,filesize=< fileFile Rotationsize in kb>
    PrintAdaptiveSizePolicy Xlog:gc +ergo*=trace
    PrintClassHistogramAfterFullGC Xlog:classhisto* =trace
    PrintClassHistogram BeforeFullGC Xlog:classhisto* =trace
    PrintGCApplicationConcurrentTime Xlog:safepoint
    PrintGCApplicationStoppcdTime Xlog isafepoint
    PrintGCDateStamps 使用time修饰器
    PrintGCTask TimeStamps Xlog:gc+task=trace
    PrintGCTimeStamps 使用uptime修饰器
    PrintHeapAtGC Xlog:gc +heap= debug
    PrintHeapAtGCExtended Xlog:gc+heap=trace
    PrintJNIGCStalls Xlog:gc+jni=debug
    PrintOldPLAB Xlog:gc+plab-trace
    PrintParallelOldGCPhaseTimes Xlog:gc +phases=trace
    PrintPLAB Xlog;gc+ plab=trace
    PrintPromotionFailure Xlog:gc+promotion=debug
    PrintReferenceGC Xlog:gc+ ref-debug
    PrintStringDeduplicationStatistics Xlog:gc +stringdedup
    Print Taskqueue Xlog:gc+ task+ stats=trace
    Print TenuringDistribution Xlog:gc+agc=trace
    PrintTerm inationStats Xlog:ge+ task+ stats=debug
    PrintTLAB Xlog;gc+tlab=trace
    TraceAdaptiveGCBoundary Xlog:heap+ergo=debug
    TraceDynamicGC Threads Xlog;gc+task=trace
    TraceMetadataHumongousAllocation Xlog:gc+ metaspace+alloc- debug
    G1TraceConcRefinement Xlog:gc+ refine=debug
    G1TraceEagerReclaimHumongousObjects Xlog;gc+humongous= debug
    G1TraceStringSymbolTableScrubbing Xlog:gc +stringtable=trace

垃圾收集器常用参数
UseSerialGC
虚拟机运行在Client 模式下的默认值,打开此开关后,使用Serial +
Serial Old 的收集器组合进行内存回收
UseParNewGC
打开此开关后,使用ParNew + Serial Old 的收集器组合进行内存回收
UseConcMarkSweepGC
打开此开关后,使用ParNew + CMS + Serial Old 的收集器组合进行内存
回收。Serial Old 收集器将作为CMS 收集器出现Concurrent Mode Failure失败后的后备收集器使用
UseParallelGC
虚拟机运行在Server 模式下的默认值,打开此开关后,使用Parallel
Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收
UseParallelOldGC
打开此开关后,使用Parallel Scavenge + Parallel Old 的收集器组合进行内存回收
SurvivorRatio
新生代中Eden 区域与Survivor 区域的容量比值, 默认为8, 代表
Eden :Survivor=8∶1
PretenureSizeThreshold
直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象
将直接在老年代分配
MaxTenuringThreshold
晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC 之后,年
龄就加1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy
动态调整Java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure
是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个
Eden 和Survivor 区的所有对象都存活的极端情况
ParallelGCThreads
设置并行GC 时进行内存回收的线程数
GCTimeRatio
GC 时间占总时间的比率,默认值为99,即允许1% 的GC 时间。仅在
使用Parallel Scavenge 收集器时生效
MaxGCPauseMillis
设置GC 的最大停顿时间。仅在使用Parallel Scavenge 收集器时生效
CMSInitiatingOccupancyFraction
设置CMS 收集器在老年代空间被使用多少后触发垃圾收集。默认值为
68%,仅在使用CMS 收集器时生效
UseCMSCompactAtFullCollection
设置CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅
在使用CMS 收集器时生效
CMSFullGCsBeforeCompaction
设置CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理。
仅在使用CMS 收集器时生效

参数描述
UseSerialGC使用Serial +Serial Old 垃圾收集器
UseParNewGC使用 ParNew+Serial Old JDK9后不在支持
UseConcMarkSweepGCParNew +Serial old +CMS
UseParallelGCParallel Scavenge+Parallel Old
UseParalleOldGCParallel Scavenge + Parallel Old
SurvivorRatioEden:Survivor=8:1默认
PretenureSizeThreshold直接晋升老年代的对象大小
MaxTenuringThreshold进入老年代的大小
UseAdaptiveSizePolicyt调整堆和各区域的大小
HandlePromotionFailure允许分配失败担保
ParallelGCThread允许GC回收的线程数
GCTimeRation默认为99 GC运行的时间仅仅为1% 仅在Parallel Scavenge有效
MaxGCPauseMilis设置最大的停顿时间 仅在Parallel Scavenge有效
CMSInitiatingOccupancyFraction老年代多少后出发GC
UseCMSCompactAtFullCollection在垃圾收集完成一次后进行碎片整理
CMSFullGCsBeforeCompaction若干次CMS后进行一次整理
UseG1GC使用G1垃圾回收器 在JDK9后的默认值
G1HeapRegionSzie=nRegion的大小并非最终
MaxGCPauseMillis设置G1的目标收集时间 默认为200ms不是硬性条件
G1NewSizePercent新生代的占比 默认5%
G1MaxNewSizePercent新生代最大占比 默认60%
ParallelGCThread冻结期间的并发线程数
ConcGCThreads=n并发标记 并发整理 执行的线程数
InitiatingHeapOccupancyPercent触发周期Java堆占用的
UseShenandoahGC使用Shenandoah OracleJDK不支持 只可以在OpenJDk12 -XX:+UnlockExperimentalVmOptions
ShenandoahGCHeuristics任何启动GC adaptive static compact pssive aggressive
UseZGC使用ZGC
UseNUMA使用NUMA内存分配 只有Paralle 和ZGC支持 以后G1也会
内存分配与回收策略
  1. Java的内存自动管理最根本就是结局这两个问题
    • 自动的对象分配内存
    • 自动的回收对象内存
  • 概念上来说应该是堆上分配 (标量替换、栈上分配)
  • 新生代对象通常分配在新生代中、少量对象(大小超过一定的阈值)会直接分配在老年代中
  • 对象的分配不是规则的具体决定与垃圾收集器
基于HotSpot虚拟机验证内存分布情况
  1. 对象优先分配在Eden区 当Eden区内存不足的时候就会发生MinorGC
    //-XX:+PrintGCDetails 垃圾收集和退出时打印回收日志和内存分布(开发环境使用工具对于日志进行分析)
    //-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
    private static final int _1MB =1024*1024;
    public void testAllocate(){
    	byte[] alloc1,alloc2,alloc3,alloc4;
    	alloc1=new byte[2*_1MB];
    	alloc2=new byte[2*_1MB];
    	alloc3=new byte[2*_1MB];
    	alloc4=new byte[4*_1MB]; //进行一一次minorGC
    	}
    }
    
  • 执行结果
  1. 大对象直接进入老年代
    • 当空间中出现大量的大对象是毁灭性的消息(朝生夕死的大对象是最致命的) 当复制大对象的时候 导致还有大量的内存就进行GC
    //-XX:PretenureSizeThreshold 大对象直接进入老年代 避免在Eden区以及Survivor两个区之间来回复制 (Olny Serial ParNew支持)
    	private static final int _1MB =1024*1024;
    	/*
    	-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 3*1024*1024
    	*/
    	public static void testPretenureSizeThreadhold(){
    		byte[] allocation;
    		allocation=new byte[4*_1MB]; //直接分配到老年代
    	}
    
    
    
    1. 日志
长期存活的对象进入老年代
  • 对象头保存年龄计数器每次MinorGC Age+=1 当达到某个阈值(默认15)就会进入老年代
-XX:MaxTenuringThreshold=1  [15]
private static final int _1MB =1024*1024;
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=1
@SuppressWarning("unused")
public static void testTenuringThre(){
	byte[] alloc1 alloc2 alloc3;
	alloc1=new byte[_1MB/4]; //Survivor1 可以存放但是age已经1直接进入老年代
	alloc2=new byte[4*_1MB];
	alloc3=new byte[4*_1MB];
	alloc3=null;
	alloc3=new byte[4*_1MB];
}

垃圾收集日志

  1. 动态的年龄判断
  • 并非只有达到 -XX:MaxTenuringThreshold 中的年龄要求。当相同年龄的对象达到Survivor对象那个的一半 对象age>=这个年龄直接进入老年代
/*
-verbose:gc -Xms20M -Xmx20M -Xmn10 -XX:+PrintGCDetials -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
*/
private static final int _1MB=1024*1024;

@SuppressWarnings("unsued")
public static void testTenuringThreshold(){
	byte[] alloc1,alloc2,alloc3;
	alloc1=new byte[_1MB/4];
	alloc2=new byte[_1MB/4];//空间达到512Kb 可以直接进入老年代
	alloc3=new byte[_1MB*4];
	alloc4=new byte[_1MB*4];
	alloc4=null;
	alloc4=new byte[_1MB*4];//发生GC aloc3 进入老年代
	
  1. 空间分配担保
  • 在发生MinorGC之前会观察老年代连续空间是否大于总空间 如果不成立 需要先查看 -XX:HandlePromotionFailure是否运行冒险 如果运行就会进行平均进入老年代对象大小是否小于连续空间MinorGc 如果不或者没有运行这个参数就会设置一次FullGC
  • 为什么说冒险
    • 新生代采用标志-复制算法 极端情况下所有的对象都存活 survivor无法装下对象 就需要将这些对象直接放入老年代
	-Xms20M -Xmx20M -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:HandlePromotionFailure=true
	@SuppressWarning("unused")
	public static void test(){
		byte[] alloc1 ,alloc2 ,alloc3,alloc4;
		alloc1=new byte[2*_1MB];
		...
		
		}
DefNew Tenured Perm(Metaspace)
//当为false的时候就会发生FullGC
//当为true的时候就不会发生FullGc
//JDK 6 Update 24 该参数存在但是不采用 效果与true的默认情况下相同
	bool TenuredGeneration::promotion_attempt_is_safe(size_tmax_promotion_in_byte) const{
	//老年代最大连续可用空间
	size_t available = max_contigous_available();
	//平均大小
	size_t av_promo = (size_t)gc_stats()->avg_proted()->padded_average();
	//老年代空间是否大于平均
	bool res = (available >= av_promo)|| (available >= max_promotion_in_byte);
	return res;

JVM性能监控

知识和经验是关键基础 数据是依赖 没有什么工具是秘密武器工具永远是知识的包装

一些用于监视和故障处理的工具
  1. 商业授权工具 JMC JRockit运维监控套件
  2. 正式支持工具 长期支持的工具不同的平台稍微有一点差异 但是不会凭空消失或者出现
  3. 试验性的工具 日后某天可能会转正 但是可能会莫名其妙的消失
  4. 工具具体的细节都在JDK中实现 所以工具本身看起来比较
  5. 开发接口的目的 借助这些工具和接口 开发者可以直接通过应用程序可完成良好的监视
  6. Jdk的工具都是高版本兼容低版本的
  7. 监控的开始
    1. 如果是在JDK5及其以下 需要手动开启 -Dcom.sun.management.jmxremote 大部分都是需要JMX的支持的 JDK6 以上的版本默认开启
系统监控工具
  1. jps
    • 可以查看正在运行的虚拟机进程
    • 虚拟机主类
    • 唯一ID(LVMID Local Virtual Machine Identifer)与系统进程ID相同
选项参数
-q只输出LVMID
-m传递给mian函数的参数
-l类的全名,jar包的路径
-vJvm传入的参数
  1. jstat
  • 显示本地远程 类加载 内存 垃圾回收 编译等等运行情况
jstat [option vmid [interval [s|ms](间隔) [count](次数)]] 
jstat -gc 2764 250 20
选项参数
- class监视类的加载、卸载数量、总空间以及装载所消耗的时间
-gc监控堆各区域情况
-gcutil堆%使用情况
-gcpermcapacity永久代
-compiler及时编译消耗的时间

在这里插入图片描述
Survivor0 Survivor1 Eden Old Metaspace Young GC Young Time Full Gc time GC TIme
S0: Survivor space 0 utilization as a percentage of the space’s current capacity. 幸存者区0

S1: Survivor space 1 utilization as a percentage of the space’s current capacity. 幸存者区1

E: Eden space utilization as a percentage of the space’s current capacity. 伊甸园区

O: Old space utilization as a percentage of the space’s current capacity. 老年代

M: Metaspace utilization as a percentage of the space’s current capacity. 元空间

CCS: Compressed class space utilization as a percentage. 压缩类空间利用率为百分比。

YGC: Number of young generation GC events. 年轻一代GC事件的数量。

YGCT: Young generation garbage collection time. 年轻一代垃圾收集时间

FGC: Number of full GC events. 完整的GC事件的数量。

FGCT: Full garbage collection time. 完全垃圾收集时间。

GCT: Total garbage collection time. 垃圾回收总时间。

  1. jinfo
  • 查看JVM配置类参数 jps -v只可以查看显式定义
  • 要查看默认 就使用 java -XX:+PrintFlagsFinal
  • jinfo -flag [+|-]name [name=value] 运行时修改参数
	jinfo -flag CMSInitiatingOccupancyFracion LVmid //查看促发垃圾收集的阈值
  1. jmap
  • java生成堆转储快照文件(heapdump)
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:+HeapDumpOnCtrlBreak //按下ctrl+break恐吓文件 或者kill -3  
    
    windows 下和jinfo一样受到限制
    map 工具的主要选项
选项功能
-dump生成堆转储快照文件 -dump:[live,]format=b,file=<filename>
-fianlizerinfoF-Queue队列等等执行的方法或对象
-heap回收器、参数配置、分代状况
-heap使用回收器的类型、参数配置 分代状态 Linux/Solaris
-histo显示堆中对象的统计信息
-F强制生成dump文件
jmap -dump:format=b,file=/root/1.bin 3500
  1. jhat
  • 配合jmap使用的堆分析工具
  1. jstack :Java堆栈分析工具
  • 生成当前线程的快照(执行方法的堆栈集合) 查看对应线程的调用堆栈查看线程到底在做什么
选项作用
-F正常请求得不到响应的时候强制输出
-l除堆栈外 显示关于锁的附加信息
-m调用本地方法的话可查看C/C++的栈
zhaojin@bogon ~ % jstack -l 1342
2021-11-10 19:29:04
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.151-b12 mixed mode):

"Attach Listener" #10 daemon prio=9 os_prio=31 tid=0x00007fc6b8968800 nid=0x3e03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"Service Thread" #9 daemon prio=9 os_prio=31 tid=0x00007fc6b9835000 nid=0x4703 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"C1 CompilerThread2" #8 daemon prio=9 os_prio=31 tid=0x00007fc6b807e000 nid=0x3b03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"C2 CompilerThread1" #7 daemon prio=9 os_prio=31 tid=0x00007fc6b88a5000 nid=0x3903 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"C2 CompilerThread0" #6 daemon prio=9 os_prio=31 tid=0x00007fc6b981b800 nid=0x3803 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:

  • java.lang.Thread.getAllStatck() 获取所有线程的StackTraceElement
	for(Map.Entry<Thread,StackTraceElement[]> stackTrance : Thread.getAllStack()){
	Thread thread=(Thread)stackTrance.getKey();
	if(thread.equals(Thread.currentThread()){
		continue;
	}
	System.out.println(thread.getName());
	for(StackTraceElement element : stact){
		System.out.println(element);
	}
}

基础工具

名称主要作用
jar创建管理jar 包 jar cvf classes.jar Foo.class Bar.class <-> jar cvfm classes.jar mymanifest -C foo/ .
javac编译器
javadocapi文档
javap字节码分析
可视化故障处理工具
基础工具JCMDJHSDB
jps -lmjcmdN/A
jmap -dump [pid]icmd [pid] GC.heap_dumpjhsdb map --binaryheap
map -histo [pid]jcmd [pid] GC.class_histogramjhsdb map --histo
public class JHDB_TestCase{
	static class Test{
		static ObjectHolder obj1=new ObjectHolder();//和类型信息放在方法区
		ObjectHolder obj2=new ObjectHolder(); //随对象实例放在堆
		void foo(){
			 ObjectHolder obj3=new ObjectHolder();//方法栈帧的局部变量表
			 System.out.println("done");
		}
	}
	private static class ObjectHolder{}
	public static void mian(String[] args){
		Test test=new JHDB_TestCase.Test();
		test.foo();
	}
}
//-Xms10M -Xmx10M -XX:+UseSerialGC -XX:-UseCompressedOops

请添加图片描述

《Java虚拟机规范》 所有class相关的信息都放在方法区 但是方法区的实现并没有要求

jps -l
jhsdb hsdb --pid <LVMID>
Tool->Heap Parameters 查看Eden 区老年代地址位置
Windows->Console 使用scanoops 在新生代(Eden开始位置 到To Survivor)
scannoops 地址 地址 类明$实例对象
Tools -》 Inspector确定存放位置
Tools-〉Compute Reverse Ptrs  (也可以使用 revptrs 地址)
然后在在 Tools -》 Inspector 确定存放位置
在Java Thread 选中main线程后查看

  1. JConsole
    基于JMC(JRockit套件)可视化监控管理工具 通过JMX的MBean对系统信息进行调整和收集 JMX是一种开放性的技术 可以放在JVM中 还可以运行与JVM 之上的软件中 典型的中间件也是基于JMX 实现管理和监控的 JMX MBean 是完全开放的可以通过API来进行调用请添加图片描述
#vi ~/.bash_profile
export JAVA_TOOL_OPTIONS="
-Djava.rmi.server.hostname=150.158.199.61 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.management.jmxremote.port=8011"

请添加图片描述

static class OOMObject{
	public byte[] place =new byte[64 *1024];
}
for(0->num){
	list.add(new OOMObject());
}
System.gc();//回收后依然在老年代 ,因为作用域

请添加图片描述

a(){
while(true){}
}
b(){
synchronized(lock){
	try{
		lock.wait();
	}catch(InterruptedException e){
		e.printStackTrace();
	}
}
}
main(){
	a.start() //RUNNABLE
	b.start() //WAITING 在 java.lang.Object@3c83ad0
}

死锁

public class MyThread extends Thread{
	@override
	public void run(){
		synchronized(Integer.valueOf(1)){
			synchronized(Integer.valueOf(2)){
				print(1+2); //BLOCKED 在java.lang.Integer@1c25fsd 拥有者 Thread-12 
			}
		}
	}
}
//Integer.valueOf() 为了减少对象的创建 在-128-127 之间的对象进行缓存 如果传入对象在这个范围内直接返回 《JAVA虚拟机规范》明确要求的默认值 可以通过 java.lang.Integer.IntegerCache.hight 参数进行设置
//只要发生了线程切换就会出现死锁		
  1. VisualVM
  • ALL-IN-One
  • 除了常规的运行监控 故障处理 还有性能分析。和JProfiler YourKit 等收费软件媲美
  • 最大的优点 不需要特殊的Agent去运行(通用性强 对程序影响小) 是其它收费工具无法
  • JDk6 Update7发布向下兼容性
JDK1.456local6 remote
运行环境
系统属性
监控面板
线程面板
性能监控
堆栈DUMP
Mbean
Jconsole
  • VisualVM 的精华功能在插件上 要使用精华功能就必须按照插件
  • 安装插件
    • 插件网站工具 ->插件->已下载
    • 工具插件菜单(下载地址JDK_HOME/lib/visualvm)
    • 应用程序邮件就可以生成dump文件 文件导入就可以打开Dump文件
    • 在PRofiler中可以对CPU内存的使用进行监控
    • BTace 插件安装后 本身就是一个程序可以通过 Hotspot的Instrument功能 在不影响原先程序运行的情况下添加新的功能 右键要调试的程序 会出现 Trace Application
	public class Test{
		public int add(int a ,int b){
			return a+b;
		}
		public static void main(String[] args[]) throws IOExecption{
			Scanner ent=new Scanner(System.in);
			for(int i=0;i<10;i++){
				ent.readLine();
				int a=Math.round(Math.random()*1000);	
				int b=Math.round(Math.random()*1000);
			}
			int c=new Test().add(); 
			System.out.println(c);
		}
	}
	@OnMethod(clazz="Test",
	method="add",
	location=@Location(Kind.RETURN)
	)
	public static void func(@Self Test instance,int a,int b,@Return int result){
	println("调用栈");
	jstack();
	println(strcat("方法参数a:",str(a)));
	println(strcat("方法参数b:",str(b)));
	println(strcat("方法返回:",str(result)));
}
//start运行后结果就会返回
//应用非常广泛 对于栈的调用 方法的返回 都有很好的作用
//基于Instrument 开发
//Instrument 是虚拟机工具接口(JVMTI)第三方程序也可以通过代理的方式修改参数和返回

请添加图片描述

4.Java Mission Control 可持续在线控制工具

  • 7x24小时的在线服务
  • 基于企业JRE定制的AMC JUT 在个人开发中免费JMC 和JFR
  • JFR
    • 建立在HotSpot虚拟机里面用于信息收集的框架 JFR对于吞吐量的影响不超过1% 所以可以持续在线
  • 可以在文件连接中进行远程连接
#vi ~/.bash_profile
export JAVA_TOOL_OPTIONS="
-Djava.rmi.server.hostname=150.158.199.61 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.management.jmxremote.port=8011"
- XX:+UnlockCommercialFeatures -XX:+FilgthRecorder
  • 连接后可以看见 Mbean 和JFR的数据
  • 启动发行记录器 JFR 的信息将会得倒记录
    • 一般信息:关于虚拟机 操作系统记录
    • 内存:关于内存管理和垃圾回收
    • 代码 : 方法、异常、编译错误
    • I/O关于文件 SOcket的输入输出信息
    • 系统:关于正在运行的虚拟机系统 进程的环境变量
    • 事件关于记录事件类型的记录信息
  • JFR一个事件录制器 当事件发生后所有的事件都会保存在一个环形记录器中 JMC 从中读取 只有 由于是环型记录器 只有最近的才可以记录
  • JFR收集的精度较高
    • Mbean 只有分代大小 收集次数 时间 收集频率(结果类)
    • JFR 分配哪些对象 、哪些在TLAB(堆为每一个线程分配空间) 分配的压力如何
HSDIS :JIT生成代码反汇编
  • 由于JVM的发展 现在虚拟机的指令只是与规定 等效但不一定按照规定执行
  • 由于有的代码是通过即时变异市场呢个放到代码缓存的 传统的方法不方便就行Debug ----HSDIS出现了
  • HSDIS 一个反汇编插件包含在Hotspot虚拟机源码当中
-XX:+PrintAssembly //把即时编译动态生成的代码输出,产生大量非常有价值的注释
-Xcomp //编译的模式执行
-XX:CompileCommand=dontinline,*Bar.sum 
-XX:CompileCommand=compileonly,*Bar.sum 

public class Bar{
}
//将会生成机械码
  • JITWatch 是 HSDIS 经常搭配的可视化日志分析工具 ,为了方便读取可以将 输出 到logfile文件:
-XX:+UnlockDiagnosticVMOption
-XX:+TraceClassLoading
-XX:+LogCompilation
-XX:LogFile=/tmp/logfile.log
-XX:+PrintAssembly
-XX:+TraceClassLoading

就可以查看对应的源码 字节码 即时编译的汇编代码

GC日志分析篇

-Xms5M -Xmx5M -Xmn2m -XX:SurvivorRatio=1 -XX:+PrintGCDetails -XX:+PrintHeapAtGC

import java.util.*;

public class Test{
        static int _1MB=1024*1024;
        public static void main (String[] args) throws InterruptedException {
                for (int i = 0; i < 10; i++) {
                       byte[] b= new byte[_1MB];
                }
                new Scanner(System.in).next();

        }


}


{Heap before GC invocations=1 (full 0):
 PSYoungGen      total 1536K, used 1024K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 1024K, 100% used [0x00000007bfe00000,0x00000007bff00000,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
 ParOldGen       total 4096K, used 0K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
  object space 4096K, 0% used [0x00000007bfa00000,0x00000007bfa00000,0x00000007bfe00000)
 Metaspace       used 2647K, capacity 4480K, committed 4480K, reserved 1056768K
  class space    used 292K, capacity 384K, committed 384K, reserved 1048576K
[GC (Allocation Failure) [PSYoungGen: 1024K->400K(1536K)] 1024K->400K(5632K), 0.0106680 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap after GC invocations=1 (full 0):
 PSYoungGen      total 1536K, used 400K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  from space 512K, 78% used [0x00000007bff00000,0x00000007bff64010,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 4096K, used 0K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
  object space 4096K, 0% used [0x00000007bfa00000,0x00000007bfa00000,0x00000007bfe00000)
 Metaspace       used 2647K, capacity 4480K, committed 4480K, reserved 1056768K
  class space    used 292K, capacity 384K, committed 384K, reserved 1048576K
}
{Heap before GC invocations=2 (full 0):
 PSYoungGen      total 1536K, used 1017K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 1024K, 60% used [0x00000007bfe00000,0x00000007bfe9a4a8,0x00000007bff00000)
  from space 512K, 78% used [0x00000007bff00000,0x00000007bff64010,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 4096K, used 3072K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
  object space 4096K, 75% used [0x00000007bfa00000,0x00000007bfd00030,0x00000007bfe00000)
 Metaspace       used 3285K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 362K, capacity 388K, committed 512K, reserved 1048576K
[GC (Allocation Failure) [PSYoungGen: 1017K->480K(1536K)] 4089K->3568K(5632K), 0.0024054 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap after GC invocations=2 (full 0):
 PSYoungGen      total 1536K, used 480K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  from space 512K, 93% used [0x00000007bff80000,0x00000007bfff8020,0x00000007c0000000)
  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
 ParOldGen       total 4096K, used 3088K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
  object space 4096K, 75% used [0x00000007bfa00000,0x00000007bfd04030,0x00000007bfe00000)
 Metaspace       used 3285K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 362K, capacity 388K, committed 512K, reserved 1048576K
}

调优实战

通过升级硬件 使用新版本JDK解决问题

  1. 案例1 :15万 PV/日文章系统 64Centos 5.4 Resin作为Web服务器 64位JDK6 -Xmx -Xms 12GB 网站不定期出现长时间失去响应
    方案: 默认使用的是吞吐量优先的垃圾收集器 12GB的堆一次FULLGC 所花费的时间就是14s 由于程序设计的原因 需要把文档从磁盘中读取到内存中 导致产生由文档序列化的大对象 直接分配在老年代 没有在MinorGC中清除 即使是12G的堆也会快速消耗殆尽
  • 32位的操作系统 1.5G的堆内存当时虽然比较缓慢 但是也不至于 到达长时间的停顿
    目前单体较大内存的管理主要有两种方案:
    1. 通过一个单独的JVM管理大量的Java堆内存
    2. 同时使用若干个虚拟机建立逻辑集群利用有效资源
    这种交互性强、对延迟要求高的应用不一定要使用(ZGC Shenandoah)同样可以使用Parallel Scavenge/old的垃圾回收器 分配较大的内存 但是Full GC的频率一定得控制好
    3. 系统是否稳定取决于大多数对象是否满足朝生夕死 大多数对象的生命周期不能太长 不能产生较多的大对象
    4. 大多数生命周期都应该是页面界别的不能说会话层的 这样的话使用管理超大内存并不困难
    使用单个虚拟机来管理内存需要考虑
    • 回收大块堆内存导致长时间停顿,自从G1收集器开始增量回收得倒解决 到了ZGC 和Shenandoah成熟得倒完整的解决
    • 大内存必须有64位虚拟机的支持 但是由于压缩指针 处理器缓存行 64位的性能明显低于同版本32位的
    • 必须保证正常运维 如果在运行中出现问题产生的Dump文件10G+有可能更大 必须使用JMC这种在线控制工具
    • 相同64位消耗内存一般比32位大 可以开启压缩指针(默认就是开启)
      使用启动多台虚拟机
    • 为每一个程序分配不同的端口然后使用反向代理达到负载均衡 由于是一台物理机 可以使用无SEssion复制的方法进行访问
    • 问题
    • 节点全局资源竞争 多个节点同时访问某个磁盘文件也很容易产生IO异常
    • 很难高效的利用资源池 有的节点连接池已经满了但是有的还是空闲(JNDI解决)
    • 如果使用32位作为节点的话每一个节点的限制都会小于4G
    • 大量的缓存池(比如HashMap K/V 缓存 逻辑集群会产生大量的浪费 这个时候可以采用集中缓存 )

改用CMS 5个32位JDK逻辑集群 每个进程2GB 堆固定1.5GB 再搭建一个Apache服务器作反代

2.案例2集群见同步出现溢出
问题: 一个基于B/S架构的MIS系统 两台双路处理器 8G内存的小型HP 应用中间件 WebLogic 9.2 每台机器开启3个 WebLogic 构成6个节点的亲和式的集群 没有就行Session共享 但是有一些需求让节点间的数据共享 开始放在数据库的 但由于竞争激烈 后来放在JboseCAche全局缓存里面 刚开始正常运行但是运行较长一段时间()后就出现不定期的内存溢出问题 开始以为是不常用代码导致的 但是并没有改动程序
正常运行的每次都可以正常回收垃圾 到一个平稳

-XX:+HeapDumpOnOutOfMemoryError

里面发现了大量的 org.jgroups.protocols.pbcast.NAKACK对象
原因:JBooseCache 是基于Jgroups就行通信的 使用协议栈的方式保收发数据的自由组合 经过每层协议栈都要结果up() down()方法 其中NAKACK 栈用于保障包的有序
为了保障发送失败的情况 在节点正确收到信息之前都必须保障 当大量的请求网络不能满足的时候就会出现大量的请求重穿 积压导致内存溢出

3. 堆外内存导致的溢出错误
小型的校园网 B/S 为了让客服端可以及时的从服务端读取到数据 使用逆向AJAX技术 I5CPu 4GB 32windows系统
测试期间不定期的出现内存溢出异常 32位系统 1.6G内存无法再扩大 开大也没有效果反而延迟好想更严重了

-XX:+HeapDumpOnOutOfMemoryError  //抛出异常的时候居然没有任何反应 无赖

只好使用jstat
发现Eden区Survivor 区都非常的稳定 垃圾收集次数也是
最后在系统日志找到了系统堆栈错误

java.lang.OutOfMemoryError:null
at sun.misc.Unsafe.allocateMemory(Native Method)

由于进程2GB的限制分配给堆堆限制是1.6GB 所有只有0.4G是它可以使用的 虚拟机对于直接内存的回收并不是会自己通知垃圾回收器而是在FULLGC的时候顺便回收 只有是在内存溢出异常的时候捕获异常 在Catch里面调用System.gc() 本案例刚好又使用大量的NIO操作需要使用到直接内存

-XX:+DisableExpilcitGC //禁止手动调用GC
-XX:MaxDirectMemorySize //直接内存大小 不够时间报OutofMemory Error[:Direct buffer memory] 或者
-Xss //线程堆栈不足的时候StackOverflowError
Socket 缓冲区:每个Socket都有缓冲区Receive Send 大约位37KB 和 25KB  连接过多的话这块区域也比较可观 IOException:Too many open file
JNI代码 如果使用JNI调用本地代码块的话使用的也是本地内存 
虚拟机垃圾收集器 : 虚拟机、垃圾收集器工作也要消耗一定量的内存

4.外部命令导致的系统缓慢

一个数字校园应用 运行在一台4路处理器的Solaris 10操作系统上 中间件位GlassFish 服务器 在做压力测试的时候发现响应的时间比较慢 系统mpstat 发现处理器的使用很高 但是占用系统资源并不是应用本身这很不正常
通过 dtrace 脚本 发现消耗最多的尽然说fork (Linux 产生新进程) 但是JAVA程序设计的时候应当只有线程 而不是进程
原来每次用户请求都会使用系统调用Runtime.getRuntime().exec()执行 过程(复制进程的环境变量 、执行进程、退出进程 返回)频繁的创建销毁进程资源损害大
根据介意 去掉Shell脚本 使用Java本身的API得倒运行结果

5.服务器虚拟机进程崩溃

B/s MIS系统 8GB内存 HP系统 WebLogic 9.2 频繁出现虚拟机关闭 hs_err_pid##.log 每台都会出现相同的问题 关闭之前出现大量相同的异常

 java.net.SocketException:Connection rest
 at java.net.SocetInputStream.read(SocketInputStream.java:168)
 

使用了大量的异步调用导致连接中断 由于运行的速度不相同导致大量的对于Socket的等待挤压 最后导致的异常 最后使用生产/消费者队列后得倒解决

5.不恰当的数据结构导致内存占用过大

问题:后台 RPC服务器 使用64位的JVM(CMS+ParNew) - Xms4g -Xmx8g -Xmn1g 平时垃圾回收都说30ms以内 10m/次 加载一个80MB的数据到内存 产出超过100万个HashMap<> Enty
MinorGC 会造成超过500ms的GC ParNew使用的是复制算法这个算法是建立在大多数对象都说朝生夕死的

95 次GC后新生代占用为100M 96次GC占用还是100 存放在Eden区
[GC (Allocation Failure) [ParNew: 803142K->100352K(91536K)] 1024K->400K(5632K), 0.5106680 secs] [Times: user=1.46 sys=0.04, real=0.60 secs]

解决办法
考虑直接把 Survivor区去掉 (-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0或 -XX:+AlwaysTenur) 让新生代地产产生的对象直接进入老年代(治标不治本)

HashMap<Long,Long> z这个结储存数据的利用太低了

讨论:
HashMap次存储空间 在HashMap<Long,Long> 中 只有 2*8 个字节的有效空间 包装成为
java.lang.long 具有8字节 的Mark Word 8字节的 KLass 指针 再有8字节的long数据 在组成hashmap后又有了16字节的对象头 8字节的next字段和 4字节的int 型hash字段 为了对其还有4字节的空白字段 最后还有 这个Entry8字节的引用
Long(24)*2+Enty(32)+Hashmap Ref(8)=88Byte 16/88 18%的利用这个空间浪费实在是太大了

6.windows虚拟内存的导致的长时间停顿
GUI桌面程序 15s发送一次心跳检测 如果30s还没有回应就是挂掉了程序上线后发现心跳检测有误报的情况 偶尔出现1分钟完全没有日志输出处于停顿

-XX:PrintGCApplicationStoppedTime -XX:+PrintGCDetails -Xloggc:gclog.log
//发现大部分收集到时间都在100ms以内
//但是又一次居然到达了差不多一分钟
-XX:+PrintReferenceGC  //显示的是真正的垃圾引用回收花的时间 但是总代时间长
AWT 程序后台运行重新启动的时间差
-XX:+Dsun.awt.keepWorkingSetOnMinmize=true

7.由于安全点导致的长时间停顿
一个承担公共计算任务的离线HBase集群 运行在JDK8 G1垃圾回收器 明天有大量的MapReduce Spark离线分析任务 有其它在线集群Replication过来数据写入 集群压力大
离线分析对于延迟要求不高-XX:MaxGCPauseMillis 500ms

[Times: user=1.51 sys=0.67, real=0.14 secs]
Toal time for which application threads were stoped : 2.2645818 seconds
//user 进程执行用户态代码耗费的时间 (处理器时间)
//sys 进程执行核心代码消耗的时间 (处理器时间)
//real:执行动作开始到结束的时间(时钟时间) 就是会有多少倍的处理时间被记录下来
-----------
//垃圾收集只用了0.14秒 但是用户线程却停了2.26已经远远超出了正常的TTSP(Time to Safepointer)
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 去检查安全点日志
[time: spin block cleanup vmop] page_trap_count
[2255 0 2255 11 0] 1
//自旋(spin) 时间犹豫部分已经到达安全点而部分未到导致的
获取超时没有到达安全点的线程
-XX:+SafepointTimeout -XX:SafepointTimeoutDelay=2000 让线程错过2000毫秒认超时 这样线程就会输出

#SafepointSynchronize::begin: Timeout detected:
# Safepoint Synchronize: :begin: Timed out while spinning to reach a safepoint.
#SafepointSynchronize: :begin: Threads which did not reach the safepoint:
#"RpcServer.listener, port=24600" #32 daemon prio=5 os prio=0 tid=0x00007f4c14b22840
nid=Oxa621 runnable [0x0000000000000000]
java.lang. Thread.State: RUNNABLE
#SafepointSynchronize::begin: (End of list)

RpcServer.listener, port=24600线程名称
安全点的选取上“能否让线程长时间执行为标准” 在循环、方法调用、跳转的地方会用到 但避免安全点过多是 int为索引的循环叫(Counted)可数循环不放入安全点 使用long类型的叫不可数放安全点

-XX:+UseCountedLoopSafepointers//强制在循环放置安全点 JDK8有BUG可能导致JVM崩溃

在处理原创掉用断开的索引使用的是int 类型导致名称都会等待较长的时间 如果不具备安全点的知识这个问题是很难排出的

8 Eclipse运行速度调优

-Xms512M
-Dcom.sun.management.jmxremote //开启JMX远程管理 (需要在VisualVM收集数据)

jsp
jstat -class LVMID 查看类加载的时间

jdk版本提可以一定程度上提升(禁用字节码严重 默认认为是安全的)
查看Eclipse minorGC发生的频率过高 可以调大-Xmn 将—Xms -Xmx 相等避免动态扩容 永久代也是

-Xverify:none
-Xmx512m
-Xms512m
-Xmn128m
-XX:PermSize=96m
-XX:MaxPermSize=96m
jps
jstat -gccause LVMID
System.gc()//代码主动调用GC FULL
-XX:+DisableExplicitGC
//由于CPU的资源非常充足 ,对于延迟的追求可以使用CMS + ParNew垃圾收集器
-Xverify:none
-Xmx512m
-Xms512m
-Xmn128m
-XX:PermSize=96m
-XX:MaxPermSize=96m
-XX:+DisableExplicitGC
-Xnoclassgc
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSInitatingOccupancyFraction=85
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值