简单介绍JVM的GC过程
在说到java虚拟机的垃圾回收机制前,应该先知道虚拟机里面有什么区域,哪些区域要在运行过程过程中时不时的对其进行垃圾清除。有哪些区域1、程序计数器占用虚拟机内存很小,功能是给字节码解释器寻址用的。在它工作时通过修改计数器值来选取下一条需要执行的字节码指令地址。像分支、循环,跳转、异常处理、线程恢复等功能都需要计数器完成。程序计数器属于“线程私有”的内存。虚拟机的多线程是通过...
在说到java虚拟机的垃圾回收机制前,应该先知道虚拟机里面有什么区域,哪些区域要在运行过程过程中时不时的对其进行垃圾清除。
有哪些区域
1、程序计数器
占用虚拟机内存很小,功能是给字节码解释器寻址用的。在它工作时通过修改计数器值来选取下一条需要执行的字节码指令地址。像分支、循环,跳转、异常处理、线程恢复等功能都需要计数器完成。
程序计数器属于“线程私有”的内存。虚拟机的多线程是通过线程轮流切换并分配CPU的执行时间的方式来实现的,任何某个时刻,CPU中一个内核都只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,这样就互不影响,独立存储。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。也不会出现OOM的异常区域。
废话太多,就记得它类似CPU里面的程序计数器,负责指出下一条指令的地址。
2、Java堆
堆这块区域算是虚拟机中最大的一块内存区域了,属于线程共享区域。虽然是线程共享区域,但内部可能会划分出多个线程私有的分配缓冲区(TLAB)。Java堆内存空间在物理空间可以不连续,但是逻辑空间上必须连续。
堆区域主要是存放对象实例和数组,细分一下Java堆会分为新生代(Eden、FromSurvivor、ToSurvivor)和老年代。分代是为了更方便的在Java运行过程中产生的垃圾进行回收。内存不够用会出现OOM异常。
这个区域很重要,因为比较特别,我们垃圾回收主要都是在这个区域操作。
-
-Xms20m 设置堆最小内存20MB
-
-Xmx20M 设置堆最大内存20MB
-
当-Xms和-Xmx值相同表示不允许堆内存进行扩展
-
-
-
-Xmn5m 表示新生代内存为5MB
-
-XX:NewRatio=3 表示新生代占堆内存1/3
-
-XX:SurvivorRatio=8 表示Eden:2个Survivor = 8:2
上面是对jvm的部分区域内存进行数值设定,这些参数都是有main函数的String[] args去接收的。在IDEA中则要在一个类的VM options处填写即可。
3、Java虚拟机栈
该内存也是线程私有,生命周期和线程一致。描述的是 Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)和 returnAddress类型(指向了一条字节码指令的地址)。会出现StackOverflowError和OutOfMemoryError。
-
-Xss128k 表示设置栈内存设置为128KB
-
该参数和本地方法栈参数共用
4、本地方法栈
类似上面Java虚拟机栈,这块内存区域不是给Java方法用的,是给Native方法使用的。在以前Java刚出来的时候,为了方便和其他语言相互调用而在虚拟机区域划分了一小块内存。
例如我们使用JNI方法去调用C、C++等语言编写的代码时使用。
5、方法区
Jdk1.8版本之前很多人都说这是永久代,该区域属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
其中运行时常量池是属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String的 intern())都可以将常量放入池中。内存不够用也会抛出OOM异常。
-
-XX:PermSize=10M 表示方法区最小内存10MB
-
-XX:MaxPermSize=10M 表示最大内存10MB
回收哪些区域
废话,当然都是要收集的呀。因此要进行垃圾收集(GC)的区域很多,但是最主要也最不好GC的地方是Java堆区域。
Java虚拟机栈都是局部变量,方法结束的时候已经没有存在的意义,它会自动喊GC器过来帮它处理掉。但是我们Java堆的存放的共享变量,有数组有类对象实例,不能像栈那样用完就扔,因为我们也不太清楚谁用过了就没用了,谁还没用也可以当垃圾处理。
所以JDK每个版本更多都是在对堆内存做优化GC算法、以及新的GC器设计。
如何判断对象是垃圾
1、引用计数法
给对象中加一个计数器,有人引用它时就count++;引用失效时就count--,当系统开始GC的那一时刻,如果对象的计数器为0就准备被处理了。
优点:实现简单,判定效率高,大部分情况下是一个不错的算法
缺点:很难解决对象之间相互循环引用的问题
2、可达性分析
类似老鹰捉小鸡游戏,通过一系列的 “GCRoots’”的对象当作老母鸡,从这些节点用引用链连着的节点是存活的,就像小鸡躲在老母鸡后面。当一个对象到“GCRoots”没有任何引用链相连的时候说明对象不可用,就像掉队的小鸡会被老鹰***掉。
GC Roots对象:虚拟机栈的引用对象;方法区类静态属性引用对象或常量引用的对象;本地方法栈Native方法引用的对象。
小鸡E、小鸡F和小鸡G将要被老鹰***掉,注意,这里是将要。实际过程中GC器的线程是一个优先级很低的线程。一般cpu很少去调用它,所以掉队的对象都会被放在垃圾队列中准备被清理。在等待死神到达前,如果有其他对象把Object5对象引用回来就不用被处理了。就好比老母鸡发现小鸡掉队赶紧过去让小鸡进队。其实后续还有很多步骤,这里不再深入。
GC过程
为什么要进行GC,因为在程序运行过程中由于内存不够用呀。现在的Java虚拟机在GC过程中都是分代GC的,分代就像我们面对不同的人会以不同的态度去面对他们,面对新生代和老年代我们也要用不同的处理方式去处理。
新生代的GC叫MinorGC,频繁但回收快;老年代的GC叫做MajorGC/Full GC,需要停止所有虚拟机线程(stop the world)去GC,次数少但耗时。
新生代里面的大部分对象存活时间超短,基本都会在下一次MinorGC触发前就没用了。当Eden(伊甸园)区满的时候就触发MinorGC时,GC掉那些用可达性分析算法判断出来的垃圾对象。老年代的对象在老年代堆内存满的时候也会进行GC,那老年代对象怎么来的?
新生代对象如何进入老年代
1、超大对象直接进入老年代,避免在MinorGC使用复制算法时浪费时间;想象成你公司老板的儿子来公司上班,身为HR的你是让他去打杂(新生代)呢还是做办公室喝茶(老年代)呢?
-
-XX:PretenureSizeThreshold=3145728
-
表示对象超过3MB直接送入老年代
2、在多次Mainor GC没被处理的对象,熬到一定岁数就给这类对象移动到老年代中,
-XX:MaxTenuringThreshold=10 表示躲过10次Mainor GC就直接进入老年代;想象成你在国企熬了十几年终于熬到经理职位了,不用太担心被辞职了。为啥说太,因为老年代也会被GC的。
-
-XX:MaxTenuringThreshold=10
-
表示躲过10次MainorGC就直接进入老年代
3、如果在 Survivor空间中相同年龄所有对象大小的总和超过Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到规定年龄才能进入。
GC器
老鹰种类有点多,内容很枯燥,这里不做介绍了
案例分析
公司服务器是一个4核8G的配置的服务器,拿出4g内存给JVM用,其中3g内存分给堆内存。部署的项目是一个网上商城,项目是使用分布式微服务框架,功能就是用来卖东西的,在高峰期每秒下单超300个。请问下面的配置方式可以吗?
-Xms:3072m –Xmx:3072m –Xss1m –XX:PerSize=256m-XX:MaxPermSize=256m
3g的堆内存去应对每秒产生的300个订单对象,会出现问题不?
下面数据是按最大值选取,这样才能保证我们系统不出意外。买东西下单用的是订单系统,订单类里面要包含订单id,订单号,订单生成时间,卖家、买家等乱七八糟的基本属性,int类型、String类型、Long等等。我们假设有1024个字节凑成1KB,一个订单对象占内存1KB,高峰期每秒产生300KB的订单对象。
订单系统在生成订单对象是还需要其他系统的对象做支持,像库存呀、有没有优惠呀、买送多少积分金币呀,会员打折吗等等其他系统对象,我们放大20倍(为啥放大20倍,因为要凑数才能让后面GC出问题)。
当订单对象在其他对象帮助下,终于生成了,这时每秒就产生了6000KB的内存。当订单生成的时候,用户不一定下单,可以会去看其他商品然后再回来查询自己的订单或者取消订单等这些操作,再放大10倍(接着凑数)。
此时每秒系统产生了约60MB的内存对象,然后这些对象都先放在Eden区,我Eden区800多MB的不怕,但是过了13s左右差不多就满了。这时候触发MainorGC,这个过程很快,毫秒级别的。案例应该对系统没啥影响呀。
影响在于GC的时候出问题了,因为第一次GC,会把Eden有用的对象放在From区,可能有60MB的对象在GC开始的时刻还没断开GCRoots就直接被复制到了From区,其他都被清理了。按照新生代进入老年代的第三条规定,这60MB的对象是直接被移到了老年代里面了。
换句话说,大约每隔13s左右有60MB下一秒就是垃圾的对象被移动到了老年代。大约7分多钟老年代就满了,老年代开始full gc了。Full GC很可怕,stop the world。过程很慢,而且系统这段时间什么外部操作都不能执行,就是卡住了。
一般网点高峰期都是中午或者晚上上班族不上班的时候,这段时间都是持续几十分钟或者更长,每隔7分多钟系统卡住1-2秒。我的天!!!准备辞职吧。
解决办法就是在新生代GC的时候不让那60MB进入老年代,方法就是扩大From区内存,不然它进入老年代。
-Xms:3072m –Xmx:3072m –Xmn2048m –Xss1m –XX:PerSize=256m -XX:MaxPermSize=256m
配置就是在原来基础上添加了一个 -Xmn2048m,作用是设置堆内存中新生代占2GB的内存,这样老年代默认就是1GB。
Edne变成2倍,也就是26秒左右Edne满了,GC的时候60MB对象跑到了From中,但是不会到老年代去。然后第二次GC的时候,这个在From区的60MB早就脱离GCRoots被清理,然后新生成的60MB对象放在了To区。每次新生代GCFrom和To只能有一个被使用。
最后,我们再给系统设置一个定时任务,每天大半夜用户差不多都在睡觉的时候让系统自动full GC一下。这就是为啥项目去生产环境做新功能上线的时候都要在大半夜去。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_37713821/article/details/103331735
更多推荐
所有评论(0)