深入理解JVM读书笔记--内存管理

时间:2023-02-15 11:54:08

一. Java的运行时数据区域

深入理解JVM读书笔记--内存管理

  (1)程序计数器(线程私有):是一块较小的内存空间,它的作用是当前线程所执行字节码的行号指示器。字节码解释器就是通过计数器的值来获得下一条需要执行的指令。

如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址,如果执行的是native方法,这个区域为空。

Java中的多线程为了能够获得正确的执行位置,每一个线程都需要一个独立的程序计数器,这块内存称为"线程私有内存"

这也是唯一一个java虚拟机规范没有规定任何OutOfmemroyError的区域。

  (2)虚拟机栈(线程私有):它与线程的生命周期相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行时都会创建一个栈帧(stack frame),用于存储局部变量表,操作数栈,动态链接,方法出口等信息。

局部变量表存放基本数据类型(booleanbytecharshortintfloatlongdouble),对象引用,returnAddress(字节码指令)类型。

这两个区域常见的异常信息:*Error:线程请求的深度超过虚拟机规定的深度;OutOfMemoryError:无法申请到足够的内存。

  (3)本地方法栈(Native method Stack类似于虚拟机栈。

  (4)堆(java heap)(线程共享): 所有线程共享的区域,java虚拟机启动的时候创建,用于存放对象实例。

虚拟机规范对堆的描述:所有的对象实例和数组都要在堆上分配,但是随着JIT技术和逃逸分析技术的成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有对象都分配在堆上渐渐变得不是那么绝对了。

它也是垃圾回收的主要区域。

  (5)方法区(线程共享):用于存储已被加载的类的结构信息,常量,静态变量,即时编译后的代码等数据。

这个区域的垃圾回收目标主要是针对常量池的回收和对类型的卸载,但是回收次数较少。

  (6)运行时常量池:方法区的一部分。用于存放编译期生成的各种字面量和符号引用。这部分区域是在类被JVM加载之后就创建出来的。

一般来讲除了保存class文件中描述的符号引用外,还以把翻译出来的直接引用也存在运行时数据区。

  (7)直接内存:这不是虚拟机运行时数据区的一部分。NIO引入了一种基于通道与缓冲区的I/O方式,他可以使用Native函数库直接分配堆外的内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

二. 新创建的对象在内存中的分配

 深入理解JVM读书笔记--内存管理

深入理解JVM读书笔记--内存管理

 

 

 

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) 方法区溢出

思路:产生大量的类填充方法区。如SpringHibernate中产生过多的代理类。

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参数进行控制,还可以使用-verboseclass以及-XX:+TraceClassLoading-XXTraceClassUnLoading查看累的加载和卸载信息。

6. 垃圾收集算法

a) 标记清除算法:标记清除算法分为两个阶段:首先标记需要回收的所有对象,标记完成后清除掉所有被标记的对象。

该算法的缺点:

效率问题:标记和清除的效率都不高;

空间问题:标记清除后会产生大量不连续的内存碎片。

 

深入理解JVM读书笔记--内存管理

 

 

b) 复制算法:将内存分成大小相等的两块,每次自用其中的一块,没进行一次垃圾回收把未清除对象复制到另外一块上。

该算法的优点:不用考虑内存碎片的问题,每次只要移动堆指针,按顺序分配内存即可,实现简单,运行高效。

该算法的缺点:算法将原来的内存缩减为一般大小,代价太高。

深入理解JVM读书笔记--内存管理

 c) 标记-整理算法

深入理解JVM读书笔记--内存管理

d) 分代收集算法

这种算法将java堆分为新生代和老年代。根据新生代和老年代的不同分别按照不同的算法进行收集。新生代使用复制算法,而老年代使用标记-清除或者标记-整理算法。

e) 

深入理解JVM读书笔记--内存管理

 

7. 垃圾收集器

a) Serial收集器:这是一个单线程收集器,他在进行垃圾收集时必须停止所有的其他工作线程。

Serial收集器对于运行在Client模式下的桌面虚拟机来说是一个很好的选择。

 

深入理解JVM读书笔记--内存管理

(非常类似于java多线程中的Barrier或者CountDownLatch)

b) ParNew收集器:Serial收集器的多线程版本。

它是运行在Server模式下的新生代的收集器。

可用参数:-XX:SurvivorRatio, -XX:PretenurSizeThreshold, -XX:HandlePromotionFailure, -XX:UseConcMarkSweepGC, -XX:UseParNewGC, -XX:ParallelGCThreads

 

深入理解JVM读书笔记--内存管理

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时使用。

 

深入理解JVM读书笔记--内存管理

e) Parallel Old收集器:Parallel Old的老年代版。使用多线程和"标记-整理"算法。

 

深入理解JVM读书笔记--内存管理

f) CMS收集器:一种以获取最短停顿时间为目标的收集器。它采用的是"标记-清除"算法。

工作过程:初始标记(initial mark)

  > 并发标记(concurrent mark)

  > 重新标记(remark)

  > 并发清楚(concurrent sweep)

在初始标记和重新标记两个阶段需要"stop the world"。初始标记标记的是GC Roots能够直接关联的对象,并发标记是Roots Tracing的过程,重新标记是标记哪些产生变动的对象记录。

 

深入理解JVM读书笔记--内存管理

g) G1收集器

8. 垃圾收集常用的参数

深入理解JVM读书笔记--内存管理

 

三.内存分配与回收策略

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. 大对象直接进入老年代。

虚拟机提供了一个参数:-XXPretenureSizeThreshold,用来设定大对象直接分配到老年代。这个参数只对SerialParNew有效。

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.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。

4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

5.空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。