Java最叼的地方就在于它的垃圾回收,同时也是Java语言相比C++ 需要程序员手动管理内存的语言的最大优势,首先还是得先聊清楚JVM的内存模型
JVM 内存模型

注意,java8及之后的JVM内存模型与之前的有了区别,比较大的改动就是移除了方法区,原本的方法区是属于堆内内存,而元空间则直接使用的物理内存,也就是说元数据区的大小不再依赖于堆内存大小。主要原因还是目前的程序中普遍存在很多运行时生成的class对象,导致原本的方法区已经不够用,容易触发 方法区gc
- 程序计数器
- 一块较小的内存区域,存储线程信息。主要是为了线程切换后能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,互不干扰,唯一一个不会抛出 OOM异常的区域
- 虚拟机栈
- 与程序计数器类似,也是线程私有。虚拟机栈的内存模型非常重要 ,理解java的线程模型务必要理解虚拟机栈的内存模型。这里简单介绍一下,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧(stack frame)用来存储 局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表用于存储 基本数据类型,对象引用(不绝对,开启逃逸分析的话,也会在栈上分配内存)
- 会抛出两种异常
- 超过虚拟机规定的最大栈深度,抛出
StackOverflowError OutOfMemryError stack
- 超过虚拟机规定的最大栈深度,抛出
- 本地方法栈
- 与虚拟机栈类似,仅仅是执行的是Native方法
- 堆
- 堆是java内存空间最大的一块,后面也会着重画下重点,并且堆是线程不安全的,每个线程公用堆内存,几乎所有的对象都在堆上分配内存,但也不绝对(比如上面提到的栈上分配)。当前主流的虚拟机基本都把堆划分为两个区域 1.新生代 2.老年代 其中新生代,又可分为
Eden,From Survivor ,To Survivor垃圾回收也是主要在这快区域 - 会抛出
OutOfMemryError Heap space
- 堆是java内存空间最大的一块,后面也会着重画下重点,并且堆是线程不安全的,每个线程公用堆内存,几乎所有的对象都在堆上分配内存,但也不绝对(比如上面提到的栈上分配)。当前主流的虚拟机基本都把堆划分为两个区域 1.新生代 2.老年代 其中新生代,又可分为
- 元空间
- 与Java堆一样,也是线程共享的区域。主要存储虚拟机加载的类信息,在java7之前我们又把它叫做方法区,当时常量池也存在这里(现在已经在堆内存中)。垃圾收集在这个区域比较少出现
- 会抛出
OutOfMemryError PermGen space
对象的内存结构
这里主要介绍下对象头信息<br />这里要理解的是,对象头是一个可变的长度,并且存的数据再每种状态下都是不同的,下表详列了每种状态下对象头所存储的信息<br />

