- 921. Minimum Add to Make Parentheses Valid 使括号有效的最少添加
- 915. Partition Array into Disjoint Intervals 分割数组
- 932. Beautiful Array 漂亮数组
- 940. Distinct Subsequences II 不同的子序列 II
不管是在面试题还是平常的技术交流中,大家都在说 JVM调优、JVM优化什么的;那你有没有想过,JVM优化到底在优化什么东西?
一个Java系统运行起来之后,也就是JVM运行起来之后,最核心的区域就是堆内存,在堆内存区域会 存放系统运行时生成的各种对象。
堆内存会被划分为新生代和老年代两个区域,对于新创建的对象来说,首先都是被放入新生代的;
随着系统不停的运行,会创建越来越多的对象,也就会有越来越多的对象进入新生代;由于Java堆内存大小是有限制的,所以这个新生代区域也会被逐渐放满,在快要放满的时候,就需要对里面的“垃圾对象”进行清理了。
这些垃圾对象,也就是 没有被 GC Roots 直接或间接引用的对象;GC Roots一般就是 静态变量、局部变量、活着的线程 等。
在一个系统中最经常创建对象的地方是在各个方法里面,当一个方法运行完毕之后,这个方法中创建的对象就没有局部变量这个GC Roots引用了,也就是成为了垃圾对象。
所以在新生代中,可能95%以上的对象都是这种没有被引用的垃圾对象。
新生代中按照默认比例分配的话,会有一块占比80%的 Eden区域 和两块分别占比10%的 Survivor区域;
在新生代快要满了的时候,就会触发新生代GC(Minor GC),使用 复制算法 对新生代的垃圾对象进行清理:
首先停止系统程序的运行,也就是进入“Stop the World”状态;
然后对所有的 GC Roots进行追踪,标记出被引用了的存活对象:
然后把 Eden区(和Survivor区)所有的存活对象都复制到另一块 Survivor区域中去:
最后把 Eden区(和Survivor区)中的剩余的垃圾对象全部回收掉,释放内存空间;并恢复系统程序运行;
这就是从系统生成对象到进入新生代,再到新生代GC的全部过程了。
这里面有一个重点:新生代GC的时候,会进入“Stop the World”状态,也就是会停止系统运行。
Q:那这种新生代GC对系统的性能影响到底大不大呢?
A:其实影响是不大的,因为新生代中存活的对象很少,所以JVM能够迅速地标记出被GC Roots引用的存活对象,然后使用效率很高的复制算法移到Survivor区,再对垃圾对象进行回收就好了,整个过程速度很快(大概就几毫秒、几十毫秒);所以对系统的性能影响是很小的。
Q:那什么时候新生代GC对系统的性能影响会很大呢?
A:如果你的系统是类似Kafka、ES之类的大数据处理相关的系统,它们每秒可能会有上万的请求量,就需要很大的JVM堆内存进行处理;
所以如果系统是部署在很大的机器上(比如64G的机器),此时分配给新生代Eden区的内存可能就会有20G以上;
这个时候由于请求量非常大生成对象也就会很快,导致频繁回收新生代;每次回收的时候由于Eden区太大,“Stop the World”的时间也就是系统停顿的时间就会比较长(可能达到几秒),也就可能导致系统请求阻塞、超时等情况;
除了新生代,系统运行时还会有不少对象会进入老年代,那有哪些对象在什么情况下会进入老年代呢?
默认值为0,当不主动设置值时,不管多大的对象都会先在新生代分配内存;
当手动设置了这个值时,如果生成一个大于这个大小的对象(比如一个超大的数组或者其他对象),就会直接在老年代中为这个对象分配内存;
G1收集器中有专门的大对象Region,大对象不存在老年代;
上面的几种情况中,分配担保和动态年龄判断都是很关键的;通常如果新生代的Survivor区内存设置过小,就可能导致这两种情况频繁发生,然后导致大量对象快速进入老年代,在老年代对象过多的时候,就会触发 Full GC。
Full GC发生的时候一般会跟着一次Young GC,也会跟着触发元数据空间的GC。
那什么条件会触发 Full GC呢?
Full GC 一般来说都很耗时,无论是CMS垃圾回收器还是G1垃圾回收器;因为它们都要经过 “Stop the World”的初始标记、追踪所有GC Roots关联对象的并发标记、再次“Stop the World”的重新标记 三个阶段,过程非常复杂也非常耗时。
Full GC 一般来说会比新生代GC慢10倍以上,比如新生代GC每次耗时50ms,其实对用户系统影响不大;但是老年代GC每次耗时1000ms,可能就会导致在GC的时候用户界面上卡顿1s左右的时间,这个影响就很大了。
所以如果JVM内存分配不合理,导致频繁地进行老年代GC,每次Full GC都让用户系统停顿1s左右的时间,这对大部分应用简直是致命的打击。
到这里我们应该知道了,Java系统的最大问题就是:因为内存分配和参数设置的不合理,导致对象频繁进入老年代,然后频繁触发Full GC,进而导致应用卡顿,还可能引发OOM等严重问题。
所以,JVM的优化,说到底就是 尽量保证你的系统只发生Young GC,减少甚至不发生Full GC。
那优化有没有一个可以量化的标准呢?
其实很难给定一个量化的标准,因为各个系统的内存条件和负载量这些都不一样,所以没法给出一个具体的标准;
但是一般来说,正常运行情况下的系统,因为你不可能无限的分配堆内存,所以随着系统的运行,一定会发生Young GC的;
负载不高的系统,一般几十分钟几个小时发生一次Young GC,每次耗时在几毫秒左右;
负载很高的系统,一般几分钟这些发生一次Young GC,每次耗时几毫秒左右,这些都是合理的;
Full GC的话,大多数系统多控制在几十分钟或者几个小时发生一次,也就比较合理了;
工欲善其事,必先利其器;想要真正去做JVM性能优化,除了JVM本身的基础知识(JVM运行区域、运行原理、垃圾回收器及回收流程),我们还需要具备几项基本技能:
在系统运行时,新生成的对象,通常来说都是优先分配在新生代中的Eden区域,然后随着垃圾回收,交替使用两个Survivor区域进行存活对象的复制;默认Eden区和两个Survivor区的比值为:8:1:1;例如使用如下JVM参数来运行系统:
-Xms4G -Xmx4G -Xmn2G -Xss1M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=10M
-XX:+PrintGCDetils -XX:+PrintGCTimeStamps -Xloggc:gc.log -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/oom
这些参数都是基于 JDK1.8设置的,不同JDK版本对应的参数名称可能不太一样。
“-Xms4G”和 “-Xmx4G”:虚拟机堆内存的初始大小和最大大小,它们两个被建议设置为相等的;
原因:如果初始堆大小设置过小,在应用内存分配不够时,JVM会先频繁地触发GC操作,当GC操作无法释放更多内存时,才会进行内存的扩充,直到最大堆大小;所以设置为相等可以避免频繁GC和扩容造成的系统开销。
“-Xmn2G”:新生代的大小;
“-Xss1M”:每个线程的栈内存大小;
“-XX:SurvivorRatio=8”:Eden:Survivor:Survivor区的比值,默认即为8,可以不设置;
“-XX:PretenureSizeThreshold=10M”:指定大对象阈值为10MB;
“XX:+PrintGCDetils”:打印详细的GC日志;
“-XX:+PrintGCTimeStamps”:打印每次GC发生的时间;
“-Xloggc:gc.log”:将GC日志写入指定文件;
“-XX:+UseParNewGC”:新生代使用ParNew垃圾回收器;
“-XX:+UseConcMarkSweepGC”:老年代使用CMS垃圾回收器;
“-XX:+HeapDumpOnOutOfMemoryError”:发生OOM的时候自动dump堆内存快照出来;
“-XX:HeapDumpPath=/usr/local/oom”:把dump出来的内存快照文件存放到指定目录;
参数 | 示例 | 说明 |
---|---|---|
-Xms | -Xms1G | 初始堆大小;默认空闲堆内存小于40%(XX:MinHeapFreeRatio),JVM就会增大堆到-Xmx的最大限制 |
-Xmx | -Xmx1G | 最大堆大小;默认空闲堆内存大于70%时,JVM会减少堆到 -Xms的最小限制 |
-Xmn | -Xmn512M | 新生代大小;(eden+ 2 survivor space)的大小;SUN官方推荐配置为整个堆的3/8 |
-XX:NewRatio | -XX:NewRatio=2 | 新生代与老年代比值;-XX:NewRatio=2表示新生代与老年代所占比例为1:2,新生代占整个堆空间的1/3 |
-XX:SurvivorRatio | -XX:SurvivorRatio=8 | Eden:Survivor:Survivor;默认为8,Eden区占新生代的8/10,Survivor区分别占新生代的1/10 |
-Xss | -Xss1M | 线程的栈内存大小;JDK5.0以后每个线程栈内存大小为1M |
-XX:MetaspaceSize | -XX:MetaspaceSize=512M | 元数据空间初始大小 |
-XX:MaxMetaspaceSize | -XX:MaxMetaspaceSize=512M | 元数据空间最大大小 |
-XX:+UseSerialGC | 虚拟机运行在Client模式下的默认值,设置后使用 Serial + Serial Old 组合进行垃圾回收 | |
-XX:+UseParNewGC | 使用 ParNew + Serial Old 组合进行垃圾回收 | |
-XX:+UseConcMarkSweepGC | 使用 ParNew + CMS + Serial Old 组合进行垃圾回收,Serial Old作为CMS收集器失败后的后备收集器使用 | |
-XX:CMSInitiatingOccupanyFraction | 使用CMS时,在老年代空间被使用多少后出发垃圾回收;默认值 68% | |
-XX:+UseCMSCompactAtFullCollection | 使用CMS时,开启对老年代的压缩 | |
-XX:CMSFullGCsBeforeCompaction=0 | 使用CMS时,多少次Full GC后,对老年代进行压缩 | |
-XX:+UseParallelGC | 虚拟机运行在Server模式下的默认值,设置后使用 Parallel Scavenge + Serial Old 组合进行垃圾回收 | |
-XX:+UseParallelOldGC | 使用 Parallel Scavenge + Parallel Old 组合进行垃圾回收 | |
-XX:+UseG1GC | 使用 G1 进行垃圾回收 | |
-XX:+PrintGC | 打印GC简单信息 | |
-XX:+PrintGCDetails | 打印GC详细信息 | |
-XX:+PrintGCTimeStamps | 打印每次GC发生的时间 | |
-Xloggc:gc.log | 将GC日志写入指定文件 | |
-XX:+HeapDumpOnOutOfMemoryError | 发生OOM的时候自动dump堆内存快照出来 | |
-XX:HeapDumpPath=/usr/local/oom | 把dump出来的内存快照文件存放到指定目录 |
JVM运行参数:
-Xmx10m -Xms10m -Xmn5m -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=10m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:log/demo1.log
示例代码:
public class Demo1 {
public static void main(String[] args) {
byte[] array1 = new byte[1024 * 1024];
array1 = new byte[1024 * 1024];
array1 = new byte[1024 * 1024];
array1 = null;
byte[] array2 = new byte[2 * 1024 * 1024];
}
}
public static void main(String[] args)在main线程的虚拟机栈中压入一个main()方法的栈帧;
byte[] array1 = new byte[1024 * 1024];在新生代Eden区中创建一个1MB的byte数组对象;
在main()方法的栈帧中创建一个 array1 的局部变量,并指向Eden区中那个1MB的byte数组对象;
array1 = new byte[1024 * 1024];继续在Eden区中创建第二个byte数组对象,并让 array1这个局部变量指向这第二个对象;那这个时候第一个byte数组对象就没有引用了,也就变成了“垃圾对象”;
array1 = new byte[1024 * 1024];同样,创建第三个byte数组对象,第二个byte数组对象变成“垃圾对象”;
array1 = null;让array1这个变量指向了null,意思就是第三个byte数组对象也变成了“垃圾对象”;也就是说这里三个对象都变成了“垃圾对象”;(但是不会马上被回收)
byte[] array2 = new byte[2 * 1024 * 1024];继续在Eden区中创建2MB的byte数组对象;
那这个时候还能正常创建吗?不行的;
来看JVM参数:-Xmn5m -XX:SurvivorRatio=8
,意思是新生代只分配了5MB的内存空间,按比例,Eden区只有4M的内存空间;
而在前面的代码执行中,已经往Eden区放入了3MB的对象了,剩余空间只有1MB不到了(因为还会有一些元数据),所以此时要想再放入2MB的新对象,是放不进去的;如图:
所以这个时候就会发生新生代GC(Young GC);如图:
在新生代GC发生之后,由于前面三个对象都已经是“垃圾对象”了,所以都能被回收,在被回收之后,Eden区又空出来接近4MB的内存空间,这个时候就能放入新的array2对象了;
那怎么知道到底是不是这样执行的呢?我们又能怎么进行验证?来看看GC日志就知道了。
使用上面的JVM参数运行这段代码,就会在log目录下的demo1.log文件中,看到这段代码执行的GC日志。
Java HotSpot(TM) 64-Bit Server VM (25.181-b13) for linux-amd64 JRE (1.8.0_181-b13), built on Jul 7 2018 00:56:38 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 1863076k(369904k free), swap 2097148k(2097148k free)
CommandLine flags: -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=5242880 -XX:NewSize=5242880 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
0.048: [GC (Allocation Failure) 0.048: [ParNew: 3498K->264K(4608K), 0.0005606 secs] 3498K->264K(9728K), 0.0006166 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 4608K, used 2395K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)
eden space 4096K, 52% used [0x00000000ff600000, 0x00000000ff814930, 0x00000000ffa00000)
from space 512K, 51% used [0x00000000ffa80000, 0x00000000ffac22d0, 0x00000000ffb00000)
to space 512K, 0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)
concurrent mark-sweep generation total 5120K, used 0K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2477K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 267K, capacity 386K, committed 512K, reserved 1048576K
CommandLine flags:基本都是我们设置的JVM参数,和没有设置时,系统默认给的JVM参数;
0.048:说明这次GC是在系统运行了 40ms左右发生的;
GC (Allocation Failure):发生了一次GC;Allocation Failure:对象分配失败;
为什么会发生?看参数,Eden区只有 4M;再看代码,我们先分配了3M,再需要分配2M的时候,Eden区不够了,所以此时需要触发一次 Young GC;
[ParNew: 3498K->264K(4608K), 0.0005606 secs]:因为这里我们使用的是 ParNew + CMS,所以 Young GC使用 ParNew收集;
3498K->264K:新生代执行了一次 Young GC, 执行之前使用了 3498K,GC 之后还有 264K的对象存活下来;
(这里可能会有疑惑,我们生成的对象是3M的数组,应该是3072KB,那GC执行前应该是使用了3072KB内存吧,但是这里为什么是3498K呢?
这是因为JVM为了存储数组,还会附带一些其他的内置信息(元数据),所以每个数组实际占用的内存是要大于1MB的)
4608K:新生代可用空间大小为4608K,也就是4.5M,为什么是4.5M? Eden区4M + 一个Survivor区0.5M = 4.5M;
0.0005606 secs:本次 GC 消耗的时间,大概消耗了1ms,因为就仅仅是回收 3M的对象;
3498K->264K(9728K), 0.0006166 secs:指整个堆内存的回收情况;
9728K:整个Java堆可用空间为9728K,新生代4.5M + 老年代5M;
[Times: user=0.00 sys=0.00, real=0.00 secs]:本次GC消耗的时间;因为只消耗了几毫秒,而这里单位是秒,只保留到小数点后两位的话,就全是0;
Heap后面的是 在JVM退出的时候打印出来的当前堆的内存情况。
Metaspace元数据空间和Class空间,存放一些类信息、常量池之类的东西,此时他们的总容量,使用内存,等等
所以经过GC日志的分析,我们不仅能够验证上面的代码执行流程,还能知道各种对象在JVM中的分配情况。
问题情景 混淆群内的小伙伴遇到这么个问题,Mailivery 这个网站登录后,明明提交的表单(邮箱和密码也正确)、请求头等等都没问题,为啥一直重定向到登录页面呢?唉,该出手时就出手啊,我也看看咋回事
实战-行业攻防应急响应 简介: 服务器场景操作系统 Ubuntu 服务器账号密码:root/security123 分析流量包在/home/security/security.pcap 相
背景 最近公司将我们之前使用的链路工具切换为了 OpenTelemetry. 我们的技术栈是: OTLP C
一 同一类的方法都用 synchronized 修饰 1 代码 package concurrent; import java.util.concurrent.TimeUnit; public c
一 简单例子 1 代码 package concurrent.threadlocal; /** * ThreadLocal测试 * * @author cakin */ public class T
1. 问题背景 问题发生在快递分拣的流程中,我尽可能将业务背景简化,让大家只关注并发问题本身。 分拣业务针对每个快递包裹都会生成一个任务,我们称它为 task。task 中有两个字段需要
实战环境 elastic search 8.5.0 + kibna 8.5.0 + springboot 3.0.2 + spring data elasticsearch 5.0.2 +
Win10下yolov8 tensorrt模型加速部署【实战】 TensorRT-Alpha 基于tensorrt+cuda c++实现模型end2end的gpu加速,支持win10、
yolov8 tensorrt模型加速部署【实战】 TensorRT-Alpha 基于tensorrt+cuda c++实现模型end2end的gpu加速,支持win10、linux,
目录如下: 为什么需要自定义授权类型? 前面介绍OAuth2.0的基础知识点时介绍过支持的4种授权类型,分别如下: 授权码模式 简化模式 客户端模式 密码模式
今天这篇文章介绍一下如何在修改密码、修改权限、注销等场景下使JWT失效。 文章的目录如下: 解决方案 JWT最大的一个优势在于它是无状态的,自身包含了认证鉴权所需要的所有信息,服务器端
前言 大家好,我是捡田螺的小男孩。(求个星标置顶) 我们日常做分页需求时,一般会用limit实现,但是当偏移量特别大的时候,查询效率就变得低下。本文将分四个方案,讨论如何优化MySQL百万数
前言 大家好,我是捡田螺的小男孩。 平时我们写代码呢,多数情况都是流水线式写代码,基本就可以实现业务逻辑了。如何在写代码中找到乐趣呢,我觉得,最好的方式就是:使用设计模式优化自己
我们先讲一些arm汇编的基础知识。(我们以armv7为例,最新iphone5s上的64位暂不讨论) 基础知识部分: 首先你介绍一下寄存器: r0-r3:用于函数参数及返回值的传递 r4-r6
一 同一类的静态方法都用 synchronized 修饰 1 代码 package concurrent; import java.util.concurrent.TimeUnit; public
DRF快速写五个接口,比你用手也快··· 实战-DRF快速写接口 开发环境 Python3.6 Pycharm专业版2021.2.3 Sqlite3 Django 2.2 djangorestfram
一 添加依赖 org.apache.thrift libthrift 0.11.0 二 编写 IDL 通过 IDL(.thrift 文件)定义数据结构、异常和接口等数据,供各种编程语言使用 nam
我正在阅读 Redis in action e-book关于semaphores的章节.这是使用redis实现信号量的python代码 def acquire_semaphore(conn, semn
自定义控件在WPF开发中是很常见的,有时候某些控件需要契合业务或者美化统一样式,这时候就需要对控件做出一些改造。 目录 按钮设置圆角
师父布置的任务,让我写一个服务练练手,搞清楚socket的原理和过程后跑了一个小demo,很有成就感,代码内容也比较清晰易懂,很有教育启发意义。 代码 ?
我是一名优秀的程序员,十分优秀!