内存分配
1.被 线程共享:
堆:用来存放对象的,几乎所有对象都放在这里,被线程/栈共享
堆也是垃圾回收的主要区域,又叫GC堆
存放被NEW出来的对象
方法区:方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。该区域是被线程共享的。
2.非共享:
虚拟机栈:用来存栈帧的,对象的引用。随着线程结束内存就释放,不需要垃圾回收。
内存溢出,通过减小最大堆和栈容量来换取更多的线程。
本地方法栈:和虚拟机栈一样,但是只为本地方法服务
程序计数器:是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。程序中的分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。
堆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图),以此类推。等发现没有对象可以被置为灰色时,所有的白色变量就一定是需要被清理的垃。当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。
- CMS的解决方案:增量更新(IncrementalUpdate):在并发标记的过程中,所有赋值对象的操作都会被JVM给记录下来放在一个集合里,通过后面的重新标记过程将所有被赋值(例如下面代码的a就是被赋值对象)的对象全部置为灰色,灰色对象是未被扫描完成的,所以会被继续扫描,这个时候,在并发标记时做了赋值被引用的对象例如下面代码的d就是被引用对象)就会被标记成灰色,在之后的并发处理时就不会被回收掉。
- G1的解决方案(SATB):原始快照(SATB):在并发标记时,会将所有的赋值null操作的原始快照(也就是被赋值为空的对象的原始引用的对象)放到一个集合里面,在重新标记的时候,会将集合里面所有被抛弃的对象全部赋值为黑色,这样gc就不会回收,就算并发标记结束之后,它依然是垃圾,这个垃圾只会变成浮动垃圾,在下一次gc的时候会回收
- 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的“降临”,等待着结束着对象的一生