这里跟java锁升级优化有关
垃圾回收
着重讲一下垃圾回收<br />在讲垃圾回收之前,先说一下什么样的对象会被垃圾回收?<br />目前主要有两种方法判断 对象是不是可被回收
- 引用计数
- 可达性分析
引用计数比较好理解,每有一个对象被持有引用加1,当引用计数为0时,就通知垃圾收集器回收。这样子的算法比较简单,但有循环引用的问题,循环应用的对象并不会被回收。实际上JVM也不是用的引用计数法
可达性分析理解起来相对抽象一下,但是目前主流的虚拟机都是用的可达性分析算法。算法的核心就是 挑选 GC Root 做可达性分析,当GC Root不可达该对象时,说明该对象要被回收,所以该算发的核心就是选择合适的GC Root 。可用作GC Root的对象主要有
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 常量引用的对象
- 本地方法栈应用的对象
引用分为三种类型
- 强引用 不用多说
- 软引用
- 系统将要发生内存溢出之前会被回收
- 弱引用
- 只要垃圾回收器 工作了就会被回收
- 虚引用
- 没什么用。。。
垃圾收集算法
标记清除
顾名思义,就是标记了再清除<br />缺点:效率低 会产生内存碎片
复制清除
为了解决效率问题,引出了复制算法。主要原理就是将内存分为两块,每次只使用一块,当一块用完了,就将还存活着的对象复制到另一块内存中,然后将剩下的全部清除。<br />这个算法优点就是 不会产生内存碎片,效率高,但是空间利用率不高(新生代用的就是复制算法,不同的是新生代将内存分为3块 Eden,From Survivor ,To Survivor)比例默认是 8:1:1
标记整理
标记整理算法和标记清除类型,不同的是 清除之后,会进行内存整理,以减少内存碎片
分代收集
分代收集严格来说并不是一种收集算法,仅仅只是一种思想。它把内存分成各个区域,每个区域都使用合适的算法去做垃圾回收,如新生代使用复制算法,老年代使用标记整理<br /><br />
垃圾收集器
Serial 收集器
新生代单线程收集器(复制清除算法),收集的时候会 stop the world,一般都不怎么用了,除了在一些 Java桌面应用的CLient端还有用外
Parnew 收集器
新生代多线程收集器,serial 收集器的多线程版本,其他跟serial收集器完全一样。清理的时候也会 stop the world。一般用来搭配使用 CMS 收集器
Parallel Scavenge 收集器
和 Parnew 收集器非常相似,都是新生代多线程收集器,但是Parallel Scavenge与其他收集器的关注点不一样,它不关注 停顿时间点(stop the world),而关注吞吐量
Serial Old 收集器
serial 收集器的老年代版本,单线程收集器,使用标记整理算法,不怎么用。用来做CMS的后备
CMS
基于标记清除。CMS 是目前公司使用的 老年代收集器,也是大多数Web应用所采用的收集器。原因在于CMS收集器是以最短回收停顿时间为目前的收集器,对于用户体验来说会比较友好。<br />CMS 收集器包含四个步骤
- 初始标记 (stop the world 但是时间很短)
- 并发标记
- 重新标记 stop the world
- 并发清除
缺点
- 对CPU敏感
- 无法处理浮动垃圾,重新标记的时候也会产生垃圾
- 内存碎片 标记清除算法的通病
G1收集器
目前最屌的了
- 停顿时间力求最短
- 标记整理
- 不需要配合其他收集器,一个就可以搞定 新生代 和老年代
- 可预测的停顿 (??这是)
回收策略
年轻代 MinorGC<br />老年代 MajorGC<br />Full Gc 会至少伴随一次 MinorGC
- 大对象直接进入老年代
- 长期存活对象进入老年代 (默认是15次 -XX:MaxTenuingThrehold = 15 设置)
-
空间分配担保
- 在发生MinorGC前,虚拟机会检查老年代最大用连续空间是否大于新生代所有对象总空间,如果这个条件成立,则可以确保这次MinorGC是安全的。如果不成立虚拟机则会检查 HandelPromotionFailure 设置值是否允许担保失败。如果允许则继续检查老年代最大可用空间是否大于历次新生代晋升到老年代对象的平均大小,如果大于,则尽管有风险也会进行一次MinorGC。如果小于或则不允许担保失败,则直接进行 Full GC
<br />
虚拟机监测工具
jps
- jps -v 输出启动详细信息
- jps -l 输出主类全名
- jps -m 输出main函数
- jps -q 输出 进程id
下面的命令都会用到 jps 查出的进程id(vmid)
jstat
用于监视虚拟机运行状态
- jstat -class vmid 监测类装载耗时
- jstat -gc vmid 监测堆状况 GC时间等
jinfo
查询配置信息
jmap
生成内存快照
- jmap -dump:format=b,file=heapdump.phrof vmid 导出dump文件,也可以加系统参数
-XX:+HeapDumpOnOutOfMemoryError或则通过-XX:+HeapDumpOnCtrlBreak在程序运行时按Crtl+Break生成 dump文件
jhat
分析 dump文件的工具,不过有点难用
jstack
打印线程堆栈
- jstack vmid
一般分析堆栈异常时用到,常用的命令组合是
1.找出占用CPU占用最高的线程id
top -bn1 -H -p <pid>
2.将找的线程id转换成16进制
printf "%x \n" <tid>
3.通过jstack定位问题代码
jstack -l <pid> | grep -A10 <tid>
- visualvm
这个比较好用,也很强大
有些时候,这些工具真的用不到。但并不代表,你不需要去了解这些