深入理解JVM 一内存

最近发现有些架构师竟然不懂JVM,我表示很吃惊,难道他工程师阶段打酱油了?那是不是说,直接去学习架构就行了 ?

接下来的一个礼拜,会把JVM核心内容做一个详细的总结。

JVM内存

这里写图片描述

其中 Heap、Method Area 是允许线程间共享访问的区域,其余部分只在独立线程独立分配。
在我们常用的jdk中使用了hotspotJVM,HotSpotJVM中对 Native Method Stack、VM Stack 没有区分,合并在一起,统称为 Stack理解即可。

program counter register

程序计数器是用来指示当前线程所执行的字节码的行号,JVM解释器就是通过使用这个计数器来选取下一条字节码指令。对于多线程,每个线程的执行实际上是获取cpu时间片(多核处理器使用每个核心并行执行一条指令),通过这个计数器保证下一次线程执行时,可以恢复到上次记录的位置继续执行,因此每个线程都有一个独立的程序计数器。

JIT vs byteCode解释器:
这里写图片描述

https://www.ibm.com/developerworks/cn/java/j-lo-just-in-time/

Stack(VM Stack+Native Method Stack)

Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、 操作数栈、 动态链接、 方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。也就是说,一个方法的执行开始与结束,对应这个这个栈帧的入栈与出栈

局部变量表存放了编译期可知的各种基本数据类型(boolean、 byte、 char、 short、 int、float、 long、 double)、 对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和
returnAddress类型(指向了一条字节码指令的地址)

Heap

所有对象、数组都在堆中创建(jdk7+该描述并不绝对)。
对象的结构:
这里写图片描述
对象头Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、 GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,类型指针,数组长度。
对象实例数据:对象真正存储的有效信息,引用类型、基本类型等等
对齐填充: 因为java中数据类型必须是byte的整数倍,通过“对齐填充”来保证。也就时这部分作为占位符的作用。
对象的创建:
1.确保常量池中存放的是已解释的类,如果没有则加载这个类类型
2.确保对象所属类型已经经过初始化阶段
3.分配内存:TLAB(Thread Local Allocation Buffer,TLAB) 或者通过CAS锁直接在eden中分配对象
4.如果需要,则为对象初始化零值否则 置初始值即可。
5.设置 对象头Header
6.如果有引用,将该对象引用放入栈中。
7.对象按照程序员的意愿进行初始化(构造器)

可以看到3,6,7,对象初始化内存后会接着分配引用,这时虽然拿到引用,但是对象初始化未完成。这个也就是double check(未使用volatile)会出现问题的原因。

在内存分配有两种方式:
1.直接移动指针划分需要的内存(通过CAS方式保证其他线程不会并行的使用该内存);
2 预先对每个线程划分一小块可使用的内存,这个线程中的对象初始化时则直接使用这一小块内存,直到预先分配的内存使用完,再去使用CAS锁去Heap中分配新的内存(-XX:+/-UseTLAB 设置)。

Heap是GC主要的管理区域,从GC的角度来看,Heap被细分为:youngGen、survivor0Gen、survivor1Gen、oldGen,GC划分的目的是为了更好地回收管理内存。

从内存分配的角度来看:Heap可能被划分为多个线程私有的分配缓冲区域(Thread Local Allocation Buffer,TLAB)。TLAB划分的目的是为了更快的分配内存。

Method Area

也被称为Non-Heap,用来区分Heap,尽管他们都是线程间共享的区域。主要存放JVM加载的class信息,如类名、类结构等类信息, 访问修饰符、 常量池、 字段描述、 方法描述等JIT编译后的代码等等。

因此对于频繁使用cglib等动态代理,会产生大量..$class,可能会导致该区域的OOM(OOM:Pengen),该区域在HotSpot等价于Pengen.
(http://www.pointsoftware.ch/en/under-the-hood-runtime-data-areas-javas-memory-model/)
(http://stackoverflow.com/questions/19340013/difference-between-class-area-and-heap)

Method Area—Runtime Constant Pool

Runtime Constant Pool作为Method Area的一部分,用于存放编译器生成的各种字面量以及符号引用(字面量和符号引用。字面量如文本字符串,java中声明为final的常量值等等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。),但是并不仅限于编译期,如String.intern()方法就可运行时加载到RuntimeConstantPool中。

举例来说:

public static void main(String[] args) {
        String s="cava";
            String str2 = new StringBuilder("ca").append("va").toString();
            System.out.println(str2.intern() == s);
            System.out.println(str2.intern() == new StringBuilder("ca").append("va").toString());

            System.out.println(str2.intern() == "cav"+"a");
    }

String对象调用intern会直接从Constant Pool中返回这个字符,如果常量池中没有,则会直接将这个String对象复制到常量池中(JDK6),或者直接将这个String的引用保存在常量池中(JDK7)。

而对于直接使用这个字符串,比如String s=”cava”;,这个字符串会直接从常量池中返回,如果这个常量池没有(首次出现),则会把这个常量的引用放入常量池。
注意,对于HotSpot虚拟机,根据官方发布的路线图信
息,现在也有放弃永久代并逐步改为采用Native Memory来实现方法区的规划了,在目前已
经发布的JDK 1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出。使用for loop 死循环调用 String.intern()并不能导致Pergen溢出。

Direct Memory

并不属于JVM内存规范中的部分,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。NIO中会使用DirectByteBuffer对DirectMemory进行操作。

/** *VM Args:-Xmx20M-XX:MaxDirectMemorySize=10M *@author zzm */
public class DirectMemoryOOM{
private static final int_1MB=1024*1024public static void main(String[]args)throws Exception{
Field unsafeField=Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe=(Unsafe)unsafeField.get(null);
whiletrue){
unsafe.allocateMemory(_1MB);
}}}
Exception in thread"main"java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at org.fenixsoft.oom.DMOOM.main(DMOOM.java20

由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显
的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就
可以考虑检查一下是不是这方面的原因。

参考资料:

深入理解JVM

http://java.jr-jr.com/2015/12/02/java-object-size/

相关文章
相关标签/搜索