JVM垃圾处理方法
-
标记-清除算法
- 标记阶段:先通过根节点,标记所有从根节点开始的对象,未被标记的为垃圾对象
- 清除阶段:将所有未标记的对象清除
-
标记-整理算法
- 标记阶段:先通过根节点,标记所有从根节点开始的可达对象,未被标记的为垃圾对象
- 整理阶段:将所有的存活对象压缩到一段连续的内存空间,之后清理边界的所有空间
-
复制算法
- 将所有的内存空间分成两块,每次只使用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,然后清除正在使用的内存块中的所有对象
GC
-
新生代、老年代、持久代
-
GC用的可达性分析算法中,可作为GC Roots对象
- Java虚拟机栈中的对象
- 方法区中的静态成员
- 方法区中的常量引用对象
- 本地方法区中的JNI(Native方法)引用对象
-
新生代转移到老年代的触发条件
- 长期存活的对象
- 大对象直接进入老年代
- Minor GC后,survivor仍然放不下
- 动态年龄判断,大于等于某个年龄的对象超过了survivor空间的一半,大于等于这个年龄的对象直接进入老年代
-
MinGC、FullGC
-
各个垃圾回收器的工作方式
还有两个老年代收集器:Serial old和Parallel old收集器
Java虚拟机内存的划分以及每个区域的功能
-
程序计数器(线程私有)
- 线程创建时创建,执行本地方法时其值为undefined
-
虚拟机栈(线程私有)
- (栈内存)为虚拟机执行Java方法服务,方法被调用时创建栈帧–>局部变量表(基本数据类型,对象引用类型)–>局部变量,对象引用
- 如果线程请求的栈深度超过了虚拟机所允许的深度,就会出现
StackOverfFow
- 虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存时,会出现
OutOfMemeory
-
本地方法栈(线程私有)
- Java虚拟机栈是为Java方法服务,而本地方法栈视为虚拟机使用到的Native方法服务
- Java虚拟机没有对本地方法的使用和数据结构做强制规定。Sun HotSpot把Java虚拟机栈和本地方法栈合二为一
- 同样抛出
StackOverfFlowError
和OutOfMemeoryError
-
Java堆
- 被所有线程所共享,在Java虚拟机启动时创建,几乎所有的对象实例都存放在这里
- GC管理的主要区域
- 物理不连续,逻辑上连续,可以动态扩展,扩展失败抛出
OutOfMemeoryError
-
方法区
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码的数据
- Sun HotSpot虚拟机把方法区称为
永久代
-
运行时常量池
- 受到方法区的限制,可能抛出
OutOfMemeoryError
- 受到方法区的限制,可能抛出
双亲委派机制
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而且把这个请求委派给父类加载器去完成,每一个层次的加载器都如此,因此所有的类加载器都会交给顶层的启动类加载器。只有当父类加载器无法完成该加载请求时(该加载器的搜索范围内没有找到对应的类)时,子加载器才会尝试直接去加载。
Student s = new Student()在内存中做的事情
- 加载Student class文件到内存
- 在栈内存为s开辟空间(对象引用类型)
- 在堆内存为学生对象开辟空间(对象实例)
- 对学生对象的成员变量进行默认初始化
- 对学生对象的成员变量进行显式初始化
- 通过构造方法给学生对象的成员变量进行赋值
- 学生对象初始化完成, 把对象地址赋值给s变量
Java的GC为什么要分代
分代的垃圾回收策略,是基于这样一个事实:**不同的对象的生命周期是不一样的。**因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
如何分代
虚拟机中共分为三代:年轻代、老年代和持久代。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和老年代是对垃圾收集影响比较大的。
-
年轻代
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分为三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到另一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到“老年区”。需要注意的是,Survivor的两个区是对称的,没先后顺序,所以同一个区中可能存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年区的只有从第一个Survivor区过去的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到老年代的可能。 -
老年代
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代。因此,可以认为老年代中存放的都是一些生命周期较长的对象。 -
持久代
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运动过程中新增的类。
Minor GC、Full GC触发条件是什么?
- 从年轻代空间(包括Eden、Survivor区域)回收内存被称为Minor GC
- 对老年代GC称为Major GC
- 而Full GC是对整个堆来说
在最近几个版本的JDK里默认包括了对永生代即方法区的回收(JDK8中无永生代了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。
Major GC的速度一般会比Minor GC慢10倍以上。下边看看有那种情况触发JVM进行Full GC及应对策略。
-
Minor GC触发条件:
当Eden区满时,触发Minor GC。 -
Full GC触发条件:
-
System.gc()方法的调用
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。虽然影响系统建议不能使用这个方法,让虚拟机自己去管理它的内存。
-
老年代空间不足
老年代空间只有在新生代对象转入及创建大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
,为避免以上两种情况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 -
方法区空间不足
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
。为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 -
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC
-
由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
-
G1 GC
G1 GC是目前为止最为复杂、也是最先进的GC,在CMS算法中,GC管理的内存被划分为新生代、老年代和永久代/元空间。这些空间必须是地址连续的。在G1算法中,采用了另外一种完全不同的方式组织堆内存,堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存,Region的大小可以通过-XX:G1HeapRegionSize
参数指定,如果没有配置,默认堆内存按照2048份均分,最后得到一个合理的大小。在G1中,还有一个特殊的区域,叫Humongous 区域。
如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾回收器造成负面影响。为了解决这个问题,G1划分了一个Humongous 区域,它用来专门存放巨型对象,下面的图片展示了G1的内存结构:
G1 GC内存结构
G1 GC的运行可以分为下面几个阶段:
-
**初始标记:**扫描根集合,标记所有从根节点可直接到达的对象并将它们的字段压入扫描栈。在分代式G1模式中,初始标记阶段借用 Young GC 的暂停,因而没有额外的、单独的暂停阶段。
-
**并发标记:**这个阶段可以并发执行,GC线程不断从扫描栈取出引用,进行递归标记,直到扫描栈清空。
-
**最终标记:**重新标记写入屏障标记的对象,这个阶段也进行弱引用处理。
-
**筛选回收:**统计每个Region被标记位活的对象有多少,如果发现完全没有活对象的Region就会将其整体回收到可分配 Region 列表中。
与其他GC相比,G1 GC有如下特点:
-
**并行与并发:**G1 GC能充分利用CPU、多核心等硬件优势,使用多个CPU或者CPU核心来缩短STW的时间,部分其他GC需要停顿java线程执行的GC操作,在G1 GC中仍然可以通过并发的方式让java程序继续执行
-
**分代收集:**和其他GC一样,分代的概念在G1 GC中仍然保留
-
**空间整合:**与CMS的标记-清理算法不同,G1 GC从整体来看是通过”标记-整理“算法实现的GC,从局部(两个Region之间)来看是通过”复制“算法来实现的,无论如何,这两种算法在运行期间都不会产生内存碎片,GC 活动之后可以提供规整的内存空间。
-
**可预测的停顿:**这是G1 GC相对于CMS的另一大优势,降低停顿时间是G1 GC和CMS GC共同关注的,但是G1 GC除了追求低停顿时间外,还建立了可预测的停顿时间模型,能让使用这明确指定在一个长度为M的时间片内,消耗在垃圾收集上的事件不得超过N毫秒。
下面的图片展示了多个GC以及他们工作的分代位置,以及如何组合使用:
JVM GC的触发条件
-
**Young GC:**当Young generation中的Eden区分配满的时候触发。
-
Full GC:
- 当准备要触发一次young GC时,如果发现统计数据Young GC的平均大小比目前Old Gen剩余的空间大,则不会触发Young GC而是转为触发Full GC。
- 如果有Perm Gen的话,要在Perm Gen分配空间但已经没有足够空间时,也要触发一次Full GC
- 调用System.gc()默认也是触发Full GC
JVM性能监控与故障处理工具
-
jps:JVM进程状况工具
选项:
-m 输出JVM进程启动时传递给主类main方法的参数
-l 输出主类的全名,如果进程执行的是jar包,输出jar包的路径
-v 输出进程启动时的JVM参数 -
jstat:JVM统计信息监控工具
该工具具有丰富的JVM统计功能,具体支持的统计可以使用man jstat来输出帮助文档 -
jinfo:java配置信息工具
jinfo用于获取当前JVM的配置信息 -
jmap:java内存映射工具
jmap用于生成堆的转储快照,下面为一个使用示例,用于将当前的JVM的堆的快照输出到文件中去 -
jhat:等jhat执行完毕后,就可以打开浏览器查看堆的情况的
-
jstack:JVM堆栈追踪工具
jstack用于生成当前堆栈的线程快照,这个命令会将所有在堆上的线程都输出,包括线程的运行状态,持有资源的状态等等,对于java应用调优,jstack是非常有用的。