深入理解java虚拟机-JVM高级特性和最佳实现(二)——了解jvm内存结构

每篇一叶

前言

上一回我们了解了java的历史背景和JVM的一些版本,这次我们要探索java的内存区域和内存溢出。
java和C++之间有一睹由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。java程序员将内存控制的权力交给了java虚拟机,一旦出现内存泄露和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会成为一项异常艰难的工作。


jdk1.8以前的数据模型

基本概念

  • 程序计数器
    线程隔离,一块较小的内存空间,当前程序所执行的字节码的行号指示器。唯一一个在java虚拟机规范中没有指定任何OutOfMemoryError情况的区域
  • java虚拟机栈
    线程隔离,生命周期和线程相同,每个方法执行的同时都会产生一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    • 局部变量表存放编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double),对象的引用(reference类型,不等同与对象本身,有可能是指向起始地址的引用指针,也可能是一个代表对象的句柄活其他与对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
      会出现的异常情况: StackOverflowError,线程请求的栈深度大于虚拟机所允许的深度;OutOfMemoryError,虚拟机栈可以动态扩展,扩展无法申请到足够内存。
  • 本地方法栈
    线程隔离,虚拟机栈为虚拟机指向java方法服务,本地方法栈为虚拟机提供Native方法服务。Sun HotSpot虚拟机直接将本地方法栈和虚拟机栈合二为一。
  • java堆
    线程内存共享。虚拟机所管理内存中最大的一块,所有线程共享。虚拟机规范中描述:所有对象实例以及数组都要在堆上分配。JIT编译器的发展后这个也不是绝对了GC的主要区域。收集器基本上采用分代收集算法-->java堆分为新生代和老年代。
  • 方法区
    线程内存共享。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据,java虚拟机任务是堆的一个逻辑部分,但是别名为非堆。有人称为永久代,其实并不等价,只是HotSopt虚拟机将GC分代收集扩展到了方法区,其他虚拟机并不存在永久代的概率。目前JDK1.7的HotSpot中已经将原本放在永久代的字符串常量池移出。JDK1.8剔除了永久代,取而代之的是元数据区(也称为元空间)。元数据区不属于JVM内存的一部分,它直接存储于本机内存,而将常量池移到了堆中。
  • 运行时常量池
    线程内存共享。方法区的一部分
  • 直接内存
    并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但这部分内存也被频繁使用。JDK1.4映入NIO,基于通道与缓冲区的I/O方式。利用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,避免java堆和Natie堆中来回复制数据。
jdk1.8内存模型
题目

以下结果在jdk1.6和jdk1.8中的结果

        String str2 = new String("str")+new String("01");
        str2.intern();
        String str1 = "str01";
        System.out.println(str2==str1);
jdk1.6:false
jdk1.8:true
分析原因:
intern()方法是当调用 intern 方法时,如果池已经包含一个等于此 `String` 对象的字符串,则返回池中的字符串。否则,将此 `String` 对象添加到池中,并返回此 `String` 对象的引用。

jdk1.6中常量池在永久代,如果字符串在常量池中找不到会将字符串拷贝到常量池中,如果str2.intern();替换成str2=str2.intern();这个时候str2代表的是对象的引用,则结果就是true了。
 jdk1.8中常量池已经移到了堆中,如果字符串在常量池中找不到不再拷贝到常量池中,而会重新生成一个对原字符串的引用

补充概念

JVM内存区域划分Eden Space、Survivor Space、Tenured Gen,Perm Gen解释

  • Eden Space 伊甸园(新生的对象)
  • Survivor Space 幸存者区
  • Tenured Gen 老年代-养老区
  • Perm Gen 永久代

HotSpot 虚拟机在java堆中对象分配、布局和访问的全过程。

HotSpot 虚拟机在java堆中对象分配、布局和访问的全过程
  • 对象的分配
  1. new指令--检查指令参数是否能在常量池中定位到类的符号引用,检查这个符号代表的类是否已经加载、解析,初始化,没有,执行类加载过程
  2. 类加载检查通过--为新生对象分配内存--
    1. 堆内存绝对规整,指针碰撞分配方式,用过的内存放一边,空闲内存放一边,中间放一个指针作为分界点指示器。
    2. 堆内存不规整。空闲列表分配方式。
  3. 具体采用什么分配方式取决于java堆是否规整。java堆是否规准由采取的垃圾收集器是否带有压缩整理功能决定。
    1. Serial、ParNew等带Compact过程的收集器,分配采用指针碰撞分配
    2. 使用CMS这种基于Mark-Sweep算法的收集器通常采用空闲列表。
  4. 分配内存并发情况,
    1. 动作同步处理,虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
    2. 内存分配按照线程划分在不同的空间进行。即每个线程在java堆中预先分配一小块内存,本地线程分配缓冲(TLAB),哪个线程需要分配内存就在哪个线程的TLAB伤分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
  5. 内存分配完后,虚拟机将分配的内存空间初始化为零值。使用TLAB,在TLAB分配时就直接进行这步。保证对象实例字段在java代码中不赋值就能直接使用。
  6. 设置对象。对象所属哪个类的实例,如何查找类的元数据信息,对象的哈希码,对象的GC分代年龄等,这些信息存放在对象头中。
  7. 上面完成后,虚拟机视角,一个新对象已经产生了,java程序视角,创建才刚刚开始,init还没执行,所有字段还为零。
  • 对象的访问


    通过句柄访问对象
通过指针访问对象

建立对象-->使用对象。java程序通过栈上的reference数据来操作堆上的具体对象。java虚拟机规范规定reference类型指向一个对象。
句柄好处:reference中存储的是句柄地址,在对象呗移动时只会改变句柄中的实例指针,而reference本身不修改
指针好处:速度更快,节省了一次指针定位的时间开销。就Sun HotSpot而言,它是使用第二种方式进行对象访问的。但从整个软件开发范围来看,句柄访问的情况也十分常见。

  1. 句柄。reference存储句柄地址。句柄中包含对象实例数据与类型数据各自的具体地址
  2. 指针。java堆对象中必须考虑放置访问类型数据的相关信息。reference中存储的是对象地址。

实战OutOfMemoryError

先设置VM args -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

  1. 堆内存溢出程序
/**
* VM Args: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOP {
    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
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
    at java.util.ArrayList.grow(ArrayList.java:261)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
    at java.util.ArrayList.add(ArrayList.java:458)
    at com.zwq.heap.HeapOOP.main(HeapOOP.java:13)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

  1. 虚拟机栈和本地方法栈溢出
public class JavaVMStackSop {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
JavaVMStackSop oom = new JavaVMStackSop();
try {
oom.stackLeak();
}catch (Throwable e){
System.out.println("stack length:"+oom.stackLength);
throw e;
}

}
}

异常信息

Exception in thread "main" java.lang.StackOverflowError
stack length:11387
    at com.zwq.heap.JavaVMStackSop.stackLeak(JavaVMStackSop.java:7)
    at com.zwq.heap.JavaVMStackSop.stackLeak(JavaVMStackSop.java:8)
...

  1. 方法区和运行时常量池溢出
/**
* 方法区和运行时常量池溢出
* String.intern()是一个native方法,在字符串常量池有则直接返回,没有则添加到常量池中
* JDK1.6及以前版本常量池在永久代内,通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,
*报错java.lang.outofmemoryerror:PermGen space
* 从而限制常量池容量
* 而JDK1.7后不会出现这个问题,会一直执行下去
*/
public class RunntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i=0;
while(true){
list.add(String.valueOf(i).intern());
}
}
}

