什么是JVM?
JVM是可运行JAVA代码的虚拟计算机,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆和一个存储方法域。
运行过程:JAVA源文件——>编译器——>字节码文件(Class文件)——>JVM——>机器码
一.JVM内存模型
堆:JVM内存管理最大的一块,被线程共享,目的是存放对象的实例,几乎所有的对象实例都会放在这里,当堆没有可用空间时,会抛出OOM异常.根据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回收器进行垃圾的回收管理
方法区:又称非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据
栈:又称方法栈,线程私有的,线程执行方法是都会创建一个栈阵,用来存储局部变量表,操作栈,动态链接,方法出口等信息.调用方法时执行入栈,方法返回式执行出栈
本地方法栈:与栈类似,也是用来保存执行方法的信息.执行Java方法是使用栈,执行Native方法时使用本地方法栈.
程序计数器:保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行Java方法服务,执行Native方法时,程序计数器为空.
二.垃圾回收GC
GC采用了分代策略来进行垃圾回收,原因有以下几种:
- 不同对象的生命周期:在程序运行过程中,有些对象是短暂存在的,而有些对象可能会存活很长时间。分代GC策略基于“弱代假说”,即大多数对象会很快变得无用,而少数长寿命对象会存活下来。通过将内存划分为新生代和老年代,GC可以对这两种对象进行差异化处理,从而提高效率。
- 优化内存管理效率:分代GC策略允许JVM针对不同的内存区域使用不同的GC算法。例如,在新生代中,对象的回收频率较高,所以通常使用快速的GC算法;而在老年代,对象的回收频率较低,因此可以使用更复杂、但影响范围较小的GC算法。这种策略可以根据各代的特性来优化内存管理效率。
- 降低GC暂停时间:长时间的GC暂停可能会对程序性能造成严重影响。通过分代,JVM可以更频繁地、但持续时间更短地执行新生代的Minor GC,而少数时候执行影响范围更大的老年代的Major GC。这有助于降低GC的暂停时间,提高程序的响应速度。
- 更精细的资源分配:不同的应用程序可能对内存的使用模式有不同的需求。分代机制允许开发者更精细地控制内存的分配和回收,例如通过JVM参数调整新生代和老年代的大小或调整晋升老年代的阈值,从而更好地适应不同应用的需求。
- 适应不同应用的需求:随着Java技术的发展,其在服务器、桌面、移动设备等多种环境中都有广泛应用。这些应用对GC性能的需求各不相同。分代GC策略为JVM提供了足够的灵活性,使其能够根据不同应用环境的特点进行调优。
1.分代回收
GC根据对象的特点对内存做了内存分代,JDK8以前主要包括新生代、老年代和永久代。JDK8之后主要是新生代和老年代。
新的对象总是会被分配到新生代中,新生代空间满了之后,执行一次GC(minor GC),同时提升幸存对象的年龄属性。达到一定的阈值之后,对象会提升到老年代中,老年代空间满了之后,也会执行一次GC (major GC)。这是分代回收的一个大概流程,不过在新生代中并不是只有空间满了之后才会执行GC回收操作。
如上图所示,新生代分为三块:Eden(伊甸)区、S0区、S1区。看见伊甸区这个名字应该会有点预感吧,新生代的初始新对象就是分配到Eden区,Eden空间满了之后执行一次GC动作。幸存对象移动到S0区,对象年龄标识+1。下一次GC动作执行后,Eden区和S0区的幸存对象会移动到S1区,对象年龄标识+1。再之后执行GC,就是S1——>S0——>S1这么个流程。这个过程中幸存对象的年龄标识达到一定的阈值就会如之前说的,提升到老年代空间中去。
JVM内存新生代Eden区和Survivor区的比例是8:1:1,其中,Eden区占用80%,Survivor占用20%,并且划分为大小相同的两部分,这样划分的原因是为了解决内存碎片的问题。
2.minor GC相关逻辑
执行15次GC后对象进入老年代:这个15是默认配置,可以通过JVM参数 -XX:MaxTenuringThreshold来设置
动态年龄判断:一批对象的总大小大于这块Sunvivor区域内存大小的50%(由-XX:TargetSurvivorRatio参数指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了。
这里这个年龄不是上面15次这个数字。举个例子,现在S0区年龄1、2、3、、、n的对象合计占了当前区域的50%,那么大于等于n的对象就直接进入老年代。
大对象直接进入老年代:-XX:PretenureSizeThreshold 指定大于该数值的对象直接进入老年代,避免在新生代的Eden和两个Survivor区域来回复制,产生大量内存复制操作
MinorGC后对象太多无法进入Suvivor区如怎么办:部分对象直接进入老年代
老年代空间分配担保:详情如下图
需要注意的是,如果执行fullGC后老年代空间大小还是不够用的话,就会抛出OOM内存溢出异常了。
3.常见垃圾回收器
回收器 | 回收区域 | 回收算法 | 回收器特性 |
---|---|---|---|
Serial | 新生代 | 标记-复制 | 单线程 |
ParNew | 新生代 | 标记-复制 | 多线程 |
Parallel Scavenge | 新生代 | 标记-复制 | 多线程 |
Serial old | 老年代 | 标记-整理 | 单线程 |
Parallel old | 老年代 | 标记-整理 | 多线程 |
CMS | 老年代 | 标记-清除 | 并发多线程 |
G1 | 划分Region,新生代/老年代 | 整体标记-整理、局部Region标记-复制 | 并发多线程 |
4.GC常见算法
引用计数器:假设有一个对象A,任何对象对A进行引用,那么对象A的引用计数器+1,当引用失效时,对象A的引用计数器-1,当对象A的引用计数器为0时,就说明对象A没用被引用,那么就可以进行回收。
标记-复制:将内存空间分为两份,每次只使用其中一份。垃圾回收时将存活对象复制到另一个空间,然后清除该空间,交换两个内存角色完成垃圾回收。新生代的垃圾回收就是用这种方式。
标记-清除:分为两个阶段。第一阶段标记,从根节点开始标记引用的对象;第二阶段清除,未标记的对象就是垃圾对象,可以清除。
标记-整理:同样分为两个极端。第一阶段标记时一样的;第二阶段开始清理前先将标记对象压缩到内存的一端,然后清理边界以外的对象。这种方式可以有效减少内存碎片化的问题。当然,多了一步移动对象的动作,对效率有一定的影响。