什么是垃圾:内存中已经不在被使用到的空间就是垃圾,要进行垃圾回收,首先需要判断一个对象是否可以被回收。
如何判断一个对象是否可以被回收:
引用计数; Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说:给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1。每当有一个引用失效时,计数器值减1。任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。由于它难解决对象之间相互循环引用的问题,所以主流的Java虚拟机里面都没有选用这种算法
枚举根节点做可达性分析。 通过一系列 “GC Roots” 的对象作为起始点,从这个被称为 “GC Roots” 的对象开始向下搜索,遍历到的对象(可达对象)被判断为存活。没有被遍历到的对象判断为死亡。
可以做GCRoots的对象 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)方法区中的类静态属性引用的对象。方法区中常量引用的对象本地方法栈中( Native方法)引用的对象标配参数:-version、-help、java -showversion,标配参数从Java1.0开始就有
X参数:java -Xint|-Xcomp|-Xmixed -version
-Xint:解释执行,Java HotSpot™ 64-Bit Server VM (build 25.211-b12, interpreted mode)-Xcomp:第一次使用就编译成本地代码,Java HotSpot™ 64-Bit Server VM (build 25.211-b12, compiled mode)-Xmixed:混合模式,Java HotSpot™ 64-Bit Server VM (build 25.211-b12, mixed mode)XX参数:
Boolean类型:-XX:+|-属性值 (开启/关闭属性) # 开启/关闭打印GC详细信息属性 -XX:+PrintGCDetails -XX:-PrintGCDetails # 开启/关闭串行垃圾回收器 -XX:+UseSerialGC -XX:-UseSerialGC kv 类型 -XX:属性key=属性值value -XX:MetaspaceSize=128m -XX:MaxTenuringThreshold=15注意:-Xms和-Xmx属于KV键值对类型的缩写形式:它们分别等价于-XX:InitialHeapSize和-XX:Max HeapSize
-Xms:初始大小内存,默认为物理内存1/64,等价于:-XX:InitialHeapSize -Xmx:最大分配内存,默认为物理内存1/4,等价于:-XX:MaxHeapSize -Xss:设置单个线程栈的大小,一般默认为512k-1024k(依赖平台),但是JVM该参数默认为0,代表的是使用默认值, 而不是说单个线程的大小为0,等价于:-XX:ThreadStackSize -Xmn:设置年轻代大小,一般不调节该参数 -XX:MetaspaceSize:设置元空间大小,元空间并不在虚拟机中,而是使用本地内存,元空间的大小仅受本地内存限制 -XX:SurvivorRatio: 设置新生代中Eden和s0/s1空间的比例,默认是-XX:SurivivorRatio=8,即Eden:s0:s1=8:1:1, -XX:SurvivorRatio=4 -> Eden:s0:s1=4:1:1 -XX:NewRatio:配置年轻代和老年代在堆结构中的占比,默认是-XX:NewRatio=2,即新生代占1,老年代占2, -XX:NewRatio=4 -> 新生代占1,老年代占4,NewRatio所设置的数值为老年代占的比例,新生代始终为1 -XX:MaxTenuringThreshold:设置垃圾的最大年龄,默认值为15
-Xms:初始大小内存,默认为物理内存1/64,等价于:-XX:InitialHeapSize D:\projects\java-simple\src\main\java\com\atLearn>jinfo -flag InitialHeapSize 37840 -XX:InitialHeapSize=209715200 -Xmx:最大分配内存,默认为物理内存1/4,等价于:-XX:MaxHeapSize D:\projects\java-simple\src\main\java\com\atLearn>jinfo -flag MaxHeapSize 11472 -XX:MaxHeapSize=2147483648 -Xss:设置单个线程栈的大小,一般默认为512k-1024k(依赖平台),但是JVM该参数默认为0,代表的是使用默认值,而不是说单个线程的大小为0,等价于:-XX:ThreadStackSize D:\projects\java-simple\src\main\java\com\atLearn>jinfo -flag ThreadStackSize 42076 -XX:ThreadStackSize=128 -Xmn:设置年轻代大小,一般不调节该参数 -XX:MetaspaceSize:设置元空间大小,元空间并不在虚拟机中,而是使用本地内存,元空间的大小仅受本地内存限制 D:\projects\java-simple\src\main\java\com\atLearn>jinfo -flag MetaspaceSize 43760 -XX:MetaspaceSize=2147483648 -XX:SurvivorRatio: 设置新生代中Eden和s0/s1空间的比例,默认是-XX:SurivivorRatio=8,即Eden:s0:s1=8:1:1。-XX:SurvivorRatio=4 -> Eden:s0:s1=4:1:1 D:\projects\java-simple\src\main\java\com\atLearn>jinfo -flag SurvivorRatio 37572 -XX:SurvivorRatio=4 -XX:NewRatio:配置年轻代和老年代在堆结构中的占比,默认是-XX:NewRatio=2,即新生代占1,老年代占2。-XX:NewRatio=4 -> 新生代占1,老年代占4,NewRatio所设置的数值为老年代占的比例,新生代始终为1。 D:\projects\java-simple\src\main\java\com\atLearn>jinfo -flag NewRatio 37728 -XX:NewRatio=6 -XX:MaxTenuringThreshold:设置垃圾的最大年龄,范围在 [0,15]。默认值为15 D:\projects\java-simple\src\main\java\com\atLearn>jinfo -flag MaxTenuringThreshold 34572 -XX:MaxTenuringThreshold=10 # 常见参数设置 -Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:PrintGCDetails -XX:+UseSerialGC
Java提供了四种引用,分别是:强引用、软引用、弱引用、虚引用,它们的关系图为:
强引用是默认支持的。当内存不足时,JVM 开始垃圾回收。对于强引用对象,就算出现 OOM,也不会对该对象进行回收。
强引用是常见的普通引用,只要还有强引用指向一个对象,就说明这个对象还活着。垃圾回收器不会对这种对象进行回收。把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,是不可被垃圾回收的。强引用是造成 Java内存泄漏的主要原因之一。
public class StrongReferenceDemo { public static void main(String[] args) { // 强引用对象 Object obj1 = new Object(); // 引用赋值 Object obj2 = obj1; obj1 = null; System.gc(); System.out.println(obj1 + "\t" + obj2); } }软引用是一种相对强引用弱化了的引用,需要使用 java.lang.ref.SoftReference 类实现,可以让对象和面一些垃圾回收。
当系统内存充足,软引用不会被回收;当系统内存不足,软引用会被回收。软引用通常用在对内存敏感的程序中,比如高速缓存就用到了软引用。
/** * VM Options: -Xms5m -Xmx5m -XX:+PrintGCDetails */ public class SoftReferenceDemo { public static void main(String[] args) { System.out.println("===========内存足够==========="); softRef_Memory_Enough(); System.out.println("============内存不足=============="); softRef_Memory_NotEnough(); } private static void softRef_Memory_NotEnough(){ Object obj1 = new Object(); SoftReference<Object> obj2 = new SoftReference<>(obj1); System.out.println(obj1 + "\t"+ obj2.get()); obj1 = null; try { byte[] bytes = new byte[30 * 1024 * 1024]; }catch (Exception e){ e.printStackTrace(); }finally { System.out.println(obj1 + "\t"+ obj2.get()); } } private static void softRef_Memory_Enough() { Object obj1 = new Object(); SoftReference<Object> obj2 = new SoftReference<>(obj1); System.out.println(obj1 + "\t"+ obj2.get()); obj1 = null; System.gc(); System.out.println(obj1 + "\t"+ obj2.get()); } }弱引用需要用 java.lang.ref.WeakReference 来实现,其生存期比软引用的时间更短。
对于弱引用对象而言,只要回收器被开启,不管 JVM 内存空间是否充足,都会回收该对象。
/** * VM Options: -XX:+PrintGCDetails */ public class WeakReferenceDemo { public static void main(String[] args) { Object obj1 = new Object(); WeakReference<Object> obj2 = new WeakReference<>(obj1); System.out.println(obj1 + "\t"+ obj2.get()); System.out.println("======================启动垃圾回收====================="); obj1 = null; System.gc(); System.out.println(obj1 + "\t"+ obj2.get()); } }假如有一个应用需要读取大量的本地图片:
如果每次读取图片都从硬盘读取则会严重影响性能;如果一次性全部加载到内存中可能导致内存溢出。使用软应用即可以解决:用一个 HashMap 来保存图片的路径和相应图片对象关联的软引用的映射关系。在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效避免 OOM 问题。 Map<String,SoftReference<Bitmap>> imageCache = new HashMap<String,SoftReference<Bitmap>>();
虚引用需要java.lang.ref.PhantomReference类来实现。虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有任何引用一样,任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用。
虚引用的主要作用是跟踪对象被回收的状态。仅仅是提供了一种确保对象被 finalize 以后,做某些事的机制。
PhantomReference 的 get 方法总是返回 null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入 finalization 阶段 gc 回收,用于实现比 finalization 机制更灵活的回收操作。
如结果所示:对于弱引用,当进行垃圾回收时,首先会被放入到一个引用队列中。
public class PhantomReferenceDemo { public static void main(String[] args) { Object obj1 = new Object(); ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); PhantomReference<Object> phantomReference = new PhantomReference<>(obj1,referenceQueue); System.out.println(obj1 + "\t" + phantomReference.get()+"\t" + referenceQueue.poll()); System.out.println("============gc================"); obj1 = null; System.gc(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(obj1 + "\t" + phantomReference.get()+"\t" + referenceQueue.poll()); } }栈溢出异常,通常发生在递归调用且未添加终止条件时。使用 -Xss 参数可以更改栈的大小。
public class StackOverflowErrorDemo { public static void main(String[] args) { stackOverFlowError(); } private static void stackOverFlowError(){ stackOverFlowError(); } } Exception in thread "main" java.lang.StackOverflowError at com.atLearn.StackOverflowErrorDemo.stackOverFlowError(StackOverflowErrorDemo.java:9)当new大对象或者不断new新对象,导致new出来的内存超过了heap的大小,会导致OOM: java heap space异常。
/** * -Xms5m -Xmx5m -XX:+PrintGCDetails */ public class JavaHeapSpaceDemo { public static void main(String[] args){ // java 虚拟机中的内存总量 long totalMemory = Runtime.getRuntime().totalMemory(); // Java 虚拟机试图使用的最大内存 long maxMemory = Runtime.getRuntime().maxMemory(); System.out.println("total memory: "+totalMemory/(1024* 1024)); System.out.println("max memory: "+maxMemory/(1024* 1024)); String str ="seu"; while (true){ str += str + new Random().nextInt(11111111)+new Random().nextInt(22222222); str.intern(); } } }程序在垃圾回收上话费了 98% 的时间,却收集不了 2% 的空间, 通常这样的异常伴随着 CPU 的冲高。
GC 回收时间过长会抛出 OutOfMemeoryError。 过长的定义是:超过 98% 的时间用来做 GC,并且回收了 2% 的堆内存连续多次 GC都只回收了不到 2% 的极端情况下才会抛出。如果不抛出 GC overhead limit,那么 GC 清理了一点点内存后很快就会被再次被填满。迫使 GC 再次执行,导致恶性循环。
/** * -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m -Xms12m -Xmx12m */ public class GCOverheadDemo { public static void main(String[] args) { int i =0; List<String> list = new ArrayList<>(); try { while (true) { list.add(String.valueOf(++i).intern()); } }catch (Throwable e) { System.out.println("i: " + i); e.printStackTrace(); throw e; } } }在NIO程序中,经常需要使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)和缓冲区(Buffer)的IO方式。它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存引用进行操作。这样能够在一些场景中显著提高性能,因为可以避免在Java堆和Native堆中来回复制数据:
ByteBuffer.allocate(capacity):第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度较慢;ByteBuffer.allocateDirect(capacity):这种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快;但是如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectBuffer对象们就不会被收,这时候堆内存充足,但是本地内存可能已经使用完毕,再次尝试分配本地内存就会出现OOM,程序直接崩溃:
/** * -XX:+PrintGCDetails -XX:MaxDirectMemorySize=2m */ public class DirectBufferMemory { public static void main(String[] args) { System.out.println("maxDirectMemory: "+sun.misc.VM.maxDirectMemory()/(1024 * 1024)); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } ByteBuffer bb = ByteBuffer.allocateDirect(10 * 1024 * 1024); } }高并发请求服务器时,经常出现如下异常:java.lang.OutOfMemoryError:unable to create new native thread,准确的讲该native thread异常与对应的平台有关。
导致原因:
应用创建了太多的线程,超过了系统承载极限对应的服务器不允许你的进程创建过多的线程,linux默认允许单个进程可以创建线程数1024个解决方案:
想办法降低你的进程创建线程的数量,分析程序是否真的需要创建那么多的线程;如果应用确实需要创建很多线程,需要修改linux默认配置,扩大linux默认限制; public class UnableCreateNewNativeThread { public static void main(String[] args) { for(int i =1;;i++) { System.out.println("i: "+i); new Thread(()->{ try { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } },""+i).start(); } } }Metaspace是Java8及其以后版本中使用的,用来代替永久代。Metaspace是方法区在HotSpot中的实现,它与永久带最大的区别是:Metaspace 并不在JVM内存中而是直接使用本地内存。也就是说在Java8中,class metadata (the virtual machines internal presentation of Java class),被存储在叫做Metaspace的Native Memory中。
永久代(Metaspace)存储的信息:JVM加载的类信息常量池静态变量即时编译后的代码 /** * 使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:MetaspaceSize为21810376B(约20M) * * VM options: -XX:+PrintGCDetails -XX:MetaspaceSize=1m -XX:MaxMetaspaceSize=1m */ public class MetaspaceOOMT { static class OOMTest{ } public static void main(String[] args) { int i = 0; try{ while (true){ i++; } }catch (Throwable e){ e.printStackTrace(); System.out.println("i: "+i); } } } Error occurred during initialization of VM OutOfMemoryError: MetaspaceGC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集。
java -XX:+PrintCommandLineFlags -version
对于一个正在运行的Java程序,可以通过jps找到Java的pid,然后 jinfo flag UseXxxGC pid 来查看 当前运行的程序是否开启了指定的垃圾回收器。
JVM垃圾回收器日志中参数说明: DefNew: Default New GenerationTenured: 老年代 OldParNew: Parallel New GenerationPSYoungGen: Parallel ScavengeParOldGen: Parallel Old Generation单线程的垃圾回收器。在进行垃圾回收时,必须暂停其他所有的工作线程直到垃圾回收线程完成工作,其他线程才可以继续。 串行收集器是最稳定,效率最高的收集器。只是用一个线程去回收。但是其在进行垃圾时会导致可能较长时间的停顿(Stop the World)。
对于限定单个 CPU 环境来说,没有线程交互的开销就可以获得最高的单线程垃圾手机效率,因此 Serial 垃圾收集器依然是 JVM 运行在 Client 模式下默认的新生代垃圾收集器。
对应参数-XX:+UseSerialGC
开启后: Serial + Serial Old 的垃圾收集器组合使用. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
新生代使用串行复制算法,,老年代使用串行 标记-整理算法。
并行 GC 使用多线程进行垃圾回收。在进行垃圾收集时,会 STW 暂停其他所有工作线程直到收集结束。
ParNew 收集器是 Serial 收集器新生代的并行多线程版本。其行为基本与 Serial 收集器一致。ParNew 垃圾收集器在垃圾收集工程中同样需要暂停其他所有工作线程。
对应参数 -XX:+UseParNewGC-XX:ParallelGCThread 限制线程数量 -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGCParallel Scavenge 收集类似 ParNew 是一个新生代收集器,使用复制算法。同时也是一个多线程的并行垃圾收集器。
优势
可控制吐吞量自使用调节策略参数
-XX:+UseParallelGC-XX:+UseParallelOldGC-XX:ParallelGCThreads=N 开启 N 个GC线程-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags
Serial Old 是 Serial 垃圾收集器老年版本,同样采用单线程的收集器,使用 标记-整理 算法,这个收集器也是 Client 默认的 JVM 老年代垃圾收集器。
Parallel Old收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法。
新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略。
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
JVM 参数 -XX:+UseConcMarkSweepGC 这个参数会默认开启 -XX:+UseParNewGC。-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC
步骤
初始标记 CMS initial mark并发标记 CMS concurrent mark (和用户线程一起,不需要停顿工作线程)重新标记 CMS remark (修正并发标记记录)并发清除 CMS concurrent sweep ((和用户线程一起,不需要停顿工作线程)优点
并发收集低停顿缺点
并发执行,对CPU资源压力大采用的标记清除算法会导致大量碎片G1收集器是一种面向服务端的垃圾回收器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能满足垃圾收集暂停时间的要求,用于取代CMS 垃圾收集器,在JDK9中,G1是默认的垃圾回收器。
在G1收集器中,Region之间的对象引用及其他收集器中的新生代和老年代之间的对象引用,JVM都是通过Remembered Set来避免全堆扫描。G1中每个Region 都有一个与之对应的Remembered Set,JVM发现程序对Reference引用的对象进行写操作时,会产生一个写中断(Write Barrier),监测Reference引用的对象是否处于不同的Region之中。如果是,便通过CardTable把相关用信息记录到被引用对象所属的Region的 Remembered Set中。当进行内存回收时,在GC更节点的枚举范围内加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
特点
G1 充分利用多 CPU,多核环境的硬件环境,尽量缩短 STW;G1 采用 标记-整理 算法,局部通过复制算法,不会产生内存碎片;G1 不再区分新生代和来年代,把内存划分成多个独立的子区域;G1 收集器将整个内存都混合在一起,但是对于不同的独立内存块,依然采用不同的 GC 方式处理;G1 依然是分代分块收集器G1收集器的运作步骤
初始标记: 只对GC Roots能够直接关联到的对象进行标记,存在STW;并发标记: 对GC Roots关联的对象进行可达性分析,可以并发执行;最终标记: 修正并发标记期间因为应用程序继续执行而导致变化的那一部分对象,存在STW;筛选回收: 根据时间来进行价值最大化的回收;参数
-XX:+UseG1GC-XX:G1HeapRegionSize=n: G1Region的大小,值必须是2的幂,范围在1MB到32MB-XX:MaxGCPauseMillis=n:最大GC停顿时间大小-XX:ConcGCThreads=n:并发GC使用的线程数-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
优势: 不产生内存碎片可以让用户指定期望的GC停顿时间,G1会根据允许的停顿时间区收集回收价值最高的垃圾