写作这篇文章的目的,是想以浅显易懂的描述让大家重新认识JVM,主要包含下面几个方面
- JVM架构逻辑
- JVM堆内存结构模型
- JVM垃圾回收机制/常用垃圾回收器(Garbage Collector)
- JVM调优
1.JVM架构逻辑
这张图描绘了JVM和系统调用之间的关系。
- 开始是将编译好的字节码文件,经过类加载器,将我们的字节码加载到内存当中,就生成了Class对象。
- 绿色表示的方法区和堆区是所有线程共享的内存区域。
- 而沙棕色表示的java栈、本地方法栈和程序计数器是运行时线程私有的内存区域,相互独立开。
- 方法区(Method Area):存放类信息、常量、静态变量
- 堆(Heap):存放对象实例和数组
Java堆是jvm里内存最大的一块,几乎所有的实例对象都是在这里分配内存,同时堆也是垃圾收集器管理的主要区域,后面有详细描述堆内存的结构。
当方法区和堆区无法满足内存分配需求,也无法再扩展时,就会抛出我们最不想要的OutOfMemoryError异常。
2.怎么找到垃圾?
JVM怎么知道堆里的对象是垃圾呢,有两种方式:
- 引用计数法
每个对象有一个引用计数属性,新增一个引用时计数加1,释放引用时计数减1,计数为0时标记为可以回收。
这个方法虽然简单,但无法解决对象相互引用的问题。
- 根查找/根可达(Root Searching)
从GC Roots开始向下搜索,搜索走过的路径称为引用链。到一个对象没有引用链相连时,则根不可达,该对象不可用标记为垃圾。
画个简单的示意图,像这样:
顺着线一直能找到的就不是垃圾,线断的(根不可达)的是垃圾。
3.垃圾回收算法
垃圾回收算法,一直以来就三种,不同垃圾回收器只是用的算法不一样。
- Mark-Sweep(标记清除)
- Copyting(复制)
- Mark-Compact(标记整理)
Mark-Sweep(标记清除)
在一块内存中,将要回收的数据,标记直接清除。
坏处:碎片化,清除后内存一个洞一个洞的。
Copyting(复制)
把内存线分为两半,每次只允许用一半。进行垃圾回收的时候,将一半内存中的存活的数据直接复制到另一块内存中,然后清理掉之前的内存区域。
效率是最高的,但会造成内存浪费。
Mark-Compact(标记整理)
将内存中的对象,标记集中移动到内存的一边进行清理,把有用的对象落到前面去(整理)。
效率比Copy略低,好处是把空间让出来了,后面的大对象可以直接在后面分配内存。
4.常用的垃圾回收器有哪些?
算法是GC的策略,而垃圾收集器,就是根据垃圾回收算法来进行垃圾处理的具体实现。
到现在为止,垃圾回收器一共有10种,如下图所示:
生产用的最多的是jdk1.8,默认是Parallel和Parallel Old。
- Serial
Serial(单线程)垃圾收集器是最基本、发展历史最悠久的收集器。
垃圾回收时,stop-the-word 停掉所有工作线程(几十毫秒),采用Copy复制算法。
- Serial Old
Serial Old(单线程)是应用在老年代的垃圾收集器,算法是Mark-Compact 标记整理算法。
- ParNew
ParNew垃圾收集器是Serial收集器的多线程版本,实现算法和Serial一样是Copy复制算法。
- Parallel Scavenge
Parallel Scavenge与前面两种新生代的收集器相比,不可以和CMS组合使用,使用算法也是Copy复制算法。它与前两种最大的区别是,他关注的是吞吐量而不是延迟。
- Parallel Old
Parallel Old是Serial Old的多线程版本,应用在老年代的收集器,实现算法和Serial Old一样是Mark-Compact 标记整理算法。
- CMS
Concurrent Mark Sweep,并发标记清理。在线程执行过程中也可也进行垃圾收集。分为四个过程:1.初始标记 2.并发标记 3.重新标记 4.并发清理
- G1(Garbage First)
JDK7就已加入JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术。同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。
G1采用的标记整理算法。
- ZGC
zgc是jdk11中要发布的最新垃圾收集器。完全没有分代的概念,先说下它的优点吧,官方给出的是无碎片,时间可控,超大堆。
- Shenandoah
Shenandoah是一款concurrent及parallel的垃圾收集器,跟ZGC一样也是面向low-pause-time的垃圾收集器,不过ZGC是基于colored pointers来实现,而Shenandoah GC是基于brooks pointers来实现的。
- Epsilon
Java11新的Epsilon垃圾收集器。
Epsilon(A No-Op Garbage Collector)垃圾回收器控制内存分配,但是不执行任何垃圾回收工作。一旦堆被耗尽,JVM就直接关闭。设计的目的是提供一个完全消极的GC实现,分配有限的内存分配,最大限度降低消费内存占用量和内存吞吐时的延迟时间。
5.GC的演化
其实垃圾回收的演化,是随着现在设备内存大小的增进而演进的。
- 最初几兆-几十兆
Serial单线程STW垃圾回收。
- 几十兆-上百兆1G
Parallel 并行多线程进行垃圾回收,但线程不是越多越好,有线程切换耗时。
- 几G-几十G到上T
Concurrent GC
6.堆内存逻辑分区
在目前1.8中用的最多的,叫分代管理的办法。
画了个堆内存逻辑分区的示意图,如下:
无论内存多大,将内存一分为二。默认新生代和老年代的比例是1:2。新生代比例8:1:1,这个新生代的比例,是根据统计学结果得出的,因为在YGC能回收90%的垃圾。新生代使用算法Copying复制算法,老年代使用的Mark Compact标记整理算法。
到一个对象诞生的时候,先放在新生代,当一个对象被清理了很多次都没被回收就会放到老年代。不同回收收集器都不一样,一般是15或6次。
JVM调优主要就是通过参数调比例。
7.垃圾回收过程
- 新生代回收 Young GC (YGC)
- 老年代回收 Old GC (OGC)
- 当YGC和OGC都发生,就叫Full GC
垃圾回收过程如下:
1.新诞生的对象放在Eden(伊甸园)区
2.当伊甸园区满了,会清理垃圾,把活着的对象复制到survivor(幸存者区),然后把整个Eden清空,这就是一个完整的YGC。
3.如果再来一次,则把Eden活着的和survivor的对象复制到survivor2,同时把Eden和survivor1清空内存,2个survivor中始终保证有一个是空的。
8.JVM调优
JVM调优其实就是通过参数控制内存大小,优化垃圾回收过程。
如下图所示:
最后汇总一下JVM常用参数配置:
类加载设置
- -XX:+TraceClassLoading 类加载日志
- -XX:+TraceClassUnloading 类卸载日志
堆设置
- -Xms 初始堆大小
- -Xmx 最大堆大小
- -XX:NewSize=n 设置年轻代大小
- -XX:NewRatio=n 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
- -XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
- -XX:MaxPermSize=n 设置持久代大小
收集器设置
- -XX:+UseSerialGC 设置串行收集器
- -XX:+UseParallelGC 设置并行收集器
- -XX:+UseParalledlOldGC 设置并行年老代收集
- -XX:+UseConcMarkSweepGC 设置并发收集器
并行收集器设置
- -XX:ParallelGCThreads=n 设置并行收集器收集时使用的并行收集线程数
- -XX:MaxGCPauseMillis=n 设置并行收集最大暂停时间
- -XX:GCTimeRatio=n 设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)
并发收集器设置
- -XX:+CMSIncrementalMode 设置为增量模式,适用於单CPU
- -XX:ParallelGCThreads=n 设置并发收集器年轻代收集方式为并行收集时,使用的收集线程数
垃圾回收统计信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename