目录:
和大多数高级语言一样,java也有自动回收内存的机制。给程序员们带来河大的便利,而不用和c++一样,时刻当心内存溢出。那么,jvm的自动内存管理机制是如何实现的?
关键点
- 1.jvm运行时内存区域
- 2.垃圾回收器和垃圾回收算法
- 3.对象的内存分配和回收
1.JVM的运行时内存区域
jvm运行时内存被分配为不同的块,以实现不同的作用。
java 1.7和之前的区域分划;
- java堆
java堆的作用是保存各种实例变量,当使用new创建一个实例后,实例机会被存储在java堆中。java堆是线程共享的。 - 方法区(永久代),更改为元空间
方法区是java1.7之前的产物,也被称为永久代,用于存放类的元数据,运行时常量池也在方法区。在java1.8之后,方法区就被遗弃了,取而代之的是元空间,作用和方法区一样,不过字符串常量池被移动到了java堆中。而且把元空间的数据存放在了本地内存上,而不是堆内存,减少了内存溢出发生的概率。 - 虚拟机栈
虚拟机栈是线程私有的,用于存储局部变量表,操作数栈,方法出口等方法执行时需要的数据。 - 本地方法栈
本地方法栈和虚拟机栈式一样的,不过运行的式本地的方法,hotspot把虚拟机栈和本地方法栈合并在一起 - 程序计数器
程序计数器也是线程私有的,用于记录当前线程锁执行的字节码的行号指示器。 - 运行时常量池
运行时常量池保存了常量值,而且还预设了一些值,比如整性的-128~127就已经被缓存到了常量池中。而对于字符串,所有的字面量会在类加载的时候缓存到常量池。关于字符串常量池的可以看我的另一篇文章java 字符串常量池. - 本地内存
元空间使用本地内存,NIO也会使用本地内存,为了避免在java堆和内核内存直接频繁的复制,开辟一个堆外内存并映射到内核内存的同一个地址,这样能提高效率。不过值得注意的是,这一块内存由于不包含在堆中,所以容易被忽略,导致内存溢出。
2.垃圾回收器和垃圾回收算法
垃圾回收是java这类语言的一个特点。在考虑GC的时候,会有以下问题。
- 哪些内存需要回收
- 什么时候回收
- 怎么回收
哪些内存需要回收(如何判断对象已死亡)
- 引用计数法
引用计数法记录每一个对象的引用次数,当引用次数等于0的时候,对象被判断为需要回收。这种算法的问题在于,如果循环引用,就会导致对象永远不会被清除。 - 可达性分析
可达性算法是从GC root出发,标记所有被引用的数据,GC root是指在虚拟机栈,元空间中的类的静态引用对象和常量的值。如果没有被标记就是没有引用的对象,这样避免了循环引用的问题,当然,效率比引用计数法低一些。java C#等语言都是使用该方法标记死亡的对象。 - 永久代/元空间的垃圾回收
永久代/原空间并不是不回收垃圾,在元空间中,当内存达到参数“MaxMetaspaceSize”设置的值的时候,就会触发对死亡对象的回收。此时的死亡对象包括- 废弃的常量:无引用的常量。(常量池在1.8被移动到java堆中,所以不会在这里回收)
- 废弃的类:所有的实例都被回收,class对象没有被引用,加载该类的ClassLoader已经被回收。
- java中的四种引用
- 强引用:普通的使用new 实例化的对象。只有当引用不存在时才会被回收。
- 软引用:软引用会在内存不足的时候就会不回收,而不用等待无引用的时候才会回收。
- 弱引用:弱引用会在下一次垃圾回收时就会被回收。
- 虚引用:虚引用并不会影响对象的生命周期,唯一的作用是,当对象被回收时收到一个系统通知。
//软引用
SoftReference<T> obj = new SoftReference<>(str);
//弱引用
WeakReference<T> obj = new WeakReference<>(str);
//虚引用
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), new ReferenceQueue<>())
2.1垃圾回收算法
标记清除算法
标记清除法时基于可达性分析的最基础的垃圾回收算法。后续的垃圾回收算法几乎都是对这个算法的改进。算法的过程是先标记死亡的对象,然后再清理内存。这个算法的缺点是:清理速度缓慢,而且会使得空间变得破碎。
复制算法
复制算法是,把内存分成两块,只用其中的一块内存,当使用的那一块内存满了,就会标记活着的对象,并把或者的对象移动到另外一块内存。并把原内存清空。
优点是:清理速度非常块,直接删除一整块内存空间就可以。
缺点是,太耗费内存,只能使用其中的一半内存。所以实际上的使用并不是直接分成两块内存,而是分成三块(Eden占80%,两个Survior,每个占10%)。实际上数据先按照普通的复制算法在两个Survior之间流转,当垃圾清理之后,已经无法存放在一个Survior中了,才会把数据移动到Eden内存块中。
这种算法非常适合新生代的对象,因为新生代的对象都是朝生夕死。每次垃圾回收都只有很少的存活对象,使用快速的复制算法能够高效的回收新生代的对象。
标记-整理算法
对于老年代(生存超过15次垃圾回收的对象),特点和新生代对象相反,存活率高,且新生代晋级到老年代的数量也少。因此,并不适合复制算法。而是使用标记-整理算法,所谓标记整理就是先标记出所有存活的对象,并把所有存活的对象整理到一边。并一次性把位于另外一边的所有数据清空。
这样做的相比于没有整理的好处是,没有碎片空间,且删除效率高。
分代式算法
从上面的描述其实可以知道,虚拟机并不一定使用一样的算法来处理新生代和老年代(G1和ZGC垃圾回收器没有分代),而是对于不同的代使用不同的算法来清除垃圾。
HotSpot回收算法实现
HotSpot是甲骨文公司实现的虚拟机,如果使用的是甲骨文公司的JDK,一般都是使用HotSpot虚拟机。
-
枚举根节点
枚举根节点即找到所有的gc root,在执行这个操作时,必须停止所有的线程,被称之为stop the world(STW),这个时候就需要考虑什么时候停止线程才算安全。
停止线程有两种做法- 抢先式中断:不需要线程配合,直接中断所有线程
- 主动式中断:当标记为需要STW时,线程运行到安全点的或者安全区域就会停止线程。
-
安全点
- 循环的末尾
- 方法返回前
- 调用方法的call之后
- 抛出异常前
-
安全区域:长时间闲置的
- Thread.sleep()
- 线程阻塞
2.2 垃圾回收器
新生代垃圾回收器
- Serial 单线程回收,标记和整理回收都会STW
- ParNew 多线程标记,单线程整理回收,适用于多cpu
- Parallel Scavenge:以吞吐量为目标的垃圾回收,上面两个垃圾回收器是以缩短每次垃圾收集的停顿时间,而吞吐量是以缩短总垃圾回收时间,适用于没有实时性要求的服务,如大量的数据分析。如果误用在实时性要求高的服务,可能会导致偶尔非常卡顿。
老年代垃圾回收器
- Serial Old Serial的老年代收集器,单线程,使用标记整理
- Parallel Old:Parallel Scavenge的老年代,使用标记整理
- CMS:获取最短回收停顿时间为目标的收集器,多次标记,使用并发标记,除了初始标记和重新标记,其他的都是并发标记。这样就节省了标记的时间,不过因为并发标记也占用了CPU资源。cms为了低停顿,并没有使用标记整理,而是直接使用标记清除,虽然停顿时间短了,但是缺点也很明确,就是会产生大量的空间碎片。容易触发full GC
注意老年代和新生代并不是任意搭配的。
不分代
- G1
和前面的垃圾回收不一样的是,G1取消了新生代,老年代的整体内存区域划分。取而代之的是把内存划分为若干块。但是每一块任然是区分代际的,所以这里分为不分代其实没有那么严谨
仍然对新生代使用复制算法(Survivor块),对老年代使用标记整理算法。
特别的是,出现了Humongous块,这个是当一个对象大小超出了分区50%的对象,会被认为是巨型对象,单独放在Humongous块。如果一个块放不下,就会寻找连续的区。如果没有找到连续的区,就会触发一次full GC。
G1垃圾回收器的特点是停顿时间短,而且由于被分成了细粒度的区块,所以可以控制停顿的时间。当时间达到限制时间时,就会停止回收其他区块而等待下一次回收。
-XX:MaxGCPauseMillis:G1的每次停顿限制时间配置,默认200毫秒
3.内存的分配和回收策略
新生代的分配和回收(Minor GC)
新生代的垃圾回收被称为Minor GC,他并不是简单的复制算法。
1.首先,数据会先分配到使用中的Eden区,当Eden满了,就会把存活的对象移动到使用中的Survivor区,并清理掉一个Eden区。
2.当Survivor满了,会把Eden区和Survivor区存活的对象移动到新的Survivor区。
2.但是,如果一个Survivor区已经无法放入所有存活的对象呢?只好通过分配担保机制提前转移到老年区(元空间)。
5.如果老年区也无法存放对象,那么就会触发一次Full GC
老年代的分配和回收机制
进入老年代的时机
- 当存活超过15垃圾回收时,对象就会晋升到老年代。(通过参数-XX:MaxTenuringThreshold设置)
- 大对象会直接进入老年区(G1是进入Humongous),这并不是一个好消息。因为新对象大部分都是朝生夕灭。一个已经死亡的对象,却在老年代占用大量的空间,长时间得不到清除。
- 动态对象年龄判断:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
- 空间分配担保:如上文所述,当Minor gc导致的空间不足,也会触发Full GC。
当老年代空间不够时,就会触发Full gc。由于使用的是标记清除或者标记整理算法,Full GC要比Minor GC慢上很多。
4.内存溢出实战
保存Dump
通过设置 -XX:+HeapDumpOnOutOfMemoryError,让发生内存溢出时自动生成dump文件。
找出占用内存最高的元凶
第一步 使用jps找出虚拟机线程
jps -l
第二步 键入 “top” 然后键入大写的"M"(一定要大写)就会按内存使用率排序,另外如果键入大写的“P”就会按CPU使用率排序。
找到元凶的pid
第三步 将PID转换成16进制
printf 5470
第三步 使用jstack 打印运行中的代码
jstack 10765 | grep ‘0x2a34’ -C5 --color
jstack 后面跟着的是虚拟机id,通过grep查找 16进制的线程ID
- -C5 表示显示5行
- –color 为找到的作色
也可以使用下面的命令直接保存成文件
jstack 10765 >> 123.txt
//-l 额外输出锁信息
jstack -l 10765 >> 123.txt