部分内容来自以下博客:
https://blog.csdn.net/baidu_22254181/article/details/82555485
https://blog.csdn.net/know9163/article/details/80574488
1 垃圾回收机制
1.1 是什么
垃圾回收机制是垃圾收集器GC(Garbage Collection)来实现的,GC是后台的守护过程。
GC特别是,这是一个低优先级的过程,但它可以根据内存的使用情况动态调整其优先级。因此,当内存低到一定程度时,它会自动运行,从而实现内存的回收。这就是垃圾回收时间不确定的原因。
1.2 发生位置
JVM内存结构包括程序计数器、虚拟机栈、本地方法栈、堆区、方法区五个区域。
其中,程序计数器、虚拟机栈和本地方法栈三个区域随线程而生,随线程而灭。因此,这些区域的内存分配和回收是确定的,因此没有必要过多考虑回收问题,因为当方法结束或线程结束时,内存自然会回收。
堆积区和方法区不同。这部分内存的分配和回收是动态的,是垃圾收集器需要注意的部分。
1.3 内存泄漏
内存泄漏是指内存中没有空闲空间,垃圾收集器无法提供更多内存。
内存泄漏的原因可能是虚拟机的堆内存设置不够大,代码中创建了大量的大对象,垃圾收集器无法长期收集。
内存泄漏的八种情况:
1)单例模式,单例模式中对象的生命周期与应用程序相同。如果单例程序中持有外部对象的参考,则该外部参考不能回收,可能导致内存泄漏。
2)资源未关闭,需要手动关闭数据库连接、网络连接、输入输出流,否则无法回收。
3)静态集合类,如果这些容器是静态的,它们的生命周期与应用程序一致,容器中的对象在程序结束前不会被释放,导致内存泄漏。
4)内部类持有外部类,如果外部类实例对象的方法返回内部类实例对象,内部类对象长期引用,即使外部类实例对象不再使用,但由于内部类持有外部类实例对象,外部类对象不会被垃圾回收,也会导致内存泄漏。
5)改变哈希值,当对象存储时HashSet中后,这个对象的哈希值不能再修改了,否则就不可能了HashSet检索对象,导致内存泄漏。
6)变量定义在不合理的作用域中的作用范围大于其使用范围,可能导致内存泄漏。此外,如果清空,也可能导致内存泄漏。
7)缓存泄漏,一旦对象放入缓存中,很容易忘记缓存对象,导致内存泄漏。
8)监听器和回调,如果客户端在接口中注册回调,但未显示取消,可能导致内存泄漏。
1.4 STW(Stop The World)
STW,全称是Stop the World,指的是GC在事件发生过程中,应用程序将被暂停。当停顿产生时,整个应用程序线程将被暂停,没有任何响应,有点像卡住。这种停顿被称为STW。
产生STW几种情况:
1)可达性算法中的枚举根节点会导致停顿。
2)调用System.gc()会导致方法Full GC,导致停顿。
3)调用finalize()方法会导致停顿。
1.5 安全点和安全区域
1.5.1 安全点(Safe Point)
从线程的角度来看,安全点可以理解为代码执行过程中的一些特殊位置。当线程执行到这些位置时,虚拟机的当前状态是安全的。
安全点的选择非常重要。太少会导致等待进入安全点的时间过长,过多会导致性能问题。可以使用执行时间较长的程序作为安全点,如方法呼叫、循环跳转、异常跳转等。
对于一些需要暂停的操作,如STW,线程进入安全点有两种方式:
1)预先中断:首先中断所有线程。如果线程不在安全点,恢复线程,让线程跑到安全点。过时了,目前还没有虚拟机。
2)主动中断:设置中断标志。当每个线程运行到安全点时,主动轮询标志。如果中断标志是真实的,中断并悬挂自己。
1.5.2 安全区域(Safe Region)
当需要暂停线程时,如果线程正在执行,可以等待进入安全点,但如果线程处于休眠或堵塞状态,等待时间会变长。
为了解决这个问题,引入了安全区域的概念。
安全区域是指引用关系不会在代码片的任何位置改变GC都是安全的,可以看作是安全点的扩展。
当线程进入安全区域时,标志已进入安全区域GC进入安全区域的线程将被忽略。
离开安全区域时,检查线程是否完成GC,只有完成GC线程可以离开,否则需要等待GC完成后才能离开。
2 对象生存判断
2.1 堆的生存判断
一般有两种方法可以判断对象是否存活:引用计数算法和可达性算法。
2.1.1 引用计数算法(Reference Counting)
每个对象都有一个引用计数属性,新增一个引用计数加1,引用释放计数减1,计数为0时可回收。
这种方法很简单,但不能解决相互引用的问题,如图所示:
public class DemoTest { public static void main(String[] args) { DemoGC demoA = new DemoGC();// step 1 DemoGC demoB = new DemoGC();// step 2 // 相互引用 demoA.instance = demoB;// step 3 demoB.instance = demoA;// step 4 // 释放对象 demoA = null;// step 5 demoB = null;// step 6 // 发生CG System.gc(); } } class DemoGC { public Object instance = null; }
实施第一步和第二步后,在堆中创建了两个例子:
demoA引用实例对象A(引用变为1),demoB引用实例对象B(引用变为1)。
执行第三步和第四步后:
demoB的instance属性引用实例对象A(引用变为2),demoA的instance属性引用实例对象B(引用变为2)。
执行第五步和第六步后:
demoA不再引用实例对象A(引用变为1),demoB不再引用实例对象B(引用变为1)。
如果此时发生GC:
虽然demoA和demoB实例对象不再引用,但其内部instance属性还在引用实例对象,所以此时实例对象的引用不是0,不能被引用GC回收。
2.1.2 可达性算法(Reachability Analysis)
从GC Roots开始向下搜索,搜索路径称为引用链。当对象到达时GC Roots由于虚拟机的二次标记机制,如果没有引用链连接,则证明该对象不可用,可回收,但不一定可回收。
可以作为GC Roots的对象:
1)虚拟机栈栈帧中引用的局部变量表,如各线程中调用的参数和局部变量。
2)本地方法栈JNI(Native方法)引用的对象,如线程start()方法中使用的对象。
3)静态属性引用的对象,比如引用类型的静态变量。
4)方法区常量引用的对象,如方法区常量池中使用字符串的对象。
5)被synchronized持有对象。
6)虚拟机内部引用,如基本类型对应Class对象、常驻异常对象、系统加载器等。
7)本地代码缓存。
8)除了固定的对象外,根据用户选用的垃圾回收器和当前回收的内存区域,还可以有临时对象加入,比如分代收集和局部收集。
回到相互循环引用的问题:
demoA和demoB它是该方法中的局部变量,其存储位置是虚拟机栈栈帧中的局部变量表,可用作GC Roots对象。
instance属性是类中的成员属性,其存储位置为堆,不能用作GC Roots对象。
所以,当demoA和demoB实例对象不再引用后,从GC Roots向下搜索会发现实例对象没有引用链连接,可以被引用GC回收。
2.1.3 二次标记
Object类有一个finalize()该方法将在回收对象之前调用,任何对象都将被调用fianlize()系统只自动调用方法一次。
被标记后,如果重写finalize()方法,并将对象添加到引用链中,此时,虽然已经标记,但不会回收。
原因在于虚拟机的二次标记机制:
1)第一次标记,标记不是引用链的对象,判断是否需要执行finalize()方法。如果已经执行或未重写,则表示不需要执行,否则表示需要执行。
2)需要执行finalize()放置方法的对象F-Queue在队列中,虚拟机自动创建的低优先级Finalizer执行线程。但虚拟机不承诺等待fianlize()方法实施后,即使重写finalize()方法不一定执行。
3)第二次标记,遍历F-Queue队列中的对象,断是否存在引用链。如果存在引用链,表示该对象不需要被回收,否则标记不存在引用链的对象,等待回收。
测试代码:
public class DemoTest {
public static void main(String[] args) throws Exception {
DemoGC.instance = new DemoGC();
DemoGC.demo();
DemoGC.demo();
}
}
class DemoGC {
// 静态变量保持引用
public static DemoGC instance = null;
public static void demo() throws Exception {
// 移除引用
instance = null;
// 通知GC
System.gc();
// 等待GC
Thread.sleep(1000);
// 判断是否可用
if (instance == null) {
System.out.println("被移除了");
} else {
System.out.println("还可以用");
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
instance = this;
System.out.println("被捞回了");
}
}
结果如下:
被捞回了
还可以用
被移除了
2.2 方法区的存活判断
方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面三个条件:
1)该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
2)加载该类的ClassLoader已经被回收。
3)该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3 对象的引用
3.1 说明
在JDK1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可达状态,程序才能使用它。
从JDK1.2版本开始,对象的引用被划分为四种级别,从而使程序能更加灵活地控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
java.lang.ref包中提供了几个类:SoftReference类、WeakReference类和PhantomReference类,它们分别代表软引用、弱引用和虚引用。
无论引用计数算法还是可达性分析算法都是基于强引用而言的。
3.2 强引用(StrongReference)
3.2.1 回收机制
强引用是使用最普遍的引用,如果一个对象具有强引用,那垃圾回收器绝不会回收它。
当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
3.2.2 使用举例
获取强引用:
Object labelReference = new Object();
3.3 软引用(SoftReference)
3.3.1 回收机制
如果一个对象只具有软引用,只有在内存空间不足时才会回收这些对象的内存,如果内存充足垃圾回收器就不会回收它。只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
可以通过软引用的get()方法获取实例对象,也可以调用引用队列的poll()方法来检测是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的那个引用对象(Reference)。
3.3.2 使用举例
获取软引用:
String str = new String("abc");// 强引用
SoftReference<String> softReference = new SoftReference<String>(str);// 软引用
软引用可以和引用队列联合使用:
String str = new String("abc");
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
str = null;
// Notify GC
System.gc();
System.out.println(softReference.get()); // abc
Reference<? extends String> reference = referenceQueue.poll();
System.out.println(reference); // null
软引用可用来实现内存敏感的高速缓存,比如浏览器的后退按钮:
Browser browser = new Browser();
SoftReference<BrowserPage> softReference;
// 首次浏览页面
public BrowserPage loadPage() {
// 从后台程序加载浏览页面
BrowserPage page = browser.getPage();
// 构建软引用
softReference = new SoftReference<BrowserPage>(page);
// 返回浏览页面
return page;
}
// 回退或者再次浏览页面
public BrowserPage backPage() {
BrowserPage page = null;
// 判断软引用的对象是否被回收
if ((page = softReference.get()) == null) {
// 重新从后台程序加载浏览页面
page = browser.getPage();
// 重新构建软引用
softReference = new SoftReference<BrowserPage>(page);
}
// 返回浏览页面
return page;
}
3.4 弱引用(WeakReference)
3.4.1 回收机制
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
同样,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
3.4.2 使用举例
获取弱引用:
String str = new String("abc");// 强引用
WeakReference<String> weakReference = new WeakReference<>(str);// 弱引用
3.5 虚引用(PhantomReference)
3.5.1 回收机制
与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它在任何时候都可能被垃圾回收器回收。
虚引用必须和引用队列(ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
3.5.2 使用举例
获取虚引用:
String str = new String("abc");
ReferenceQueue<String> referenceQueue = new ReferenceQueue<String>();
PhantomReference<String> phantomReference = new PhantomReference<String>(str, referenceQueue);
虚引用主要用来跟踪对象被垃圾回收器回收的活动。
4 垃圾回收算法
4.1 标记-清除(Mark-Sweep)算法
4.1.1 原理
标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
4.1.2 解释
回收前:
回收后:
4.1.3 缺点
一个是效率问题,标记和清除过程的效率都不高。
另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,当程序需要分配较大对象时,无法找到足够的连续内存,不得不提前触发另一次垃圾收集动作。
4.2 复制(Copying)算法
4.2.1 原理
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
4.2.2 解释
回收前:
回收后:
4.2.3 缺点
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
4.3 标记-整理(Mark-Compact)算法
4.3.1 原理
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。
该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
4.3.2 解释
回收前:
回收后:
4.3.3 缺点
效率低,并且在移动过程中,需要全面暂停应用程序,即会触发STW(Stop The World)。
4.4 分代收集(Generational Collection)算法
4.4.1 原理
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。
一般情况下将堆区划分为新生代(Young Generation)和老年代(Tenured Generation),有的虚拟机将方法区看做是永久代(Permanet Generation)。
年老年代的特点是每次垃圾收集时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量的对象需要被回收,永久代的回收主要回收废弃常量和无用的类,那么就可以根据不同代的特点采取最适合的收集算法。
4.4.2 解释
年轻代由Minor GC进行回收,采用复制算法。Major GC主要回收老年代区域,采用标记清除算法。Full GC回收整个堆。
5 分代收集细说
5.1 新生代
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,所以选用复制算法。
5.1.1 内存分配
因为大部分新生成的对象的生命周期都很短,所以将新生代分为一块较大的Eden区和两块较小的Survivor区。一块较大的Eden区用来存放新生成的对象,两块较小的Survivor区用来存放在多次GC存活下来的对象,一块称为S0区,另一块称为S1区。
5.1.2 触发时机
新生代发生的GC也叫做Minor GC,Minor GC发生频率比较高:
当Eden区满了一定会触发GC。
在Major GC的时候会先触发Minor GC。
其他情况。
5.1.3 GC机制
当第一次发生GC时,先将垃圾对象清除,然后将Eden区还存活的对象一次性复制到任意一个Survivor区,最后清空Eden区。为了区分方便,将使用的Survivor区称为From区,将空闲的Survivor区称为To区。
当再次发生GC时,先将垃圾对象清除,然后将Eden区和From区还存活的对象一次性复制到To区,最后清空Eden区和From区。每次GC完成之后,将正在使用的To区称为From区,将空闲的From区称为To区。
对象在放到Survivor区时都会设置一个年龄,并且每经过一次GC后都会将年龄加一,当对象的年龄超过虚拟机设置的阈值之后,会将对象放到老年代。
5.2 老年代
因为老年代中对象存活率高、没有额外空间对它进行分配担保,所以使用标记清除算法或标记整理算法来进行回收。
5.2.1 进入老年代的途径
大对象直接进入老年代。
经过多次Minor GC后仍在Survivor区的对象。
动态年龄判断,计算某个年龄的对象,大于Survivor区的一半,大于或等于这个年龄的对象进入老年代。
空间分配担保,经过Minor GC后Survivor区不足以存放对象,进入老年代。
5.2.2 触发时机
老年代发生的GC也叫做Major GC,Major GC发生频率比较低,当老年代内存满时触发。
6 垃圾收集器
6.1 Serial
新生代垃圾收集器,串行运行,采用复制算法,响应速度优先。
对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。但需要STW(Stop The World),停顿时间长。
Serial收集器是Client模式下默认的垃圾收集器,可以通过-XX:+UseSerialGC来强制指定。
6.2 SerialOld
老年代垃圾收集器,串行运行,采用标记-整理算法,响应速度优先,是Serial收集器的老年代版本。
6.3 ParNew
新生代垃圾收集器,并行运行,采用复制算法,响应速度优先,是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
除了Serial收集器外,目前只有它能与CMS收集器配合工作。
ParNew收集器是Server模式下首选的垃圾收集器,可以使用-XX:ParallelGCThreads来限制垃圾收集的线程数。
6.4 Parallel
新生代垃圾收集器,并行运行,采用复制算法,吞吐量优先。
追求高吞吐量,高效利用CPU,主要是为了达到一个可控的吞吐量。
Parallel收集器是Server模式下默认的垃圾收集器,可以通过-XX:+UseParallelGC来强制指定,可以使用-XX:ParallelGCThreads来限制垃圾收集的线程数。
6.5 ParallelOld
老年代垃圾收集器,并行运行,采用标记-整理算法,吞吐量优先。
6.6 CMS(Current Mark Sweep)
JDK1.5推出,JDK1.9废弃,JDK1.14移除。
老年代垃圾收集器,并发运行,采用标记-清除算法,响应速度优先。
以获取最短回收停顿时间为目标,高并发、低停顿,CPU占用比较高,响应时间快,停顿时间短。
收集过程分为如下四步:
1)初始标记,标记GCRoots能直接关联到的对象,有STW现象,暂停时间非常短。
2)并发标记,进行可达性分析过程,时间很长,不需要暂停用户线程,可与其他垃圾收集线程并发运行。在这个阶段使用了三色标记。
3)重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长,不需要暂停用户线程。
4)并发清除,回收内存空间,时间很长,不需要暂停用户线程。
其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。
CMS优点:
并发收集,低延迟。
CMS缺点:
产生内存碎片,对CPU资源非常敏感,无法处理浮动垃圾。
6.7 G1(Garbage First)
JDK1.7推出,JDK1.9默认。
新生代和老年代垃圾收集器,并发、并行运行,采用复制算法和标记-整理算法,响应速度优先,同时注重吞吐量。
G1的目标是在延迟可控的情况下获得尽可能高的吞吐量。
使用G1收集器时,将整个堆划分为多个大小相等的独立区域(Region),每个Region都按照分代收集算法代表一种分区,分区有伊甸园区,幸存者0区,幸存者1区,老年代等分类。
G1收集器有以下特点:
1)并行和并发。使用多个CPU来缩短STW停顿时间,与用户线程并发执行。
2)分代收集。独立管理整个堆,但是能够采用不同的方式处理新对象和旧对象。
3)空间整合。Region之间是复制算法,整体上可以看作是标记-整理算法,这两种算法都能避免产生内存碎片。
4)可预测的停顿。能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
7 GC调优
7.1 查看日志
输出GC日志:
-XX:+PrintGC
输出GC的详细日志:
-XX:+PrintGCDetails
输出GC的时间戳(以基准时间的形式):
-XX:+PrintGCTimeStamps
输出GC的时间戳(以日期的形式):
-XX:+PrintGCDateStamps
在进行GC的前后打印出堆的信息:
-XX:+PrintHeapAtGC
设置日志文件的输出路径:
-Xloggc:../logs/gc.log
7.2 代大小优化
7.2.1 -Xms、-Xmx
-Xms设置堆内存初始内存大小,默认为物理内存的1/64。
-Xmx设置堆内存的最大内存,默认为物理内存的1/4。
这两个参数通常设置为相同的值,在清理完堆后就不需要重新分隔计算堆的大小,从而提升了性能。
7.2.2 -Xmn、-XX:SurvivorRatio、-XX:NewRatio
-Xmn决定了新生代空间的大小。
-XX:SurvivorRatio用来控制新生代Eden、S0、S1三个区域的比率,假如值为4则表示:Eden:S0:S1=4:3:3。
-XX:NewRatio用来控制新生代和老年代的比率,假如为2则表示:老年代:新生代=2:1。
7.2.3 -XX:MaxTenuringThreshold
-XX:MaxTenuringThreshold控制对象在经过多少次Minor GC之后进入老年代,此参数只有在Serial串行GC时有效。
7.2.4 -XX:PermSize、-XX:MaxPermSize
-XX:PermSize、-XX:MaxPermSize用来控制方法区的大小,通常设置为相同的值。
在JDK1.8之后,废弃了永久代(也就是方法区),如果同时使用这两个设置,会产生警告。
8 通过程序理解JVM
8.1 查看堆内存分配
-Xms设置堆内存初始内存大小,默认为物理内存的1/64。-Xmx设置堆内存的最大内存,默认为物理内存的1/4。
代码如下:
public static void main(String[] args) {
// 机器内存大小的4分之1
System.out.println("max memory: " + (Runtime.getRuntime().maxMemory() / 1024 / 1024) + "MB");
// 机器内存大小的64分之1
System.out.println("total memory: " + (Runtime.getRuntime().totalMemory() / 1024 / 1024) + "MB");
}
结果如下:
max memory: 3620MB
total memory: 245MB
结果说明:
本机电脑是16G,去掉一些自身的占用后,堆内存的最大值约为机器内存的1/4,堆内存的初始值为机器内存的1/64。
8.2 修改堆内存分配
在Eclipse中修改堆内存分配:
然后点击“Apply”,运行程序:
public static void main(String[] args) {
// 机器内存大小的4分之1
System.out.println("max memory: " + (Runtime.getRuntime().maxMemory() / 1024 / 1024) + "MB");
// 机器内存大小的64分之1
System.out.println("total memory: " + (Runtime.getRuntime().totalMemory() / 1024 / 1024) + "MB");
}
发现结果变为:
max memory: 18MB
total memory: 5MB
Heap
PSYoungGen total 1536K, used 838K [0x00000000ff980000, 0x00000000ffb80000, 0x0000000100000000)
eden space 1024K, 81% used [0x00000000ff980000,0x00000000ffa51ae0,0x00000000ffa80000)
from space 512K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffb80000)
to space 512K, 0% used [0x00000000ffa80000,0x00000000ffa80000,0x00000000ffb00000)
ParOldGen total 4096K, used 0K [0x00000000fec00000, 0x00000000ff000000, 0x00000000ff980000)
object space 4096K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff000000)
Metaspace used 2699K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 289K, capacity 386K, committed 512K, reserved 1048576K
说明堆内存已经被修改了。
8.3 占用内存后再次查看内存分配
修改代码如下:
public static void main(String[] args) {
// 查看GC信息
System.out.println("初始内存");
System.out.println("max memory: " + Runtime.getRuntime().maxMemory());
System.out.println("free memory: " + Runtime.getRuntime().freeMemory());
System.out.println("total memory: " + Runtime.getRuntime().totalMemory());
byte[] b1 = new byte[1 * 1024 * 1024];
System.out.println("分配了1M");
System.out.println("max memory: " + Runtime.getRuntime().maxMemory());
System.out.println("free memory: " + Runtime.getRuntime().freeMemory());
System.out.println("total memory: " + Runtime.getRuntime().totalMemory());
}
结果如下:
初始内存
max memory: 18874368
free memory: 4929688
total memory: 5767168
分配了1M
max memory: 18874368
free memory: 3881096
total memory: 5767168
Heap
PSYoungGen total 1536K, used 838K [0x00000000ff980000, 0x00000000ffb80000, 0x0000000100000000)
eden space 1024K, 81% used [0x00000000ff980000,0x00000000ffa51ac8,0x00000000ffa80000)
from space 512K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffb80000)
to space 512K, 0% used [0x00000000ffa80000,0x00000000ffa80000,0x00000000ffb00000)
ParOldGen total 4096K, used 1024K [0x00000000fec00000, 0x00000000ff000000, 0x00000000ff980000)
object space 4096K, 25% used [0x00000000fec00000,0x00000000fed00010,0x00000000ff000000)
Metaspace used 2699K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 289K, capacity 386K, committed 512K, reserved 1048576K
结果说明:
初始时,最大内存约为20M,空闲内存约为5M,初始内存约为6M。
在分配了1M的内存后,最大内存约为20M,空闲内存约为4M,初始内存约为6M。
再次修改代码如下:
public static void main(String[] args) {
// 查看GC信息
System.out.println("初始内存");
System.out.println("max memory: " + Runtime.getRuntime().maxMemory());
System.out.println("free memory: " + Runtime.getRuntime().freeMemory());
System.out.println("total memory: " + Runtime.getRuntime().totalMemory());
byte[] b1 = new byte[1 * 1024 * 1024];
System.out.println("分配了1M");
System.out.println("max memory: " + Runtime.getRuntime().maxMemory());
System.out.println("free memory: " + Runtime.getRuntime().freeMemory());
System.out.println("total memory: " + Runtime.getRuntime().totalMemory());
byte[] b2 = new byte[6 * 1024 * 1024];
System.out.println("分配了6M");
System.out.println("max memory: " + Runtime.getRuntime().maxMemory());
System.out.println("free memory: " + Runtime.getRuntime().freeMemory());
System.out.println("total memory: " + Runtime.getRuntime().totalMemory());
}
结果如下:
初始内存
max memory: 18874368
free memory: 4929672
total memory: 5767168
分配了1M
max memory: 18874368
free memory: 3881080
total memory: 5767168
分配了6M
max memory: 18874368
free memory: 4405352
total memory: 12582912
Heap
PSYoungGen total 1536K, used 838K [0x00000000ff980000, 0x00000000ffb80000, 0x0000000100000000)
eden space 1024K, 81% used [0x00000000ff980000,0x00000000ffa51ad8,0x00000000ffa80000)
from space 512K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffb80000)
to space 512K, 0% used [0x00000000ffa80000,0x00000000ffa80000,0x00000000ffb00000)
ParOldGen total 10752K, used 7168K [0x00000000fec00000, 0x00000000ff680000, 0x00000000ff980000)
object space 10752K, 66% used [0x00000000fec00000,0x00000000ff300020,0x00000000ff680000)
Metaspace used 2699K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 289K, capacity 386K, committed 512K, reserved 1048576K
结果说明:
再次分配6M之后,原空闲内存大小不能满足,所以重新分配了空间。
8.4 OOM异常
修改JVM参数:
-Xms8m -Xmx8m -XX:+PrintGCDetails
运行代码:
public static void main(String[] args) {
// 查看GC信息
System.out.println("初始内存");
System.out.println("max memory: " + Runtime.getRuntime().maxMemory());
System.out.println("free memory: " + Runtime.getRuntime().freeMemory());
System.out.println("total memory: " + Runtime.getRuntime().totalMemory());
byte[] b1 = new byte[3 * 1024 * 1024];
System.out.println("分配了1M");
System.out.println("max memory: " + Runtime.getRuntime().maxMemory());
System.out.println("free memory: " + Runtime.getRuntime().freeMemory());
System.out.println("total memory: " + Runtime.getRuntime().totalMemory());
byte[] b2 = new byte[6 * 1024 * 1024];
System.out.println("分配了6M");
System.out.println("max memory: " + Runtime.getRuntime().maxMemory());
System.out.println("free memory: " + Runtime.getRuntime().freeMemory());
System.out.println("total memory: " + Runtime.getRuntime().totalMemory());
}
结果如下:
初始内存
max memory: 7864320
free memory: 6998920
total memory: 7864320
分配了1M
max memory: 7864320
free memory: 3853176
total memory: 7864320
[GC (Allocation Failure) [PSYoungGen: ...] 3917K->3704K(7680K), 0.0004714 secs] ...
[GC (Allocation Failure) [PSYoungGen: ...] 3704K->3712K(7680K), 0.0003756 secs] ...
[Full GC (Allocation Failure) [PSYoungGen: ...] [ParOldGen: ...] 3712K->3614K(7680K), [Metaspace: ...], 0.0042747 secs] ...
[GC (Allocation Failure) [PSYoungGen: ...] 3614K->3614K(7680K), 0.0001818 secs] ...
[Full GC (Allocation Failure) [PSYoungGen: ...] [ParOldGen: ...] 3614K->3602K(7680K), [Metaspace: ...], 0.0033216 secs] ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.demo.test.DemoTest.main(DemoTest.java:18)
Heap
PSYoungGen total 2048K, used 46K [0x00000000ffd80000, 0x0000000100000000, 0x0000000100000000)
eden space 1536K, 3% used [0x00000000ffd80000,0x00000000ffd8b9e0,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 5632K, used 3602K [0x00000000ff800000, 0x00000000ffd80000, 0x00000000ffd80000)
object space 5632K, 63% used [0x00000000ff800000,0x00000000ffb84878,0x00000000ffd80000)
Metaspace used 2724K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 292K, capacity 386K, committed 512K, reserved 1048576K
结果说明:
当新生代Eden区内存不足时,触发Minor GC回收新生代,当老年代内存不足时,触发Major GC,当GC后内存仍不足时,触发OOM异常。
8.5 查看内存分配占比
修改JVM参数:
-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC
代码如下:
public static void main(String[] args) {
byte[] b = null;
// 连续向系统申请10MB空间
for (int i = 0; i < 10; i++) {
b = new byte[1 * 1024 * 1024];
}
}
结果如下:
Heap
def new generation total 960K, used 805K [0x00000000fec00000, 0x00000000fed00000, 0x00000000fed00000)
eden space 896K, 89% used [0x00000000fec00000, 0x00000000fecc96e0, 0x00000000fece0000)
from space 64K, 0% used [0x00000000fece0000, 0x00000000fece0000, 0x00000000fecf0000)
to space 64K, 0% used [0x00000000fecf0000, 0x00000000fecf0000, 0x00000000fed00000)
tenured generation total 19456K, used 10240K [0x00000000fed00000, 0x0000000100000000, 0x0000000100000000)
the space 19456K, 52% used [0x00000000fed00000, 0x00000000ff7000a0, 0x00000000ff700200, 0x0000000100000000)
Metaspace used 2661K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
结果说明:
可以看到,在新生代中的total大小约为1M,并且eden:from:to约为8:1:1。
再次修改配置:
-Xms21m -Xmx21m -XX:NewRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
代码如下:
public static void main(String[] args) {
byte[] b = null;
// 连续向系统申请10MB空间
for (int i = 0; i < 10; i++) {
b = new byte[1 * 1024 * 1024];
}
}
结果如下:
[GC (Allocation Failure) [DefNew: ...] 6048K->1562K(21824K), 0.0029985 secs] ...
Heap
def new generation total 6784K, used 5839K [0x00000000fea00000, 0x00000000ff150000, 0x00000000ff150000)
eden space 6080K, 87% used [0x00000000fea00000, 0x00000000fef2d188, 0x00000000feff0000)
from space 704K, 76% used [0x00000000ff0a0000, 0x00000000ff126b40, 0x00000000ff150000)
to space 704K, 0% used [0x00000000feff0000, 0x00000000feff0000, 0x00000000ff0a0000)
tenured generation total 15040K, used 1024K [0x00000000ff150000, 0x0000000100000000, 0x0000000100000000)
the space 15040K, 6% used [0x00000000ff150000, 0x00000000ff250010, 0x00000000ff250200, 0x0000000100000000)
Metaspace used 2661K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
结果说明:
可以看到,新生代约为7M,老年代约为14M,说明占比为1:2。