/**
* 方法区内存溢出
*/
public class JavaMethodAreaOp {
public static void main(String[] args) {
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o,args);
}
});
enhancer.create();
}
}
static class OOMObject{

}
}

需要配置cglib包引用

 <!-- [https://mvnrepository.com/artifact/cglib/cglib](https://mvnrepository.com/artifact/cglib/cglib) -->

<dependency>

<groupId>cglib</groupId>

<artifactId>cglib</artifactId>

<version>3.2.4</version>

</dependency>

附:由于jdk6.0及以下版本和JDK7.0以上版本存在差异,以下代码运行结果不一致

public class RunntimeConstantPoolOOM {
    public static void main(String[] args) {
        String str1 = new StringBuffer().append("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);
        String str2 = new StringBuffer().append("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }
}

jdk1.6结果

false
false

jdk1.8结果

true
false

出现差异原因:String.intern()是一个Native方法,作用:jdk1.6及以前版本,如果字符串常量池已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象否则,将String对象包含的字符串添加到常量池中,并返回此String对象的引用,而new StringBuffer()对象存放在堆中,很明显返回false。jdk1.7后intern()方法不会再复制实例,只是在常量池中记录首次出现的实例引用,由于“java”之前就出现,不符合首次首先,所以返回false,“计算机软件”首次出现,则返回true

  1. 本机直接内存溢出
/**
* 本机直接内存溢出
*/
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);
}

}
}

异常信息

java.lang.OutOfMemoryError
    at sun.misc.Unsafe.allocateMemory(Native Method)
    at com.zwq.heap.DirectMemoryOOM.main(DirectMemoryOOM.java:18)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

附:文中流程图为原创,代码来源周志明《java虚拟机 Jvm高级特性和最佳实现》,代码结果在idea上验证过。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容