内存(Memory)作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行。
内存管理是指软件运行时对计算机内存资源的分配和使用。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。
GC (Gabage Collection) 垃圾回收,主要作用是释放和回收内存资源。
Java自动管理内存,不用像 C++ 需要手动管理内存, Java 程序员只管 New New New 即可,Java 会自动回收过期的对象。
程序计数器、Java栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的! 我们把Java对象的回收释放交给GC就万事大吉了吗!不,不良的开发习惯,还是会导致出现内存泄漏的问题。进一步了解Java GC的原理,知道其执行的过程,理解背后的思想。不光能够提高代码开发的质量,还有从中学习到宝贵的开发经验。
认识GC之前我们先看看一个案例,判断该代码算法是否会造成内存泄漏
public class Test { public Object obj; public void test(){ obj = new Object(); //...其他代码 } }如果公司交给你一个需求,来实现自动垃圾回收,你会怎么开发。 首先你会想,哪些是垃圾对象,这些垃圾对象可以分类吗,这些垃圾对象分别在哪里,应该怎么回收这些垃圾对象。
哪些是可以被GC回收的对象呢?
引用计数判断法(reference-counting):每个对象都有个引用计数器,被引用一次时+1,引用失效时-1,当对象的引用计数器为0时,则是可以被GC回收的对象。可达性分析法(Readchablility Analysis):以一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链(不可达)时,则是可以被GC回收的对象(GC Roots必然活的对象,不会被回收的对象)。根据不同的垃圾对象判断条件来分类,可分为引用计数判断法-回收算法和可达性分析法-回收算法。引用计数回收算法就是根据引用计数判断法来回收垃圾对象。我们重点讲可达性分析法回收算法。Java目前已不推荐使用引用计数回收算法。
可达性分析法回收算法实现可分为两个阶段标记和回收。标记都是以可达性作为标记,回收可分为直接回收、复制回收、移动(也叫整理)回收。
标记算法:直接回收标记为不可达的对象。将红色框框直接回收,我们发现回收之后会很多内存碎片。 复制算法:该算法很好的解决了内存空间碎片的问题,它将内存分为大小相同的两个区域,运行区域,预留区域,所有创建的新对象都分配到运行区域,当运行区域内存不够时,将运作区域中存活对象全部复制到预留区域,然后再清空整个运行区域内存,这时两块区域的角色也发生了变化,每次存活的对象就像皮球一下在运行区域与预留区域踢来踢出,而垃圾对象会随着整个区域内存的清空而释放掉。虽然解决了内存碎片的问题,但是预留一半的内存区域未免有些浪费,并且如果内存中大量的是存活状态,只有少量的垃圾对象,收集器要执行更多次的复制操作才能释放少量的内存空间,得不偿失。 标记整理算法:回收之前,算法将存活的对象向内存空间的一端移动,然后将存活对象边界以外的空间全部清空。这样解决了内存碎片问题,也不存在空间的浪费问题。但是也存在移动的动作,当需要整理大量且都是存活的对象时,则需要移动大量的存活对象,且释放的内存也只有一丁点。根据GC的运行方式和GC线程与用户线程的关系划分:
串行:单线程运行,用户线程暂停,Stop The World。不适合服务器环境。并行:多线程运行,用户线程暂停,回收效率高于串行。适用于科学计算或者大数据分析等弱交互的场景。并发:多线程运行,用户线程几乎不用暂停,可以与用户线程同时执行。适用于对响应时间有要求的场景。我们知道了什么是垃圾对象,又有哪些算法可以回收这些垃圾对象。接下来就是开发实现垃圾回收功能。
在选择具体的回收算法时,可以不用拘于某个算法。对不同生命周期的对象的回收动作进行分类,分别采用不同的垃圾回收算法,以提高回收的效率。分析JVM中的对象生命周期,我们可以得出下面的结论:
绝大多数的对象都是“朝生夕灭”的,既创建不久即可消亡;熬过越多此垃圾回收过程的对象就越难以消亡;我们可以将“朝生夕灭”的分为新生代,“难以消亡”分为老年代。新生代(PSYoungGen)特点垃圾对象多,回收动作频繁,用标记复制算法回收效率高。老年代(ParOldGen)特点垃圾对象少,生存时间长,回收作动少,用标记清除、标记整理算法。上述回收方式可以称为分代收集算法。 上面都是GC的设计思路和回收模型,JDK已有了对应的落地实现,我们要通过这些设计思想来理解和应用这些落地实现。
G1是JDK1.8之后的,这个本篇不作拓展。新生代(Young Gen)都是采用复制算法,老年代(Old Gen)都是采用标记清除、标记整理算法。这些垃圾收集器主要区别是回收方式的不同。
串行执行是最古老、最简单、效率高的一种回收方式。执行时,用户线程暂停,适用于单核服务器。
并行执行是串行的升级版,多线程执行垃圾回收,效率大大提高。执行时,用户线程暂停,适用于多核服务器。
并发执行是多线程与用户线程并发执行,目的是尽量减少对用户线程的暂停时间,实现最短的回收停顿时间。
Serial(新生代):串行执行ParNew(新生代):并行执行Parallel(新生代):并行执行,是ParNew的升级版Serial Old(老生代):串行执行Parallel Old(老生代):并行执行CMS(老生代):并发执行(Concurrent Mark-Sweep),使用标记清除算法JVM设置垃圾回收器的配置参数,就是在上述的垃圾回收器之间选择,针对性配置垃圾回收器。
//资源类测试 public static void main(String[] args) throws Exception { System.out.println("Start..."); for (int i = 1; i <= 3; i++) { System.out.println("i = " + i); //堆中创建10M的对象 byte[] bytes = new byte[1024 * 1024 * 10]; Thread.sleep(1000); } }GC信息名称解释:
新生代和老年代都使用单线程串行执行GC。
JVM参数配置:-Xms20m -Xmx20m -XX:+UseSerialGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails
堆分为:def new generation(新生代)、tenured generation(老年代)、Metaspace(元空间)
Heap def new generation total 6144K, used 165K [0x00000000fec00000, 0x00000000ff2a0000, 0x00000000ff2a0000) eden space 5504K, 3% used [0x00000000fec00000, 0x00000000fec29608, 0x00000000ff160000) from space 640K, 0% used [0x00000000ff160000, 0x00000000ff160000, 0x00000000ff200000) to space 640K, 0% used [0x00000000ff200000, 0x00000000ff200000, 0x00000000ff2a0000) tenured generation total 13696K, used 10811K [0x00000000ff2a0000, 0x0000000100000000, 0x0000000100000000) the space 13696K, 78% used [0x00000000ff2a0000, 0x00000000ffd2ee78, 0x00000000ffd2f000, 0x0000000100000000) Metaspace used 2843K, capacity 4486K, committed 4864K, reserved 1056768K class space used 302K, capacity 386K, committed 512K, reserved 1048576K新生代适用多线程并行执行GC,老年代使用单线程串行执行GC。
JVM参数配置:-Xms20m -Xmx20m -XX:+UseParNewGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails
堆分为:par new generation(新生代)、tenured generation(老年代)、Metaspace(元空间)
Heap par new generation total 6144K, used 165K [0x00000000fec00000, 0x00000000ff2a0000, 0x00000000ff2a0000) eden space 5504K, 3% used [0x00000000fec00000, 0x00000000fec29608, 0x00000000ff160000) from space 640K, 0% used [0x00000000ff160000, 0x00000000ff160000, 0x00000000ff200000) to space 640K, 0% used [0x00000000ff200000, 0x00000000ff200000, 0x00000000ff2a0000) tenured generation total 13696K, used 10811K [0x00000000ff2a0000, 0x0000000100000000, 0x0000000100000000) the space 13696K, 78% used [0x00000000ff2a0000, 0x00000000ffd2ee78, 0x00000000ffd2f000, 0x0000000100000000) Metaspace used 2843K, capacity 4486K, committed 4864K, reserved 1056768K class space used 302K, capacity 386K, committed 512K, reserved 1048576K堆警告信息:Java HotSpot™ 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release(Java HotSpot(TM)64位服务器VM警告:不推荐将ParNew年轻收集器与Serial旧收集器一起使用,并且在将来的发行版中可能会删除它)
拓展:-XX:ParallelGCThreads=并行线程数(默认CPU数),可以自定义设置并行的线程数。
新生代和老年代都使用多线程并行GC。
又称吞吐量收集器(吞吐量=用户线程执行时间/用户线程执行时间+GC线程运行时间),吞吐量越高,说明CPU执行效率高,最高100%执行用户线程,不执行GC。
JVM参数配置:-Xms20m -Xmx20m -XX:+UseParallelGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails
堆分为:PSYoungGen(新生代)、ParOldGen(老年代)、Metaspace(元空间)
Heap PSYoungGen total 6144K, used 169K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000) eden space 5632K, 3% used [0x00000000ff980000,0x00000000ff9aa558,0x00000000fff00000) from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) ParOldGen total 13824K, used 10814K [0x00000000fec00000, 0x00000000ff980000, 0x00000000ff980000) object space 13824K, 78% used [0x00000000fec00000,0x00000000ff68f970,0x00000000ff980000) Metaspace used 2843K, capacity 4486K, committed 4864K, reserved 1056768K class space used 302K, capacity 386K, committed 512K, reserved 1048576K拓展:-XX:MaxGCPauseMillis=毫秒数,可以自定义配置GC的暂停时间,采用自适应调节策略,是对ParNew的重大提升。JDK1.8默认配置,查看JVM默认配置
java -XX:+PrintCommandLineFlags -version新生代和老年代都使用多线程并行GC。与-XX:+UseParallelGC一样,可相互激活。JDK1.6之后有效,1.6之前还是Parallel(新生代)+Serial Old(老生代)。
新生代使用多线程并发GC,老年代使用多线程并发GC,并以Serial Old(老生代)作为异常时备用。
并发标记执行流程:
初始标记(STW initial mark) ***暂停应用并发标记(Concurrent marking)并发预清理(Concurrent precleaning)重新标记(STW remark) *** 暂停 应用并发清理(Concurrent sweeping)并发重置(Concurrent reset)JVM参数配置:-Xms20m -Xmx20m -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails
堆分为:par new generation(新生代)、concurrent mark-sweep generation(老年代)、Metaspace(元空间)
Heap par new generation total 6144K, used 55K [0x00000000fec00000, 0x00000000ff2a0000, 0x00000000ff2a0000) eden space 5504K, 1% used [0x00000000fec00000, 0x00000000fec0dda0, 0x00000000ff160000) from space 640K, 0% used [0x00000000ff160000, 0x00000000ff160000, 0x00000000ff200000) to space 640K, 0% used [0x00000000ff200000, 0x00000000ff200000, 0x00000000ff2a0000) concurrent mark-sweep generation total 13696K, used 10815K [0x00000000ff2a0000, 0x0000000100000000, 0x0000000100000000) Metaspace used 2843K, capacity 4486K, committed 4864K, reserved 1056768K class space used 302K, capacity 386K, committed 512K, reserved 1048576K备用担保机制:由于是并发执行,GC线程和用户线程会同时增加对堆内存的占用,所以CMS必须在老年代堆内存用尽之前完成垃圾回收,否抓CMS回收失败,会触发担保机制,Serial Old(老生代)垃圾收集器会暂停所有用户线程,进行GC,会造成用户线程较长的停顿时间。
拓展:-XX:CMSFullGCsBeforeCompaction=次数(默认为0),多少次CMS之后,进行一次压缩的Full GC(由于使用的是标记清除,会又内存碎片产生)
新生代和老年代都使用单线程串行执行GC。
JVM参数配置:-Xms20m -Xmx20m -XX:+UseSerialOldGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails
已被移除,可以通过-XX:+UseSerialGC实现
总结:
JVM参数配置:-Xms20m -Xmx20m -XX:+UseG1GC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails
堆分为:garbage-first heap、Metaspace(元空间)
Heap garbage-first heap total 20480K, used 11020K [0x00000000fec00000, 0x00000000fed000a0, 0x0000000100000000) region size 1024K, 2 young (2048K), 1 survivors (1024K) Metaspace used 2843K, capacity 4486K, committed 4864K, reserved 1056768K class space used 302K, capacity 386K, committed 512K, reserved 1048576K总结:在第一段代码中,是否会内存泄漏,答案是会,当test()方法执行完成后,obj对象所分配的内存不会马上被认为是可以被释放的对象,只有在Test类创建的对象被释放后才会被释放,但是对于我们业务代码逻辑来说,这个obj对象在test()方法执行完成后,就已经不再使用了,可以被作为垃圾对象回收。