JVM 调优实战

内存分配

1.被 线程共享:

堆:用来存放对象的,几乎所有对象都放在这里,被线程/栈共享

堆也是垃圾回收的主要区域,又叫GC堆

存放被NEW出来的对象

方法区:方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。该区域是被线程共享的。

2.非共享:

虚拟机栈:用来存栈帧的,对象的引用。随着线程结束内存就释放,不需要垃圾回收。

内存溢出,通过减小最大堆和栈容量来换取更多的线程。

本地方法栈:和虚拟机栈一样,但是只为本地方法服务

扫描二维码关注公众号,回复: 14584189 查看本文章

程序计数器:是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。程序中的分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。

堆he 栈区别:

堆:堆空间一般由程序员来分配,可以由垃圾回收机制来回收。一般用来存放new创建的对象和数组。  

栈:栈是后进先出的存储空间,一般用来存储基本类型的数据和对象的引用。

关于栈和栈帧,我们做个小结:

每个JVM线程有一个私有栈,栈在线程创建的同时被创建。

栈由许多帧组成,也叫 "栈帧"

每次方法调用都会创建一个栈帧

换句话说,当一个Java/Scala/JVM方法被执行时:

当方法被执行时,一个新的栈帧被创建并用来给这个方法存储数据

栈帧大小各不相同,取决于方法的参数、局部变量和算法

当一个方法被执行时,程序只能访问当前栈帧中的数据,你能看到的只有栈顶的帧

垃圾回收

jdk8环境下,默认使用 Parallel Scavenge(新生代)+ Serial Old(老年代)

引用计数法:根据当前对象是否存在引用来判断,被引用了,则+1,失去引用-1,如果引用为0,那么就直接回收.缺点:最大的问题,是很难解决对象之间互相引用的情况。


可达性算法(引用链法):从一个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。对象可达性分析之后,发现没有与GCRoots相连接,此时会被第一次标记并筛选。第二次,若还是发现没有被引用,则清除。

可作为GCRoot的对象包括以下几种:

    a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。 

    b.方法区中的类静态属性引用的对象。 

    c.方法区中的常量引用的对象。 

    d.本地方法栈中JNI本地方法的引用对象。

并发场景,三色标记算法(jdk11)

此方法就是根据可达性分析算法的思想进行标记与回收的,**CMS和G1**垃圾回收器就是用的此方法进行标记的

黑色:根对象,或者该对象与它的子对象都被扫描

:对象本身被扫描,但还没扫描完该对象中的子对象

白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象

最开始所有对象都是白色的,第一步把其中全局变量和函数栈里的对象置为灰色(1图)。第二步把灰色的对象全部置为黑色(2图),然后把原先灰色对象指向的变量都置为灰色(3图),以此类推。等发现没有对象可以被置为灰色时,所有的白色变量就一定是需要被清理的垃。当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化多标漏标的情况就有可能发生。

  1. CMS的解决方案:增量更新(IncrementalUpdate):在并发标记的过程中,所有赋值对象的操作都会被JVM给记录下来放在一个集合里,通过后面的重新标记过程将所有被赋值(例如下面代码的a就是被赋值对象)的对象全部置为灰色,灰色对象是未被扫描完成的,所以会被继续扫描,这个时候,在并发标记时做了赋值被引用的对象例如下面代码的d就是被引用对象)就会被标记成灰色,在之后的并发处理时就不会被回收掉。

  1. G1的解决方案(SATB):原始快照(SATB):在并发标记时,会将所有的赋值null操作的原始快照(也就是被赋值为空的对象的原始引用的对象)放到一个集合里面,在重新标记的时候,会将集合里面所有被抛弃的对象全部赋值为黑色,这样gc就不会回收,就算并发标记结束之后,它依然是垃圾,这个垃圾只会变成浮动垃圾,在下一次gc的时候会回收

  1. Incremental Update算法和SATB算法对比

        SATB 算法是关注引用的删除。(B->C 的引用),而Incremental Update 算法关注引用的增加。(A->C 的引用)。变成灰色的成员还要重新扫,重新再来一遍,效率太低了

        所以 G1 在处理并发标记的过程比 CMS 效率要高,这个主要是解决漏标的算法决定的。

