目录
G1的解决方案:SATB (Snapshot At the Begining)
什么是垃圾?
在c语言中,分配内存空间是手动的,用完释放也是需要手动删除的
在java中,你new了一个对象就会在堆内存中创建内存空间,如果该对象没有其它引用指向,就视为垃圾。
可以理解为,创建了一个对象,该对象在堆中的地址是会在栈中被引用,相当于有一跟线在牵着,如果那根线断了,就说明它是个垃圾了
怎么判定是垃圾?
引用计算器
每个对象都有一个计算器,被引用时就计算器就加一,为零时说明就是个垃圾了;但是会出现一堆垃圾出现的情况,也就是说对象之间循环指向,但是没有一个对象是被栈中所指向的;
根可达算法(Root Searching)
如上图所示,找到根对象,如果根对象中的线断了,那么其它的都视为垃圾。解决上面产生的问题
常用的垃圾回收算法
- 标记清除(Mark-Sweep)
- 拷贝(Copying)
- 标记压缩(Mark-Compact)
标记清除
内存中的角色:存活对象、可回收(垃圾)、未使用
找到垃圾,标记垃圾,清除垃圾
造成的问题:内存碎片化严重,这里清一块,那里清一块,就会造成内存的不连续性
拷贝
将内存一分为二,一边使用,一边用于拷贝,在第一块区域中,找到存活对象,并将它拷贝到另一块区域中,然后就可以把第一块区域的数据全部清掉了;相当于腾了个地方吧
好处:没有碎片
坏处:浪费空间 例如:10个g的内存只能当5个g的内存使用
标记压缩
标记垃圾时,同时将内存中的所有存活对象放在一端,然后清除所标记的垃圾;这样清理完成后就不会产生碎片;
好处:不会碎片化,没有浪费空间
坏处:效率较低
JVM内存分代模型(用于分代垃圾回收算法)
- 新生代(new -yong)
- 老年代(old)
- 部分垃圾回收器使用的模型,有些垃圾回收器是没有新老年代之分的,比如G1(注意区分垃圾回收器与垃圾回收算法
- 在jdk1.7时还有一个永久代 ,1.8时叫元数据区
- 永久代/元数据用来装什么的:Class对象
- 永久代必须指定大小限制,所以可能会造成永久代的内存溢出,因为有大小设置,一旦动态代理对象时,产生的class大小很可能就会超出指定的大小
- 元数据区就可以设置大小,也可以不设置,无上限(受限于物理内存)
- 字符串常量:1.7是存在永久代中;1.8存在堆内存中
- 方法区(MethodArea):是一个逻辑概率,1.7→永久代;1.8→元数据
堆内存逻辑分区
- 分为新生代和老年代,它们在堆内存中所占的比例是1:2
- 新生代使用拷贝垃圾回收算法,老年代使用标记压缩算法
新生代:
- 分为三个区域:Eden、From Survivor、To Survivor 比例:8:1:1
- 新new出来的对象如果大小合适,就存放在eden区,如果过大,直接放入tenured区
- YGC:也就是清理eden区的垃圾;
- 在第一次YGC时,会将eden区中所有的存活对象拷贝到From Survivo中,那么在eden区中就可以直接标记清除所有的垃圾了
- 再YGC时,将eden+From Survivo区中的存活对象放入To Survivor区中,然后直接干掉eden和From Survivo区中的垃圾
- 再YGC时,就会将eden+To Survivor中存活对象转入From Survivor中,然后在干掉垃圾,这样就构成了一个回收循环;也就是说,新生代中内存的使用率是90%,因为,总有一个Survivor是用不到的,它不参与垃圾回收,用于保存存活对象;
老年代:
怎样才会进入老年代呢?
答:每经过一次GC那么对象的"年龄"就会加一,当年龄达到15时就会进入老年代(CMS 16),或者当对象过大也会直接进入老年代;
老年代存放的都是顽固分子
如果老年代装不下了怎么办?
答:进行FGC,使用FGC会有STW,所以尽量不要产生FGC(无法避免)
FGC
- 全局GC,也就是YGC+OGC,新生代和老年代一起回收
GC Tuning(Generation)调优
- 尽量减少FGC
- MinorGC=TGC
- MajorGC=FGC
常见的垃圾回收器
十种垃圾回收器
新生代:ParNew、Serial、Parallel Scavenge(PS)
老年代:CMS、Serial Old、Parallel Old(PO)
其它四种不区分新老年代
新老年代的回收器都是配合使用的(图中有虚线相连的都是可以搭配使用的)
- JDK1.0的时候默认使用的是:Serial+Serial Old
- JDK1.8的默认使用:PS+PO
- ParNew+CMS没有被默认使用过,因为CMS存在很大的缺陷
线上运行的时候基本使用的就是上面这三种组合,像后面的4种很少使用
接下来就聊一聊垃圾回收器的实现
Epsilon:
- 啥也不做,两种作用:调试和确认不用GC就可以干活
Serial+Serial Old
- 当多个线程并发运行产生的垃圾达到一定量时,就会触发GC,当执行GC时,所有的线程都会停止,等待GC完毕才能继续执行;那么停止的这段时间就叫STW(Stop to World)停止整个世界
- GC时使用的是单线程回收,所以在早期JDK时内存还不够大,可以使用。但是,到现在,内存都是十几几十个g的了,所以还使用单线程效率会非常的慢
PS+PO
- 与上面的实现是相差不多的,最大的区别在于GC时不再是单线程,而是使用的多线程
ParNew+CMS
- CMS在GC时分为四个阶段:初始标记→并发标记→最终标记→并发清理
- 初始标记:会STW,但它只标记根上的对象,所以时间会很短
- 并发标记:是运行线程与GC同时运行,所以会产生误标的情况,产生的问题:
- 例如在这段时间中:开始标记一个对象不是垃圾,但在GC去标记其它对象的时候,该对象的引用没了,那么它就应该被标记为垃圾。这种情况属于漏标,问题不是很大,只要在下一轮GC找到并清除就可以,也就是浮动垃圾
- 或者一开始标记是垃圾,在GC标记其它的对象时,又有对象指向了该对象,那么它就应该不是个垃圾。这种情况就比较严重,把不是垃圾的对象给清除了,会造成空指针异常。
- 重新标记:STW,时间也会很短,因为在上个阶段就标记的差不多了,错误会很少。所以这样就能完整的标记垃圾
- 并发清理:一边清理一边运行
- 总结:最消耗时间的还是并发标记,因为做的事情最多的还是在这个阶段,但是是并发的,所以影响不大
CMS最大的问题在于:如果触发了FGC的话,他就会使用Serial Old清理,那么Serial Old的问题就在于它是单线程的,如果一旦执行Serial Old,那么整个程序很有可能就卡死不动了,因为是单进程的,所以GC所需要的时间可想而知。这也是它没被JDK有默认使用阶段的原因。
CMS使用的算法:三色标记(并发标记时使用的算法)
三色标记的颜色:白色、灰色、黑色
白色:还没有被标记的对象
灰色:标记过的对象,但没有完全被标记,该对象中的子对象可能还未被标记,在下一轮中会继续查找
黑色:完全标记过的对象,不会在标记
在并发标记的过程产生的问题,如下图所示:
第一种情况:
在并发标记中A→B→C,如果B指向C的线突然断了,那么c就会成为垃圾,但是不影响,因为垃圾回收是一轮一轮的,只要下一次能回收掉就可以了。这种垃圾就称为浮动垃圾
第二种情况:
A→B→C,B指向C的线断了,然后A直接指向了C;产生的问题:因为A是黑色的,所以C就不会被标记(漏标),那么C就会被回收掉,程序就会出错。
CMS的解决方案
如果黑色有新的指向,那么就把黑色降级为灰色
但是该解决方案还是会产生漏标
假设灰色标记中两个属性,当A线程在扫描完第一个属性后,该属性重新指向了一个白色标记,那么这时候灰色标记就应该还是灰色的,但是,当A线程扫描完所有属性后,发现没问题,就会把该标记变成黑色的,这样也产生了漏标现象。
所以CMS在最后阶段,就需要重新扫描,STW,所以效率有时候也不会很好
G1的解决方案:SATB (Snapshot At the Begining)
当B指向C的指针消失时,会将该指针存储到栈中,当下一轮扫描的时候,就会在栈中找有没有指针,如果有,就拿出来在执行,如果指向存在就标记不是垃圾,否则就真是垃圾了。
ZGC和Shenandoah的解决方法:颜色指针
一个java的指针是64bit,其中42个是指向真正的java对象的,18个是没有用的,那么就还剩下4个bit,一个颜色指针就是一个bit,用来做状态区分,也就是当引用该指针时,根据颜色指针的状态做相对应的操作
为什么ZGC管理4个T的内存?
2^42=4T
用42位代表一个对象的地址
JVM调优
调优前的基础概念
- 吞吐量:用户代码时间 /(用户代码执行时间+垃圾回收时间)
- 响应时间:STW越短,响应时间越好
所谓的调优,就是看你需要调哪一方面的,是吞吐量啊还是响应时间,根据具体业务而定
什么是调优
- 根据需求进行JVM规范调优和预调优
- 优化运行JVM运行环境(慢或者卡顿)
- 解决JVM运行过程中出现的各种问题(OOM)
调优所使用的基本指令
标准: -开头,所有的HotSpot都支持;例如:java -version
非标准: -X开头,特定版本HotSpot支持特定命令
不稳定: -XX开头,下个版本可能取消
- java -Xms... :最小堆(用于指定堆的最小值)
- java -Xmx... :最大堆(用于指定堆的最大值)
- java -XX:+PrintFlagsWithComments :打印所有的参数并带解释(只有JVMdebug版本能用)
- java -XX:+PrintFlagsFinal | wc -l :打印所有的jvm运行时需要的指令(七百多个指令/728)
调优场景 :liunx环境下
-
项目背景:贷款公司中的风险评估 根据风险评估判断可以贷多少钱给用户 根据模型评估
模拟代码展示:
public class FullGC_Probleam {
/**
* 银行卡用户信息类
*/
private static class CardInfo {
/*
数据有:用户花的钱,用户的姓名,用户的年龄,用户的生日
根据这些数据计算该用户具体能代多少钱
*/
//用户花的钱
BigDecimal price = new BigDecimal(0.0);
String name = "张三";
int age = 5;
//生日
Date birthdate = new Date();
//该方法想象成匹配的过程
public void m() {}
}
//线程池new出来,
private static ScheduledThreadPoolExecutor executor = new
ScheduledThreadPoolExecutor(50,
new ThreadPoolExecutor.DiscardOldestPolicy());
public static void main(String[] args) throws Exception {
executor.setMaximumPoolSize(50);
//一直循环进行数据的模型匹配
for (;;) {
//每隔100ms,就评估一百个用户的风险值
modelFit();
Thread.sleep(100);
}
}
/**
* 模型匹配
*/
private static void modelFit() {
//从数据库中获取用户数据
List<CardInfo> taskList = getAllCardInfo();
//遍历查询到的所有用户信息
taskList.forEach(info -> {
//创建线程池,每过一段时间就执行m()方法
executor.scheduleWithFixedDelay(() -> {
//do sth with info
info.m();
//退2 3毫秒进行计算
}, 2, 3, TimeUnit.SECONDS);
});
}
//方法进行计算
/**
* 获取用户信息,相当于是从数据库中获取的数据
* 然后封装到数据库实体类中,并放入集合中
*
* @return
*/
private static List<CardInfo> getAllCardInfo() {
List<CardInfo> taskList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
CardInfo ci = new CardInfo();
taskList.add(ci);
}
return taskList;
}
}
- 执行指令启动程序:java -Xms100M -Xmx100M -XX:+PrintGC com.heima.article.controller.v1.FullGC_Probleam
- jps:查看进程id
- top:查询所有进程的使用情况
- jstack pid(进程id):查看该进程具体运行的方法状态(改指令对这个调优没有关系,可以用于死锁的情况)
- jinfo pid:查看进程的信息,堆大小,执行命令等等
- jstat -gc pid:查看gc信息
调优使用的工具是阿里的arthas图形化工具,他是使用JVM的JVMTI工具接口实现的
首先启动arthas:java -jar arthas-boot.jar
看到如下就说明启动成功了:
- 使用:sc *xxx(可以指定包名) 查找需要查看的类
- 使用:sm *xxx 查找需要查看类中的方法
- trace 查看一个方法的状态
- monitor 可以查看一个方法传入的值与返回的值
- jmap -histo 9547 | head -20 在jvm中可以查看指定线程数的线程具体数据信息,arthas中没有该指令
上面程序在运行一段时间后,会频繁的FGC,回收不掉对象,产生内存泄漏及内存溢出问题。注:产生多的内存泄漏就会造成内存溢出问题。内存泄漏是造成内存溢出的一个原因。
使用:jmap -histo 9547 | head -20 指令多次查看线程中内存的使用情况,看是哪个线程使用的内存变化较大,这样就可以快速定位到是那个对象出现的问题,在使用arthas中的指令就可以定位到是那个方法那个进程的问题了。
重点注意:jmap指令执行时会STW,如果项目已经发布了那么该指令是不好被使用的。因为如果内存很大,那么STW的时间就会很长,那么怎么程序就会出现卡顿现象。这种现象是不被允许的
可以这么说:
- 就说我们的项目是在集群状态下的,即使停了一台服务器对我们的业务也是没有影响的。
- 将系统放入测试环境进行压测。
- 在运行程序时设置参数HeapDump,OOM的时候会自动产生堆转储文件。但是,该方法太晚了,出现了OOM才执行,所以不太好。
设定垃圾回收器打印日志参数时需要注意的事项
不单单使用一个日志文件进行存储,这样会导致日志文件难以查看。不方便阅读,所以在设置日志文件时:需要指定多个日志文件循环存储(日志文件大小限制建议20M)。例如:A日志、B日志、C日志满了,那么就会覆盖A日志在进行存储。
指令:-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation - XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails - XX:+PrintGCDateStamps -XX:+PrintGCCause
在生产环境中日志文件,后面日志名字,按照系统时间产生,循环产生,日志个数5个,每个大小 20m,这样的好处在于整体大小100M。能控制整体文件大小。
注:一般记录日志的时候,记录日志文件 如果只有一个日志文件,肯定不行,有些服务器可能一天产生的日志文件就上T,连着30天就是30T,产生量大,如果查找问题,查找不到问题所在。 其实这个工作是运维干的活儿 。