第二部分 自动内存管理机制

第2章 Java内存区域与内存溢出异常

简写

OOME:OutOfMemeryError
SOFE:StackOverflowError

2.2 内存数据区域

2.2.1 程序计数器

程序计数器(Program Counter Register)是一块小的内存空间,是当前线程所执行的字节码的行号指示器
在多线程的情况下,各个线程拥有独立计数器。
执行Java方法时,指向字节码指令地址,Native方法时为Undefined(此处没有规定OOME:JVM规范)。

2.2.2 Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks),线程私有。
定义:方法执行的内存模型。
创建栈帧(Stack Frame),存储局部变量表、操作数栈、动态链接、方法出口等信息。

局部变量表

局部变量表:编译期可知的基本数据类型、对象引用和returnAddress(指向字节码指令地址)。
64位long和double占用2个局部变量空间(Slot),其他1个。
其空间在编译期完成分配。

异常规定

SOFE:线程请求的栈深度大于虚拟机允许的深度。
OOME:无法申请到额外的内存

2.2.3 本地方法栈

本地方法栈(Native Method Stack)。
为Native方法提供服务。
可能与JVMS合一(Sun Hotspot)。
同样也会抛出SOFE,OOME。

2.2.4 Java堆

所有线程共享。
作用:存放对象实例和数组,不是绝对(见11章11.3.5节后期优化相关)。
GC主要区域
细分:新生代,老年代。(Eden,From Survivor,To Survivor)
堆可以处于物理不连续的空间。(逻辑连续即可)
OOME: 内存不足且无法扩展时。

直接内存

NIO里使用Native函数库分配堆外内存。
通过堆中的DirectNyteBuffer对象作为引用进行操作。
好处:避免在Java堆和Native堆中来回复制数据。
可能出现OOME。
详细介绍见第3章

2.2.5 方法区

方法区(Method Area)。
作用:存储加载的类信息、常量、静态变量、即时编译代码等。
是堆的一个逻辑部分,别名Non-Heap(非堆)。
别名:永久代,但本质上不等价。Hotspot基于永久代实现方法区,且GC可以扩展过去。
规范指明:可以不实现GC。
OOME:内存不足。

2.2.6 运行时常量池(方法区)

方法区的一部分。
作用:存放字面量和符号引用。
运行期也可以放入,如String类的intern()方法。
会抛出OOME

概念解释

字面量:字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。比如字面量3,也就是指3。再比如 string类型的字面量"ABC", 这个"ABC" 通过字来描述, 所以就是字面量。
符号引用:
一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。
直接引用:
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄

2.3 HotSpot虚拟机管理对象过程

创建

  1. 检查类加载情况;(见7章)
  2. 从堆分配内存
    1. 内存规整时,指针直接移位,称为“指针碰撞 Bump the Pointer”。
    2. 不规整时,需要维护列表,称为“空闲列表 Free List
    3. 方式由内存是否规整确定,内存规整由GC器是否带有压缩整理功能确定。
    4. 如何保证线程安全:
      1. CAS
      2. 隔离分配,每个线程拥有一部分本地线程分配缓冲。
  3. 初始化为零值
  4. 初始化对象头:类的元数据
  5. 执行对象的

见目录,HotSpot解释器代码片段

对象内存布局

组成:对象头,实例数据,对齐填充。

对象头

包含两部分

  1. Mark Word:hashcode、GC分代年龄、锁状态、线程持有锁、偏向锁(线程)id
  2. 类型指针:指向类元数据,非必须。
  3. 对于数组包含长度。

MarkOop

偏向锁:
作用:在单线程(非并发情况,即没有竞争锁的时候)访问同步代码块的时候,可以忽略同步锁机制,来提升性能,
举例:
比如有一线程A,第一次访问同步代码块,申请锁对象,拿到锁对象后,把线程A的ID 写入对象头即偏向线程ID,会把锁对象的状态改为01,即偏向锁状态,此时线程A再次进入同步代码块的时候,则直接忽略掉同步代码块,这样就达到提高性能的作用。(在并发时,偏向锁是多余的们也可以理解为不存在的,因为他会自动升级为轻量级锁,和重量级锁,)

实例数据

各数据字段数据

对齐填充

HotSpot 要求起始地址为8字节的整数倍

2.3.3 对象访问定位

实现方式:

  1. 句柄:reference中存储稳定的句柄地址,在对象移动时(GC)无需修改regerence
  2. 直接指针:访问实例速度更快。(HotShot采用)


2.4 OOME测试

