JVM内存
1.方法区 – 存放字节码文件
2.程序计数器-- 记录字节码文件执行到那个位置
3.Java虚拟机栈 – 程序方法压栈出栈 方法中的局部变量
4.Java堆内存 – 方法执行中创建的对象存储
5.常量池 – static修饰的一般常量存储空间 图示未体现
6.内存其他区域 本地方法栈 native修饰的一些变量
新生代老年代
新生代:创建和使用完后的对象立马放的区域 注意:刚开始创建的对象大部分优先分配在新生代;什么时间变为老年代?
老年代:创建后需要长期使用的对象
新生代转老年代:默认新生代对象15次垃圾回收还未被回收掉的对象会转到老年代
新生代触发垃圾回收条件:新生代内存满了,创建新的对象时触发 YOUNG GC
注意:新生代触发垃圾回收后还有很多对象,新创建的对象可能会直接进入老年代内存;
特大超大的对象直接进入老年代
老年代触发垃圾回收条件:老年代内存满了,触发垃圾回收
永久代:其实就是方法区
为什么区分:
垃圾回收算法有关,新生代的垃圾回收算法和老年代的垃圾回收算法不同;
JVM内存分配相关参数
为什么了解参数:频繁的GC容易造成线上问题,有需要的话我们要结合业务和估算来进行JVM调优
相应的springboot上面查找
启动jar文件指令:jar -Xms512M -Xmx512M … -jar App.jar
调优前需要考虑
- 堆内存估算方式:每个对象占的字节,每秒多少对象?多久一次GC合适?选择内存多大的服务器?多少台服务器?
- 方法区内存设置:一般上线时设置几百MB是够用的,但是要观察后续的运行情况
- 栈内存的设置:一般默认512kb到1MB
JVM类加载
1.加载时机 —用到才加载
2.双亲委派模型 防止类重复加载
3.验证 准备 解析 初始化都做了那些事情
3.1 验证 检验字节码文件符合虚拟机要求
3.2 准备 为加载的类分配内存空间 静态变量分配空间并给出一个初始值;
3.3 解析 符号引用替换为直接引用
3.4 初始化 变量赋值 静态代码块执行 子类初始化是父类必先初始化
JVM垃圾回收机制
1.回收机制 实例对象没有任何一个方法局部变量、局部变量、常量指向它,一般会被认为垃圾;
思考:方法去中的类会被垃圾回收吗?什么时间回收?
会。
满足三种情况下会回收:
1.该类在堆的所有实例对象君被回收;
2.加载该类的ClassLoader被回收
3.该类的Class对象没有任何引用
回收时机
1.被局部变量、静态变量直接强绑定的对象在垃圾回收时是不会被回收的,被类间接引用的软引用在内存满了的情况下是会被回收的;
软引用示例:
// student 相当于弱引用
private static People people = new People(new Student());
回收算法
复制算法
创建的对象首先存在上方内存区域,触发GC后,把还在使用的对象复制到下方的内存区域,上方剩下的垃圾对象整体回收。
好处:防止一个区域造成的大量内存碎片化情况
缺点:需要一般的长空内存区域,内存利用率不高
复制算法优化 Eden区和Survivor区
1.创建对象首次放在Eden区,第一次触发GC后,还在使用的对象放在第一个Survivor区,清空Eden区的垃圾对象;
2.第二次触发GC后,Eden区和放Survivor的使用对象复制到为空的另一个Survivor区域,清空Eden和Survivor内的垃圾对象;
3.上述第二步循环;
好处:内存利用率提升。
思考:一些万一情况
1.垃圾回收后,survivor放不下?
2.超大对象,survivor放不下?
老年代相关
新生代进入老年代的情况
1.多次young GC后,依然存活的对象(默认15次)
2.动态年龄判断–survivor区域存活对象大于50%了,此时触发young GC后大于平均年龄也会直接进入老年代;
3.大对象直接进入老年代(JVM参数可以设置参考大小值)
4.young GC后存活对象超过Survivor区域大小,直接进入老年代;
老年代内存分配规则
1.在执行young GC之前都会先检查老年代的内存大小,是否能存放所有的新生代对象;如果大于老年代内存空间,没设置平均新生代每次像老年代同步对象大小时,会直接触发FULL GC(对老年代垃圾对象进行回收)
注意:FULL GC算法比young GC算法慢十倍,会造成系统卡顿等情况;
老年代回收算法
标记算法:标记那些对象是垃圾对象,然后把这些对象清理掉;还要防止内存碎片化(可通过参数设置决定几次FULL GC后压缩内存,降低内存碎片化的影响),算法复杂;
所以所谓的JVM优化,就是尽量让对象在新生代回收,避免频繁的进行老年代垃圾回收,同时也要避免新生代频繁的垃圾回收;
JVM垃圾回收器
serial&serial old垃圾回收器
分别用来回收新生代和老年代的垃圾对象
单线程模式,开启后会让运行系统卡死,现在Java后台几乎不用;
ParNew &CMS垃圾回收器
ParNew:一般新生代垃圾对象回收
- 垃圾回收时,系统程序工作的所有线程均停掉,多线程开启回收
CMS:老年代垃圾回收
CMS四步:
- 初始标记 系统工作线程全部停止,速度很快
- 并发标记
- 重新标记 系统工作线程全部停止,速度很快
- 并发清理
多线程垃圾回收,性能较好,一般生产常用
CMS垃圾回收器采用的是垃圾回收线程和系统线程尽量同时进行,防止很长时间的Stop the World
G1垃圾回收器
统一收集新生代和老年代,性能更好;
特点:把内存拆分为很多小的region,新生代和老年代各自对应一些region,回收的时候尽量以最小的时间回收垃圾对象最多的region,尽量保证我们指定的垃圾回收时系统的停顿时间;
最大特点:可以设置一个垃圾回收最大的预期停顿时间;
一般内存比较小的机器使用parNew垃圾回收新生代一般没有问题,一次就几十毫秒停顿,但是当机器内存很大,比如32核64G的,堆内存可能时32G,而新生代区域7G,可以想一下每次垃圾回收可能系统的停顿时间就上去了,此时用G1垃圾回收,设置系统停顿时间往往是最好的;
Stop the World
垃圾回收时,系统要暂停不能创建新对象,这就是Stop the World;
不同的是:单线程垃圾回收还是多线程垃圾回收,垃圾回收的频率和时间;
万恶的OOM
会造成OOM的区域
1.永久代内存溢出(存放方法区的区域);
jvm参数设置示例:
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m
上述参数是给永久代分配512M的存储区域;
什么情况下会造成永久代区域的内存溢出:
1.jvm参数设置采用的默认设置,默认设置永久代一般只分配几十M的大小,系统稍微大点就会触发oom
2.cglib动态生成类写的有点问题,大量的动态类创造总成;
2.每个线程的虚拟机栈内存溢出;
线程一般默认的是1m
栈内存一般是方法的出栈和入栈,如果方法深度比较大,每个方法中的局部变量比较多可能会造成这个情况
最常见的原因:递归使用不小心;
一般不会出现;
3.堆内存空间溢出;
触发条件:有限的堆内存中存放了过多的存活对象造成;
一般两种场景下会出现:
1.系统高并发,请求量过大,导致存活对象太多,继续放入新对象时放不下了崩溃;
2.一些对象的引用没有及时取消;