为什么CMS用增量更新,G1用SATB:

因为增量更新之后会重新深度扫描,G1是以region的方式存储对象,而CMS是以一个连续的老年代存储对象,G1会涉及到跨代扫描,G1的代价相对于CMS要高。

而且G1较CMS更强调用户体验,重新深度扫描会加大STW时间,所以G1选择原始快照。

另外如果一个对象从非垃圾对象变成了垃圾对象,这就称为浮动垃圾,浮动垃圾不会被gc清除掉,但是由于浮动垃圾不会影响到gc的运算过程,所以在下一次gc时回收掉即可,另外,针对并发标记以及并发清理开始后产生的对象,通常的做法是直接当成黑色,本轮gc不会对其进行清除,这类对象也可能成为垃圾,也算是浮动垃圾的一种。

垃圾回收器(三种方法:标记清除、复制算法、标记整理):

标记-清除:算法顾名思义,主要就是两个动作,一个是标记,另一个就是清除。

标记就是根据特定的算法(如:引用计数算法,可达性分析算法等)标出内存中哪些对象可以回收,哪些对象还要继续用。

标记指示回收,那就直接收掉;标记指示对象还能用,那就原地不动留下。

CMS收集器是基于“标记—清除”算法实现的,时间快,响应快,会产生碎片

响应优先选择CMS

标记-整理:首先标记出所有需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

G1从整体来看是基于“标记—整理”算法实现的收集器,是基于“复制”算法实现的

吞吐量高选择G1

标记-复制:将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存 使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

复制算法的缺点显而易见,可使用的内存降为原来一半。

Serial收集器:是一个单线程的收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

ZGC和G1区别:

1.G1的每个Region大小是完全一样的,而ZGC的Region大小分为3类:2MB,32MB,N×2MB。

2.ZGC不分代

3.ZGC非常快

4.虽然ZGC属于最新的GC技术, 但优点不一定真的出众. ZGC只在特定情况下具有绝对的优势, 如巨大的堆和极低的暂停需求. 而实际上大多数开发在这两方面都不太成问题(尤其是在服务器端), 而对GC的性能/效率更在意. GC技术这些年其实并没有很大的发展, 也就是说没有银弹, 某些方面具有优势肯定是牺牲其它方面换来的, ZGC也很明显, 官方的设定目标是不损失超过15%的G1GC性能, 也就是说从吞吐速率上肯定无法跟G1相比了, 更没法跟完全STW的GC去比

-XX:+UseConcMarkSweepGC表示启用了CMS垃圾回收器

-XX:+UseG1GC 表示启用了G1垃圾回收器

-Xms 等价于-XX:InitialHeapSize 表示初始化堆大小

-Xmx 等价于-XX:MaxHeapSize 表示最大堆的大小

-Xss 等价于 -XX:ThreadStackSize 线程堆栈的大小

JVM垃圾回收机制

JVM中(堆)共划分为三个代:年轻代、年老代和持久代


年轻代:存放所有新生成的对象,占1/3空间

Eden区被对象填满时,就会执行Minor GC,并把所有存活下来的对象转移到其中一个survivor区。


年老代:在年轻代中经历了N次垃圾回收仍然存活的对象,将被放到年老代中,故都是一                           些生命周期较长的对象,占2/3空间

老年代内存被占满时进行垃圾回收。老年代的垃圾收集叫做Major GC,Major GC通常是跟full GC是等价的,收集整个GC堆。

Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。

 
持久代:用于存放静态文件,如Java类、方法等。

其中用System.gc()强制执行的是年老代.

总结对象的一生

对象从开始new出来,先经过栈判断能否在栈上分配,能的话直接入栈,不用直接pop掉无需经过GC,如果不能就需要判断是不是大对象,是的话直接进入old区,不是的话,再判断能否再TLAB中分配,无论能否再TLAB中分配最终都是再Eden中分配,只是效率不同,进入Eden的对象如果没有熬过YGC的检验就会提前结束生命,最终进入Old区的对象,等待FGC的“降临”,等待着结束着对象的一生

猜你喜欢

转载自blog.csdn.net/SmallTenMr/article/details/123971330