工具:https://www.eclipse.org/mat/
Xms:堆最小值
Xmx:堆最大值
HeapDumpOnOutOfMemoryError:OOME时保存快照

import java.util.ArrayList;
import java.util.List;
/**
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * @author ichukai
 */
public class HeapOOM {
    static class OOMObject{
    }
    public static void main(String[] args){
        List<OOMObject> list=new ArrayList<OOMObject>();
        while (true){
            list.add(new OOMObject());
        }
    }
}

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid83183.hprof ...
Heap dump file created [27724413 bytes in 0.178 secs]

2.4.2 虚拟机栈和本地方法栈溢出SOF

无限递归栈溢出

stack length:774
Exception in thread "main" java.lang.StackOverflowError

创建线程导致内存溢出

java.lang.OutOfMemoryError: unable to create new native thread
  1. 可以通过减少最大堆来换取更多线程

code

不要测试线程代码,会导致系统卡死

/**
 * VM Args: -Xss160k
 */
public class JavaVmStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    private static void endlessLoop() throws Throwable {
        JavaVmStackSOF javaVmStackSOF = new JavaVmStackSOF();
        try {
            javaVmStackSOF.stackLeak();
        } catch (Throwable t) {
            System.out.println("stack length:" + javaVmStackSOF.stackLength);
            throw t;
        }
    }
    private static void leakByTread() {
        while (true){
            Thread thread=new Thread(new Runnable() {
                public void run() {
                    while (true){
                    }
                }
            });
            thread.start();
        }
    }
    public static void main(String[] args) throws Throwable {
        endlessLoop();
//       leakByTread(); //不要测试线程代码,会导致系统卡死
    }
}

2.4.3 方法区和常量池溢出

JDK7此方法已失效

import java.util.ArrayList;
import java.util.List;

/**
 * VM args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args){
        // 保持常量池引用
        List<String> list=new ArrayList<String>();
        int i=0;
        while (true){
            list.add(String.valueOf(i++).intern());
        }
    }
}

String.intern():

jdk7下前者为true,因为intern方法不再把值放到常量池,而是放实例引用,而后者“java”已经有引用了,所以不是同一个。

public class StringInternTest {
    public static void main(String[] args){
        String str1=new StringBuilder("软件").append("开发").toString();
        System.out.println(str1==str1.intern()); 
        String str2=new StringBuilder("ja").append("va").toString();
        System.out.println(str2==str2.intern());
    }
}

动态类导致方法区溢常

通过CGLib填充动态类信息,可导致OOM。
动态产生的大量JSP应用
基于OSGi应用,被不同加载器加载。

直接内存溢出

由DirectMemory导致的内存溢出,在dump不会有明显异常,使用了NIO可以排查。
MaxDirectMemorySize 默认与Xmx(堆最大值)一样。
测试下来无效,后面再去看Java 规范

import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
 */
public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) throws IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}

第3章 垃圾收集器和内存分配策略

3.1 概述

GC三问:

  1. 哪些内存需要收集 what
  2. 什么时候收集 when
  3. 如何收集 how

Why:
为什么要学:排查内存溢出、泄漏问题,当GC成为性能瓶颈。

线程私有的区域不需要考虑回收问题。

3.2如何判断对象失效

  1. 引用计数法(错误,相互引用但仍然收集了)。

3.2.1可达性分析法

GC Root对象包括

  1. 虚拟机栈(栈帧中本地变量表)中引用的对象
  2. 方法区类静态引用的对象
  3. 方法区常量引用对象
  4. 本地方法栈中JNI(Native方法)引用的对象

3.2.3 多种引用

目的:利用缓存,有空间保留,无空间抛弃。
强引用:无论如何保留
软引用:有空间保留,内存要溢出时清理,通过SoftReference
弱引用:保留到下次收集前,WeakReference
虚引用:给GC一个通知,PhantomReference

3.2.3 生存还是死亡

  1. 经过两次标记,第一次时如果有finalize()方法则执行。
  2. 不要使用finalize

3.2.4 回收方法区

无用类判断:

  1. 所有实例以回收
  2. 加载该类的ClassLoader已回收
  3. 类对应的java.lang.class对象没有被引用

动态生成类的场景需要

3.3 垃圾回收算法

  1. 标记清除:效率不高,内存碎片
  2. 复制:效率高,但需要额外内存
  3. 标记整理:先整理到一端,然后回收

3.3.4 分代收集

新生代使用复制
老年代标记清理/整理

3.4 hotSpot算法实现

