1、堆和栈是JVM内存中的两个不同区域,作用也不同。对象的分配是在堆上进行的。
栈中包含一系列的栈帧,是来存储局部变量、操作数栈、动态链接、方法出口等信息。
2、堆是线程共享的区域,栈是线程私有的区域。
防止内存中出现多份同样的字节码。如果没有 PDM 而是由各个类加载器自行加载的话,用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都能加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证,同时,也会给虚拟机的安全带来隐患。
双亲委派机制能够保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。
这样可以保证系统库优先加载,即便是自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载,从而保证安全性。
对象由对象头、实例数据、对其填充三部分组成。
对象头包括1、对象运行时数据(GC分代年龄、符号引用等) 2、指针类型: 指向 对象的类的元数据类型 3、如果是一个对象数组,则还包括 数组的长度信息
实例数据就是 对象的真正有效的信息,包括从父类继承来的以及自定义的
对齐填充是指 对象的起始地址必须是8字节的整倍数。
运行程序中没有任何指针指向的对象,就是需要被回收的垃圾。
计算机内存有限,如果不回收,就会一直占用内存空间,最终导致OOM。
早期垃圾回收:使用者自己手动申请空间,手动释放空间。
缺点一旦忘了释放,会造成内存泄露。
自动内存管理: 优点: 降低内存溢出与内存泄露风险,让程序员更专注于业务逻辑。
缺点: 一点出现问题,难以定位与改错。
垃圾回收发生在堆和方法区。 重点是堆。
频繁发生在年轻代(堆)、较少发生在老年代(堆)、基本不动 元空间(方法区)。
标记阶段、清除阶段
主要有两种方法:
①、引用计数法:
定义:对每个对象保存一个计数器属性,如果有其他对象引用了,则+1,引用失效后-1,若 为0,则表示可以回收。
优点:实现简单、便于识别、判定效率高,回收没有延迟。
缺点:
1、计数器也占用空间,增加空间消耗。
2、每次引用变化,都要计算。增加时间消耗。
3、无法解决循环引用问题。(根本缺点)
java没有采用引用计数法,python使用了,python采用手动解除和使用弱引用weakref的方式来应对以上缺点。
②、可达性分析法:
定义:以根节点集合GCRoots为起始点,按照从上至下的方式搜索被根节点集合直接或间接连接的对象,视为可达对象。非可达对象则视为可回收对象。
优点:可以解决循环引用问题
①、虚拟机栈中引用的对象
②、本地方法栈Jni中引用的对象
③、方法区类静态属性所引用的对象
④、方法区中常量所引用的对象
⑤、所有被同步锁synchronized持有的对象
⑥、java虚拟机内部的引用
除此之外,根据垃圾回收器及垃圾回收区域的不同,也有一些可能被临时作为GCRoot的对象。
可达性分析法必须在一个能够保证一致性的快照中进行
可触及、可复活、可回收
一个对象是否可回收要经过两个标记阶段。
1、经过可达性分析法 判断 对象是否可达,若可达,则不可收回。若不可达,标记为 不可达。
2、对不可达对象进行判断,是否重写了finalize()方法,若没有重写,则 标记为可回收。
若重写了 ,则执行,执行后对象可能被复活,则不可回收。若没有被复活,则可回收。
3、finalize()只能被执行一次。若已执行过,则第二次运行到此时直接将对象标记为可回收。
1、标记-清除算法
2、复制算法
3、标记-压缩算法
1、算法描述: ①、标记可达对象 ②、对堆内存进行遍历,清除非可达对象
2、缺点:①、效率不算高 ②、GC时,停止整个应用程序 ③、清理出的内存空间是不连续的,容易产生内存碎片。
详细说下第三个缺点原因:
标记-清除算法的清除并不是真正的清除,而是将可以被清除的对象地址保存在空闲的地址列表中,下次有新对象需要加载时,逐个判断空闲列表中每个垃圾的空间是否够存放,如果够,则存放。
并不是一种新的算法,而是一种算法思想,不同代采用不同的垃圾收集算法,组合使用。如年轻代使用 复制算法,老年代使用标记-压缩算法。
在以上提到的算法,GC中都会STW,如果时间过长,会影响用户体验。因此让垃圾回收线程每次只回收一小片区域,与应用线程交替运行,直到垃圾回收完成。
优点是降低了延迟,单次STW时间很短。缺点是频繁的线程和上下文切换消耗,使得垃圾回收的成本上升,系统吞吐量下降。
System.gc()、 Runtime.getRuntime().gc()
显式的调用垃圾回收,但不能保证程序立刻进行GC,原因 “”安全点、安全区域的概念“”
内存溢出(OOM)
原因:没有空闲内存,垃圾回收器也无法提供更多的内存。
没有空闲内存的原因,jvm的堆内存设置的不够,可以通过-Xms、 -Xmx调整
代码中创建了大量的大对象,并且长时间不能被垃圾回收。
内存泄露(Memory Leak)
描述: 一个对象不会被程序用到了,但垃圾回收器却不能回收他。
举例:1、一个需回收的对象 在单例模式中的单例对象中被引用。因为单例模式的声明周期跟应用程序一样长,所有这个对象就无法被回收。2、一些提供了close方法的资源未关闭导致内存泄露。如数据库连接、网络连接(socket)和io连接
STW (stop the world)
gc发生过程中,整个应用程序线程都会被暂停等待,没有响应。
垃圾回收的并行与并发
并行: 同一时间点可同时执行
并发: 同一时间段同时执行,同一时间点只有一个执行。
安全点、安全区域
应用线程执行过程中并非在所有地方都能够停顿下来开始GC,只有在特定的位置才能进行GC,这些位置就被称为安全点或安全区域。通常会选择应用程序一些执行时间较长的指令作为安全点。如方法的调用、循环跳转、异常跳转
如何保证GC发生时,可以使应用程序线程在最近的安全点停顿下来呢?
1、抢先式中断。
首先中断所有线程,如果还有线程不在安全点上,就恢复线程,让线程跑到安全点
2、主动式中断
设置一个中断标志,让线程运行到安全点时主动轮询这个标志,为真,则自己中断挂起。
引用的四种类型
强引用: 引用存在就不会被回收。
软引用: 引用存在,但内存不足时,被回收
弱引用: 引用存在,但只要垃圾回收器发现了它,就会被回收
虚引用: 对对象的生命周期起不到任何影响,只是为了让对象被收回时,能够接受系统通知。
1、从语法层面看变化
2、从API层面看变化
3、底层优化:主要指JVM的优化
1、按线程数分 :串行垃圾回收器、并行垃圾回收器
2、按工作性质分: 并发式垃圾回收器、独占式垃圾回收器
3、按内存碎片处理方式分: 压缩式、非压缩式
4、按工作的内存空间分: 年轻代的垃圾回收器、老年代的垃圾回收器
1、吞吐量
2、延迟
3、内存占用情况
年轻代垃圾回收器:
serial GC : 第一款垃圾回收器,用于年轻代,单线程工作,他是所有收集器中额外内存消耗最小的;对于单核处理器或者处理器核心数较少的环境来说,可以获得最高的单线程手机效率,适用于运行在客户端模型下的虚拟机。
Parnew : serial 的多线程版本
Parallel Scavenge:
目标是达到一个可控制的吞吐量。
主要参数:
-XX:MaxGCPauseMills 最大单次垃圾回收消耗时间: >0的毫秒数
-XX:GCTimeRatio :控制运行用户线程时间与垃圾回收时间的比值
-XX:+UserAdaptiveSizePolicy 自适应调节策略,虚拟机自动调整新生代大小及伊甸园去与幸存区比例。
老年代垃圾回收器:
serial old : serial的老年代版本,单线程,使用标记-整理算法
Parallel old : 在JDK1.4中发布,在jdk1.6后, Parallel成为默认的垃圾回收器.多线程收集。使用标记-整理算法
CMS: 以获取最短回收停顿时间为目标的收集器。使用标记-清除算法。
优点:并发收集、低停顿。
缺点:1、对处理器资源十分敏感。处理器资源很少时,慢
2、无法处理浮动垃圾
3、基于标记-清除算法,因此会有内存碎片化问题
工作过程可以分为
1、初始标记: 标记GCRoots节点直接关联的对象。此过程会STW
2、并发标记: 从GCRoots根节点遍历整个对象图。此过程不会STW
3、重新标记: 由于并发标记阶段不会STW,所以引用可能会变动,所以需要修改部分标记。此过程会STW
4、并发清除: 清除标记阶段标记的可回收对象
G1 : 里程碑式的垃圾收集器,开创了收集器面向局部收集的设计思路和基于Region的内存布局形式和具有优先级的区域回收方式,基于标记-复制算法,jdk9以后成为 默认的服务端垃圾回收器。
目标是 在延迟可控的情况下获取尽可能高的吞吐量
优点:停顿更小
缺点:
1、需要耗费更多内存:需要java堆的10%-20%的内存来维持收集器工作
工作过程可以分为
1、初始标记: 标记GCRoots节点直接关联的对象。此过程会STW
2、并发标记: 从GCRoots根节点遍历整个对象图。此过程不会STW
3、最终标记 此过程会STW
4、筛选回收 此过程会STW
G1基于标记-复制算法,CMS基于标记-清除算法。
因此CMS会有内存空间碎片化问题,G1不会有。
但G1的内存占用和运行额外执行负载比CMS高。
内存占用来说,二者都使用卡表来处理跨代指针,但G1是基于Region的,所以卡表更为复杂,开销更大。
因此各有优缺点,需根据实际情况判断,据作者经验,在小内存应用上,CMS的表现大概率好于G1,在大内存应用上,G1更容易发挥优势。这个java堆容量的平衡点通常在6g-8g之间。
Epsilon、
Shenandoah: 第一款非Oracle开发的垃圾回收器
ZGC : 基于Region内存布局,不设分代,使用了读屏障、染色指针,和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器,JDK11中引入
核心重点就是 可并发的标记整理算法的实现。
多重映射:将多个不同的虚拟内存地址映射到同一物理内存地址上;是使用染色指针技术的伴生产物
优点: 支持“NUMA-Aware”的内存分配。
“NUMA-aware” ,非统一内存访问架构,是一种为多处理器或者多核处理器的计算机所设计的内存架构。
支持NUMA的内存分配的仅有 parallel scavenge 、zgc
ZGC是未来发展的趋势,目前让处于实验阶段。
主要应该从以下三个方面考虑:
1、应用程序的主要关注点是什么?
如果是服务端的数据分析、科学计算等,则更为管住吞吐量
如果是SLA应用,更关注延迟
如果是客户端或嵌入式,更为关注 内存占用
2、运行应用的基础设施如何?
3、使用jdk的发行商是什么?
垃圾收集器参数?
见另一篇博客
jps 作用:jvm process status tools,用于检测系统中所有的hotspot虚拟机进程
jstat 作用 : 监测jvm运行时的状态信息
jmap 作用: 生成 堆 dump文件
jhat 作用: 分析堆dump文件
jstack 作用: 生成虚拟机当前时刻的线程快照
jinfo 作用: 实时查看和调整jvm运行参数
jdk自带的: 1、jconsole 2、 jvisualvm
第三方的: 1、 MAT 2、GChisto
1、调整堆大小
-Xmx 设置堆的大小
2、设置新生代大小,新生代不能太小,否则会有大对象涌入老年代
-XX:NewSize 新生代大小
-XX:NewRatio 新生代和老年代占比
-XX:SurvivorRatio 伊甸园区与 survivor区占比
3、根据实际情况自主设定垃圾回收器
-XX:+UseParNewGC 新生代用
-XX:+UseConcMarkSweepGC 老年代用