JVM学习(一)
运行时数据区
线程共享内存区域
方法区
类信息、常量、静态变量、即时编译期后的代码
方法区
在JDK1.7之前方法区使用永久代实现,JDK1.8之后使用元空间实现。
元空间与永久代最大的区别是元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
堆内存
数组、对象实例(几乎所有对象,为什么是几乎?结尾说)
线程私有内存区域
程序计数器
记录当前线程正在执行的字节码指令(CLASS)的地址(行号)。
为什么需要程序计数器?因为Java是多线程,切换线程时需要记录线程所执行的位置,以便于切换回来时可以正常执行。
虚拟机栈
虚拟机栈存储当前线程所运行方法所需的数据,指令、返回地址。
Java 虚拟机栈是基于线程的。哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。
栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。
栈帧
每个方法在执行的同时都会创建一个栈帧。栈帧里包括动态连接、返回地址、操作数栈、局部变量表。
局部变量表
存储八大基本数据类型局部变量、对象引用。
操作数栈
代码的执行、操作指令在操作数栈里执行,执行完成后出栈,执行结果存储在部变量表里。
返回地址
方法的返回值返回到哪里的地址。
动态链接
因为Java中的多态特性,代码在编译的过程中无法确定执行哪段代码的。所以需要动态链接来确定具体执行哪段代码。
abstract class Person{
public abstract void wc();
}
private class Man extends Person{
@Override
public void wc() {
Log.v(TAG, "男厕");
}
}
private class Woman extends Person{
@Override
public void wc() {
Log.v(TAG, "女厕");
}
}
private void demo() {
//在编译时无法确定会执行哪个wc方法
Person man = new Man();
man.wc();
man = new Woman();
man.wc();
}
本地方法栈
保存native方法的信息,当JVM创建的线程调用native方法后,native方法不会在虚拟机栈中创建栈帧。而是通过动态链接直接调用native方法。
为什么JVM要使用-栈?
栈(Stack):先进后出,出口和入口是一个。符合JAVA中方法间的调用。调用方法是入栈,方法执行完成是出栈。
创建对象的过程
虚拟机创建对象,分配内存的过程。
1. 检查加载
执行相应的类加载过程。
2. 分配内存
接下来虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
指针碰撞
如果Java堆中内存是规整的,所有使用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为**“指针碰撞”**。
空闲列表
空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互错乱的,那就无法进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表。
分配方式选择
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
并发安全
除如何划分可用空间之外,还需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案。
CAS机制(比较和交换机制)
对分配内存空间的操作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
PS:CAS操作需要输入两个数值,一个旧值(操作前期望的值)和一个新值,在操作期间先比较旧值有没有发送变化,如果没有变化,才交换成新值,否则不进行交换。
举例:比如要存入一个值时,期望在没有存入前为空,存入后为B。在存入时会判断当前即将要存入的值是否为null,如果匹配成功才会存入B。
分配缓冲
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块私有内存,也就是本地线程分配缓冲。
如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用。
TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。
TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满,就新申请一个TLAB。
3. 内存空间初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为默认值(如int值为0,boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Ps:注意不是构造方法
4. 设置
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希值、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
5. 对象初始化
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,因为所有的字段都还为零值。所以,一般来说,执行new指令之后还会接着把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对齐填充。
对象头
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
对象所实例化的数据
对齐充填
对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。
对象的访问定位
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。
句柄
如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
直接指针
如果使用直接指针访问, reference中存储的直接就是对象地址。
谁优谁劣
两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
对Sun HotSpot而言,它是使用直接指针访问方式进行对象访问的。
虚拟机优化技术:逃逸分析
逃逸分析是虚拟机提供的一种优化技术。基本思想是,对于线程私有的对象,将它打散分配在栈上,而不分配在堆上。好处是对象跟着方法调用自行销毁,不需要进行垃圾回收,可以提高性能。注意,任何可以在多个线程之间共享的对象,一定都属于逃逸对象。
栈上分配
理论上讲,所有对象应该在堆上分配。但是运行过程中,发现对象并没有逃出该方法的作用范围,那么该对象就可以栈上分配。那么在栈上分配对象,就不需要垃圾回收。
同步省略
如果一个对象被发现只能从一个线程被访问到,那么这个对象的操作可以不考虑同步。
//sb对象就没有逃逸出方法test。
public String test() {
StringBuilder sb = new StringBuilder();
return sb.toString();
}
//sb对象逃逸出方法test。
public StringBuilder test() {
StringBuilder sb = new StringBuilder();
return sb;
}