一. Java的运行时数据区域
(1)程序计数器(线程私有):是一块较小的内存空间,它的作用是当前线程所执行字节码的行号指示器。字节码解释器就是通过计数器的值来获得下一条需要执行的指令。
如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址,如果执行的是native方法,这个区域为空。
Java中的多线程为了能够获得正确的执行位置,每一个线程都需要一个独立的程序计数器,这块内存称为"线程私有内存"。
这也是唯一一个java虚拟机规范没有规定任何OutOfmemroyError的区域。
(2)虚拟机栈(线程私有):它与线程的生命周期相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行时都会创建一个栈帧(stack frame),用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
局部变量表存放基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用,returnAddress(字节码指令)类型。
这两个区域常见的异常信息:*Error:线程请求的深度超过虚拟机规定的深度;OutOfMemoryError:无法申请到足够的内存。
(3)本地方法栈(Native method Stack): 类似于虚拟机栈。
(4)堆(java heap)(线程共享): 所有线程共享的区域,java虚拟机启动的时候创建,用于存放对象实例。
虚拟机规范对堆的描述:所有的对象实例和数组都要在堆上分配,但是随着JIT技术和逃逸分析技术的成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有对象都分配在堆上渐渐变得不是那么绝对了。
它也是垃圾回收的主要区域。
(5)方法区(线程共享):用于存储已被加载的类的结构信息,常量,静态变量,即时编译后的代码等数据。
这个区域的垃圾回收目标主要是针对常量池的回收和对类型的卸载,但是回收次数较少。
(6)运行时常量池:方法区的一部分。用于存放编译期生成的各种字面量和符号引用。这部分区域是在类被JVM加载之后就创建出来的。
一般来讲除了保存class文件中描述的符号引用外,还以把翻译出来的直接引用也存在运行时数据区。
(7)直接内存:这不是虚拟机运行时数据区的一部分。NIO引入了一种基于通道与缓冲区的I/O方式,他可以使用Native函数库直接分配堆外的内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
二. 新创建的对象在内存中的分配
3. 实战OutOfMemoryException
a) 模拟Heap内存溢出
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
异常信息:
[GC [DefNew: 8192K->1024K(9216K), 0.0262916 secs] 8192K->4618K(19456K), 0.0263380 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] [GC [DefNew: 6401K->1024K(9216K), 0.0352715 secs] 9996K->9748K(19456K), 0.0353201 secs] [Times: user=0.05 sys=0.00, real=0.05 secs] [GC [DefNew: 7664K->7664K(9216K), 0.0000458 secs][Tenured: 8724K->10240K(10240K), 0.0796814 secs] 16388K->11929K(19456K), [Perm : 379K->379K(12288K)], 0.0798297 secs] [Times: user=0.08 sys=0.00, real=0.08 secs] [Full GC [Tenured: 10240K->8002K(10240K), 0.0838774 secs] 19456K->15528K(19456K), [Perm : 379K->379K(12288K)], 0.0839470 secs] [Times: user=0.09 sys=0.00, real=0.09 secs] [Full GC [Tenured: 8604K->8604K(10240K), 0.0847476 secs] 17820K->17820K(19456K), [Perm : 379K->379K(12288K)], 0.0850530 secs] [Times: user=0.08 sys=0.00, real=0.08 secs] [Full GC [Tenured: 8604K->8591K(10240K), 0.0913390 secs] 17820K->17807K(19456K), [Perm : 379K->373K(12288K)], 0.0914197 secs] [Times: user=0.09 sys=0.00, real=0.09 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2760) at java.util.Arrays.copyOf(Arrays.java:2734) at java.util.ArrayList.ensureCapacity(ArrayList.java:167) at java.util.ArrayList.add(ArrayList.java:351) at org.wk.core.jvm.HeapOOM.main(HeapOOM.java:14) Heap def new generation total 9216K, used 9216K [0x315e0000, 0x31fe0000, 0x31fe0000) eden space 8192K, 100% used [0x315e0000, 0x31de0000, 0x31de0000) from space 1024K, 100% used [0x31de0000, 0x31ee0000, 0x31ee0000) to space 1024K, 0% used [0x31ee0000, 0x31ee0000, 0x31fe0000) tenured generation total 10240K, used 8597K [0x31fe0000, 0x329e0000, 0x329e0000) the space 10240K, 83% used [0x31fe0000, 0x32845748, 0x32845800, 0x329e0000) compacting perm gen total 12288K, used 374K [0x329e0000, 0x335e0000, 0x369e0000) the space 12288K, 3% used [0x329e0000, 0x32a3d8f8, 0x32a3da00, 0x335e0000) ro space 10240K, 54% used [0x369e0000, 0x36f5e4a8, 0x36f5e600, 0x373e0000) rw space 12288K, 55% used [0x373e0000, 0x37a822a0, 0x37a82400, 0x37fe0000)
|
b) 模拟虚拟机栈和本地方法栈内存溢出
-verbose:gc -Xss128k
stack length2404 Exception in thread "main" java.lang.*Error at org.wk.core.jvm.JavaStackSOF.stackLeak(JavaStackSOF.java:6) at org.wk.core.jvm.JavaStackSOF.stackLeak(JavaStackSOF.java:7) at org.wk.core.jvm.JavaStackSOF.stackLeak(JavaStackSOF.java:7) |
c) 运行时常量溢出(典型的方法是String.intern())
参数设置:-XX:PermSize -XX:MaxPermSize
-verbose:gc -XX:PermSize=10M -XX:MaxPermSize=10M
[GC 4416K->493K(15872K), 0.0041773 secs] [GC 4187K->666K(15872K), 0.0039332 secs] [GC 4588K->1441K(15872K), 0.0041589 secs] [Full GC 3212K->1829K(15872K), 0.0617274 secs] [Full GC 1829K->1829K(15936K), 0.0578372 secs] Exception in thread "main" [Full GC 1906K->666K(15936K), 0.0214695 secs] java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at org.wk.core.jvm.RunTimeConstantPoolOOM.main(RunTimeConstantPoolOOM.java:12)
|
d) 方法区溢出
思路:产生大量的类填充方法区。如Spring或Hibernate中产生过多的代理类。
e) 本机直接内存溢出
参数:-XX:MaxDirectMemorySize制定。默认的是与java堆的最大值一样。
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at org.wk.core.jvm.DirectMemoryOOM.main(DirectMemoryOOM.java:15) |
二. 垃圾回收算法
1. 引用计数法
思路:给每一个对象添加一个引用计数器,如果有一个地方引用该对象时,计数器加一,当引用失效时,计数器减一,垃圾回收器只需要收集计数器为0的对象。
2. 根搜索算法
a) 思路:通过一系列的名为"GC Roots"的对象作为起点,从这些节点向下搜索,搜索的路径成为引用连(Reference Chain),当一个对象到"GC Roots"没有任何应用链相连,则证明此对象不可用,即为可回收对象。
b) Java中的GC Roots
i. 虚拟机栈(栈帧中的本地变量表)中的引用对象
ii. 方法区中的类静态属性引用的对象
iii. 方法区中的常量引用对象
iv. 本地方法中的JNI引用对象
3. Java中引用的概念
a) 强引用(Strong Reference):程序中普遍存在,类似"Object obj = new Object()",只要引用存在,GC就不会回收该对象。
b) 软引用(Soft Reference):描述一些还有用,但是不是必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列到回收范围进行二次回收。(JDK中有SoftReference的实现)
c) 弱引用(Weak Reference):描述比软引用更弱的引用对象。它的生命周期是下一次垃圾回收之前。(JDK中有WeakReference的实现)
d) 虚引用(Phantom Reference):虚引用不会对对象的生命周期产生影响,更无法通过虚引用获得一个对象的实例。为对象设置一个虚引用的目的就是垃圾回收器回收时获得一个通知。
4. 如何判定一个类是无用的类:
a) 该类的所有实例已被回收。
b) 该加载类的ClassLoader 已被回收。
c) 该加载类的java.lang.Class对象没有在任何地方使用,即不能通过反射访问此类。
5. 通过虚拟机参数查看类的引用情况:-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XXTraceClassUnLoading查看累的加载和卸载信息。
6. 垃圾收集算法
a) 标记清除算法:标记清除算法分为两个阶段:首先标记需要回收的所有对象,标记完成后清除掉所有被标记的对象。
该算法的缺点:
效率问题:标记和清除的效率都不高;
空间问题:标记清除后会产生大量不连续的内存碎片。
b) 复制算法:将内存分成大小相等的两块,每次自用其中的一块,没进行一次垃圾回收把未清除对象复制到另外一块上。
该算法的优点:不用考虑内存碎片的问题,每次只要移动堆指针,按顺序分配内存即可,实现简单,运行高效。
该算法的缺点:算法将原来的内存缩减为一般大小,代价太高。
c) 标记-整理算法
d) 分代收集算法
这种算法将java堆分为新生代和老年代。根据新生代和老年代的不同分别按照不同的算法进行收集。新生代使用复制算法,而老年代使用标记-清除或者标记-整理算法。
e)
7. 垃圾收集器
a) Serial收集器:这是一个单线程收集器,他在进行垃圾收集时必须停止所有的其他工作线程。
Serial收集器对于运行在Client模式下的桌面虚拟机来说是一个很好的选择。
(非常类似于java多线程中的Barrier或者CountDownLatch)
b) ParNew收集器:Serial收集器的多线程版本。
它是运行在Server模式下的新生代的收集器。
可用参数:-XX:SurvivorRatio, -XX:PretenurSizeThreshold, -XX:HandlePromotionFailure, -XX:UseConcMarkSweepGC, -XX:UseParNewGC, -XX:ParallelGCThreads
c) Parallel Scavenge收集器(吞吐量优先收集器):它的关注点是达到一个可控制的吞吐量。
可用参数:-XX:MaxGCPauseMillis, -XX:GCTimeRatilo(1-100的整数), -XX:+UseAdaptivePolicy(这是一个开关参数,这个参数打开后就不需要用户手工指定新生代,Eden,与Survivor的大小,虚拟机会根据性能的监控信息自动的这些参数,以提供最合适的停顿时间和最大的吞吐量,这称为GC自适应调节策略)。
d) Serial Old收集器:单线程收集器,采用"标记整理算法"。主要是Client模式下的地JVM应用。Server模式下的主要用途:在JDK1.5以及以前的版本中配合Parallel Scavenge收集器搭配使用;另一个作为CMS的后背预案,在并发收集发生Concurrent Mode Failure时使用。
e) Parallel Old收集器:Parallel Old的老年代版。使用多线程和"标记-整理"算法。
f) CMS收集器:一种以获取最短停顿时间为目标的收集器。它采用的是"标记-清除"算法。
工作过程:> 初始标记(initial mark)
> 并发标记(concurrent mark)
> 重新标记(remark)
> 并发清楚(concurrent sweep)
在初始标记和重新标记两个阶段需要"stop the world"。初始标记标记的是GC Roots能够直接关联的对象,并发标记是Roots Tracing的过程,重新标记是标记哪些产生变动的对象记录。
g) G1收集器
8. 垃圾收集常用的参数
三.内存分配与回收策略
1. 对象优先分配在Eden区
当Eden区没有足够的内存空间时,发起一次Monitor GC。
package org.wk.core.jvm;
public class MinorGC { private static final int _1MB = 1024 * 1024;
public static void testAllocation() { byte[] a1, a2, a3, a4; a1 = new byte[2 * _1MB]; a2 = new byte[2 * _1MB]; a3 = new byte[2 * _1MB]; a4 = new byte[4 * _1MB]; }
public static void main(String[] arg) { testAllocation(); } } |
[GC [DefNew: 6487K->148K(9216K), 0.0094068 secs] 6487K->6293K(19456K), 0.0094970 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] Heap def new generation total 9216K, used 4408K [0x315e0000, 0x31fe0000, 0x31fe0000) eden space 8192K, 52% used [0x315e0000, 0x31a08fe0, 0x31de0000) from space 1024K, 14% used [0x31ee0000, 0x31f053f0, 0x31fe0000) to space 1024K, 0% used [0x31de0000, 0x31de0000, 0x31ee0000) tenured generation total 10240K, used 6144K [0x31fe0000, 0x329e0000, 0x329e0000) the space 10240K, 60% used [0x31fe0000, 0x325e0030, 0x325e0200, 0x329e0000) compacting perm gen total 12288K, used 378K [0x329e0000, 0x335e0000, 0x369e0000) the space 12288K, 3% used [0x329e0000, 0x32a3e920, 0x32a3ea00, 0x335e0000) ro space 10240K, 54% used [0x369e0000, 0x36f5e4a8, 0x36f5e600, 0x373e0000) rw space 12288K, 55% used [0x373e0000, 0x37a822a0, 0x37a82400, 0x37fe0000) |
2. 大对象直接进入老年代。
虚拟机提供了一个参数:-XX:PretenureSizeThreshold,用来设定大对象直接分配到老年代。这个参数只对Serial和ParNew有效。
public static void testPretenureSizeThresHold() { byte[] a4 = new byte[4 * _1MB]; } |
Heap def new generation total 9216K, used 507K [0x315e0000, 0x31fe0000, 0x31fe0000) eden space 8192K, 6% used [0x315e0000, 0x3165ef38, 0x31de0000) from space 1024K, 0% used [0x31de0000, 0x31de0000, 0x31ee0000) to space 1024K, 0% used [0x31ee0000, 0x31ee0000, 0x31fe0000) tenured generation total 10240K, used 4096K [0x31fe0000, 0x329e0000, 0x329e0000) the space 10240K, 40% used [0x31fe0000, 0x323e0010, 0x323e0200, 0x329e0000) compacting perm gen total 12288K, used 378K [0x329e0000, 0x335e0000, 0x369e0000) the space 12288K, 3% used [0x329e0000, 0x32a3e9f8, 0x32a3ea00, 0x335e0000) ro space 10240K, 54% used [0x369e0000, 0x36f5e4a8, 0x36f5e600, 0x373e0000) rw space 12288K, 55% used [0x373e0000, 0x37a822a0, 0x37a82400, 0x37fe0000) |
3. 长期存活的对象将进入老年代
可以通过参数:-XX:MaxTenuringThreshold设置。
public static void testTenuringThreshold() { byte[] a1, a2, a3; a1 = new byte[_1MB / 4]; a2 = new byte[4 * _1MB]; a3 = new byte[4 * _1MB]; a3 = null; a3 = new byte[4 * _1MB]; } |
参数值为:1时。
[GC [Tenured: 8192K->4501K(10240K), 0.3121115 secs] 8791K->4501K(19456K), [Perm : 378K->378K(12288K)], 0.3121796 secs] [Times: user=0.02 sys=0.01, real=0.31 secs] Heap def new generation total 9216K, used 327K [0x315e0000, 0x31fe0000, 0x31fe0000) eden space 8192K, 4% used [0x315e0000, 0x31631f98, 0x31de0000) from space 1024K, 0% used [0x31de0000, 0x31de0000, 0x31ee0000) to space 1024K, 0% used [0x31ee0000, 0x31ee0000, 0x31fe0000) tenured generation total 10240K, used 8597K [0x31fe0000, 0x329e0000, 0x329e0000) the space 10240K, 83% used [0x31fe0000, 0x32845420, 0x32845600, 0x329e0000) compacting perm gen total 12288K, used 378K [0x329e0000, 0x335e0000, 0x369e0000) the space 12288K, 3% used [0x329e0000, 0x32a3eaf8, 0x32a3ec00, 0x335e0000) ro space 10240K, 54% used [0x369e0000, 0x36f5e4a8, 0x36f5e600, 0x373e0000) rw space 12288K, 55% used [0x373e0000, 0x37a822a0, 0x37a82400, 0x37fe0000) |
参数值为:15时。
4.动态对象年龄判断
如果Survivor中相同年龄的对象占到此空间的一半,那么改年龄和大于该年龄的对象放到老年区。
5. 空间分配担保
在发生Minor GC时,会计算每次进入老年区的平均大小是否大于老年区空间的剩余大小,如果大于则进行一次Full GC。
四.对象分配规则
1.对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
2.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
5.空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。