JVM(一)-内存区域
1.运行时数据区域
(1)程序计数器
线程私有,记录当前线程执行到哪里了,分支、循环、异常、线程切换等基础功能都依赖它。
内存小,无OOM。
(2)Java虚拟机栈
线程私有,生命周期与线程相同。为Java方法服务,描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口 等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出 栈的过程。
(3)本地方法栈
线程私有,生命周期与线程相同。为Native方法服务。Native方法就是所谓的本地方法,用关键字native修饰,像接口一样定义,用其他语言实现,可以返回任何Java类型;使用的时候对其他类没什么影响,其他类甚至都不知道它们调用的Native方法;可以被继承然后用Java语言写。作用:与java环境外交互,与操作系统交互。
(4)Java堆
所有线程共享,存实例对象,是垃圾收集器管理的主要区域,所以也叫GC堆。可以处于物理不连续但逻辑连续的内存空间。
(5)方法区
所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。除了和Java堆一样不需要连续的内存和可以 选择固定大小或者可扩展外,还可以选择不实现垃圾收集。
(6)运行时常量池
是方法区的一部分。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。编译、运行时都可以产生,例如String类的intern()方法。
String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等 于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包 含的字符串添加到常量池中,并且返回此String对象的引用。有就引用,没有就新建并加到池中。
(7)直接内存
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓 冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储 在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著 提高性能,因为避免了在Java堆和Native堆中来回复制数据。
2.HotSpot虚拟机对象探秘
(1)对象的创建
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一 个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没 有,那必须先执行相应的类加载过程。
如何在堆中划分内存?
- 绝对规整,用过的放一边,没用过的放一边。
- 不规则,维护一个散列表指向具体内存地址。
线程安全方面怎么办?创建对象很频繁,高并发不是线程安全的,例如正在给A分内存,指针没来及修改就被B拿去分内存。解决方案有2种。
- CAS
- 每个线程自己预分配一块内存(Thread Local Allocation Buffer,TLAB),各自线程在本地缓冲分配区中进行,用完后上锁重新分配内存。
内存分配后,默认值都是0(不包括对象头)。接下来就是对对象进行必要设置,信息存在对象头里。然后就是初始化,执行<init>方法,为变量赋予我们想赋予的值。
(2)对象的内存布局(对象头+实例数据+对齐填充)
对象头=MarkWord+类型指针。前者用于存储自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,如果是数组对象,还有一块用于记录数组长度的数据,所以大小不定。后者是指向它的类元数据的指针,确定这个对象是哪个类的实例。
(3)对象的访问定位
需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有「使用句柄」和「直接指针」两种。