3.4.1 枚举跟节点

  1. 准确式GC:虚拟机知道对象引用位置,使用OopMap的数据结构记录。

3.4.2 安全点

通过设置安全点,让所有线程停顿。
中断方式:

  1. 抢先中断,直接中断线程,如果没到安全点,让其执行至安全点
  2. 线程主动中断,线程轮询标志

3.4.3 安全区域

  1. 进入安全区域后检查标识,执行GC

3.4 垃圾收集器

回收的具体实现。

3.5.1 serial

  1. 单线程,stop the world
  2. 在client模式下是可行的选择

3.5.2 ParNew

  1. Serial的多线程版本

3.5.3 Parallel Scavenge

  1. 新生代,复制算法,多线程
  2. 控制吞吐量

3.5.4 Serial Old

  1. 单线程,老年代
  2. 标记整理

3.5.5

  1. 多线程
  2. 标记整理

3.6.7

  1. 并发
  2. 标记清除

G1 3.5.7

  1. 并行与并发:利用多核环境
  2. 分代收集
  3. 空间整合,整体基于标记-整理,局部基于复制
  4. 可预测的停顿:使用者能指定,通过分区回收

步骤:

  1. 视始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

3.5.8GC日志

33.125:【gc[De fNew:3324K->152K(3712K)0.0025925secs】3324k->152K(11904K)0.0031680secs
100.667:【Full GCTenured:0k->210K(10240K),0.0149142secs4603k->210k(19456K),【erm:299k->2999k(21248k),0.0150007secs】【Times: user=0.01 sys=0. 00, real=0.02 secs]

最前数字:发生时间,启动后秒数
DefNew 区域,跟GC器的类型有关,名字可能不同。
299k->2999k(21248k):回收前->回收后(总容量)
"[GC"说明只收集GC堆的部分区域。通常就是minor GC,只收集young gen。
"[Full GC"说明收集了整个GC堆的所有区域,包括young、old、perm。
这两种日志都是stop-the-world GC的表现。

3.5.9 参数总结

3.6 内存分配与回收策略

自动内存管理解决的问题:给对象分配内存和回收对象
JVM区域总体分两类,heap区和非heap区。
Heap区又分为:
Eden Space(伊甸园)、
Survivor Space(幸存者区)、
Old Gen(老年代)。
非heap区又分:
Code Cache(代码缓存区);
Perm Gen(永久代);
Jvm Stack(java虚拟机栈);
Local Method Statck(本地方法栈);
默认 eden:survivor=8:1

3.6.1 对象优先在Eden分配

内存不足时发起Minor GC

3.6.2 大对象直接进入老年代

典型:很长的字符串和数组。
避免大量复制。(新生代基于复制算法)

3.6.2 长期存活的对象直接进入老年代

通过Age计算器,每过一次Minor GC+1,默认15岁

3.6.4 动态对象年龄判定

如果Survivor中相同年龄的对象大于空间的一半,把大于该年龄的放入老年代

3.6.5 空间分配担保

在MinorGC时,内存不足时放入老年代担保。
失败则进行Full GC。

第4章 虚拟机性能监控与故障处理工具

4.2 命令行工具

4.2.1 jps:虚拟机进程状况工具

JVM Process Status,查询进程:

jps -l
90032 jdk.jcmd/sun.tools.jps.Jps
83269 org.jetbrains.idea.maven.server.RemoteMavenServer
46853 org.apache.catalina.startup.Bootstrap
51623 org.jetbrains.jps.cmdline.Launcher

4.2.2 jstat:虚拟机统计信息监视工具

GC、类加载信息等等。

jstat -options
-class
-compiler
-gc
-gccapacity
-gccause
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation

4.2.3 jinfo:Java配置信息工具

查看JVM启动参数
-v 查看显示指定的值

jinfo -v 46853

4.2.4jmap:Java内存映像工具

生成堆转储快照
jmap -dump vmid
-dump 快照

4.2.5 jhat

快照分析工具

4.2.6 jstack

Stack Trace for java
线程快照工具

4.2.7 HSDIS:JIT 生成代码反汇编

4.3 可视化工具

4.3.1 JConsole:Java监视与管理控制台

  1. 内存监控
  2. 线程监控:死锁

4.3.2 visualVM

https://visualvm.github.io/download.html

  1. 插件中心
  2. 生成快照
  3. 分析程序性能
  4. BTrace插件:加入调试代码

其他工具

Eclipse Memory Analyzer

hprof file工具
https://www.eclipse.org/mat/

Comments
Write a Comment