JVM运行时数据区域

1、运行时数据区域

 

程序计数器

记录正在执行的虚拟机字节码指令的地址(若是正在执行的是本地方法则为空)。java

Java 虚拟机栈

每一个 Java 方法在执行的同时会建立一个栈帧用于存储局部变量表操做数栈常量池引用等信息。 从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。 执行引擎所运行的全部字节码指令都只针对当前栈帧进行操做。程序员

 

操做数栈:
一个后进先出(Last-In-First-Out)的操做数栈,也能够称之为表达式栈(Expression Stack)。
操做数栈和局部变量表在访问方式上存在着较大差别,操做数栈并不是采用访问索引的方式来进行数据访问的,
而是**经过标准的入栈和出栈操做来完成一次数据访问**。
每个操做数栈都会拥有一个明确的栈深度用于存储数值,一个32bit的数值能够用一个单位的栈深度来存储,而2个单位的栈深度则能够保存一个64bit的数值,
固然操做数栈所需的容量大小在编译期就能够被彻底肯定下来,并保存在方法的Code属性中。

能够经过 -Xss 这个虚拟机参数来指定每一个线程的 Java 虚拟机栈内存大小:面试

java -Xss512M HackTheJava

该区域可能抛出如下异常:算法

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  • 栈进行动态扩展时若是没法申请到足够内存,会抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈与 Java 虚拟机栈相似,它们之间的区别只不过是本地方法栈为本地方法服务。缓存

本地方法通常是用其它语言(C、C++ 或汇编语言等)编写的,而且被编译为基于本机硬件和操做系统的程序,对待这些方法须要特别处理。安全

 

全部对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。并发

现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不一样类型的对象采起不一样的垃圾回收算法,能够将堆分红两块:函数

  • 新生代(Young Generation)
  • 老年代(Old Generation)

堆不须要连续内存,而且能够动态增长其内存,增长失败会抛出 OutOfMemoryError 异常。布局

能够经过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。性能

java -Xms1M -Xmx2M HackTheJava

 

方法区

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

和堆同样不须要连续的内存,而且能够动态扩展,动态扩展失败同样会抛出 OutOfMemoryError 异常。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,可是通常比较难实现。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。可是很难肯定永久代的大小,由于它受到不少因素影响,而且每次 Full GC 以后永久代的大小都会改变,因此常常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

运行时常量池

运行时常量池是方法区的一部分。

Class 文件中的常量池(编译器生成的各类字面量和符号引用)会在类加载后被放入这个区域。

除了在编译期生成的常量,还容许动态生成,例如 String 类的 intern()。

直接内存

在 JDK 1.4 中新加入了 NIO 类,它可使用 Native 函数库直接分配堆外内存(Native 堆),而后经过一个存储在 Java 堆里的 DirectByteBuffer 对象做为这块内存的引用进行操做。

这样能在一些场景中显著提升性能,由于避免了在 Java 堆和 Native 堆中来回复制数据

2、HotSpot虚拟机对象

对象的建立

对象的建立步骤:

 

  1. 类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用, 而且检查这个符号引用表明的类是否已被加载过、解析和初始化过。 若是没有,那必须先执行相应的类加载过程。

  1. 分配内存

在类加载检查经过后,接下来虚拟机将为新生对象分配内存。 对象所需的内存大小在类加载完成后即可肯定,为对象分配空间的任务等同于把一块肯定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定, 而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

  • 内存分配的两种方式
内存分配的两种方式 指针碰撞 空闲列表
适用场景 堆内存规整(即没有内存碎片)的状况 堆内存不规整的状况
原理 用过的内存所有整合到一边,没有用过的内存放在另外一边,中间有一个分界值指针,只须要向着没用过的内存方向将指针移动一段与对象大小相等的距离 虚拟机会维护一个列表,在该列表和总分记录哪些内存块是可用的,在分配的时候,找一块足够大的内存块划分给对象示例,而后更新列表记录
GC收集器 Serial ParNew CMS
  • 内存分配并发问题

在建立对象的时候有一个很重要的问题,就是线程安全,由于在实际开发过程当中,建立对象是很频繁的事情, 做为虚拟机来讲,必需要保证线程是安全的,一般来说,虚拟机采用两种方式来保证线程安全:

(1)CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是, 每次不加锁而是假设没有冲突而去完成某项操做, 若是由于冲突失败就重试,直到成功为止。 虚拟机采用CAS配上失败重试的方式保证更新操做的原子性。

(2)TLAB: 每个线程预先在Java堆中分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。 哪一个线程要分配内存,就在哪一个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才采用上述的CAS进行内存分配。

  1. 初始化零值

内存分配完成后,虚拟机须要将分配到的内存空间都初始化为零值(不包括对象头), 这一步操做保证了对象的实例字段在 Java 代码中能够不赋初始值就直接使用, 程序能访问到这些字段的数据类型所对应的零值。

  1. 设置对象头

初始化零值完成以后,虚拟机要对对象进行必要的设置, 例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不一样,如是否启用偏向锁等,对象头会有不一样的设置方式。

  1. 执行init方法

在上面工做都完成以后,从虚拟机的视角来看,一个新的对象已经产生了, 但从 Java 程序的视角来看,对象建立才刚开始,<init> 方法尚未执行,全部的字段都还为零。 因此通常来讲,执行 new 指令以后会接着执行 <init > 方法, 把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算彻底产生出来。

对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局能够分为3块区域:

(1)对象头

(2)实例数据

(3)对齐填充

  1. 对象头

Hotspot虚拟机的对象头包括两部分信息:

一部分用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志等等),

另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是那个类的实例

  1. 实例数据

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各类类型的字段内容。

  1. 对齐填充

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位做用。 由于Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍, 换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍), 所以,当对象实例数据部分没有对齐时,就须要经过对齐填充来补全。

对象的访问定位

创建对象就是为了使用对象,咱们的Java程序经过栈上的 reference 数据来操做堆上的具体对象。 对象的访问方式视虚拟机的实现而定,目前主流的访问方式有两种:

(1)使用句柄

(2)直接指针

  1. 使用句柄

若是使用句柄的话,那么Java堆中将会划分出一块内存来做为句柄池, reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息

 

  1. 直接指针

若是使用直接指针访问,那么Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息, 而reference中存储的直接就是对象的地址

 

这两种对象访问方式各有优点:

(1)使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址, 在对象被移动时只会改变句柄中的实例数据指针,而reference自己不须要修改

(2)使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

3、String类和常量池

  1. String对象的两种建立方式
String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false

这两种不一样的建立方法是有差异的:

第一种方式是在常量池中获取对象("abcd" 属于字符串字面量,所以编译时期会在常量池中建立一个字符串对象),

第二种方式一共会建立两个字符串对象(前提是 String Pool 中尚未 "abcd" 字符串对象)。

  • "abcd" 属于字符串字面量,所以编译时期会在常量池中建立一个字符串对象,指向这个 "abcd" 字符串字面量;

  • 使用 new 的方式会在堆中建立一个字符串对象。

 

  1. String类型的常量池比较特殊。它的主要使用方法有两种:
  • 直接使用双引号声明出来的String对象会直接存储在常量池中。

  • 若是不是用双引号声明的String对象,可使用 String 提供的 intern 方法。 String.intern() 是一个 Native 方法,它的做用是: 若是运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用; 若是没有,则在常量池中建立与此 String 内容相同的字符串,并返回常量池中建立的字符串的引用

String s1 = new String("计算机");
String s2 = s1.intern();
String s3 = "计算机";
System.out.println(s2);//计算机
System.out.println(s1 == s2);//false,由于一个是堆内存中的String对象一个是常量池中的String对象,
System.out.println(s2 == s3);//true,由于两个都是常量池中的String对象
  1. 字符串拼接:
String str1 = "str";
String str2 = "ing";
		  
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //TODO:在堆上建立的新的对象	  
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

 

注意:尽可能避免多个字符串拼接,由于这样会从新建立对象。 若是须要改变字符串的话,可使用 StringBuilder 或者 StringBuffer

面试题:String s1 = new String("abc");问建立了几个对象?

建立2个字符串对象(前提是 String Pool 中尚未 "abcd" 字符串对象)。

  • "abc" 属于字符串字面量,所以编译时期会在常量池中建立一个字符串对象,指向这个 "abcd" 字符串字面量;

  • 使用 new 的方式会在堆中建立一个字符串对象。

(字符串常量"abc"在编译期就已经肯定放入常量池,而 Java 堆上的"abc"是在运行期初始化阶段才肯定)。

String s1 = new String("abc");// 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 输出false
//由于一个是堆内存,一个是常量池的内存,故二者是不一样的。
System.out.println(s1.equals(s2));// 输出true

4、8种基本类型的包装类和常量池

  • Java基本类型的包装类的大部分都实现了常量池技术, 即Byte,Short,Integer,Long,Character,Boolean; 这5种包装类默认建立了数值**[-128,127]**的相应类型的缓存数据, 可是超出此范围仍然会去建立新的对象。

  • 两种浮点数类型的包装类Float,Double 并无实现常量池技术

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,若是在的话就直接返回缓存池的内容。

Integer的部分源码:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

在 Java 8 中,Integer 缓存池的大小默认为 -128~127。

static final int low = -128;
static final int high;
static final Integer cache[];

static {
    // high value may be configured by property
    int h = 127;
    String integerCacheHighPropValue =
        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
        try {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        } catch( NumberFormatException nfe) {
            // If the property cannot be parsed into an int, ignore it.
        }
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
        cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
}
  • 示例1:
Integer i1=40;
//Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);从而使用常量池中的对象。
Integer i2 = new Integer(40);
//建立新的对象。
System.out.println(i1==i2);//输出false
  • 示例2:Integer有自动拆装箱功能
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
  
System.out.println("i1=i2   " + (i1 == i2)); //输出 i1=i2  true
System.out.println("i1=i2+i3   " + (i1 == i2 + i3)); //输出 i1=i2+i3  true
//i2+i3获得40,比较的是数值
System.out.println("i1=i4   " + (i1 == i4)); //输出 i1=i4 false
System.out.println("i4=i5   " + (i4 == i5)); //输出 i4=i5 false
//i5+i6获得40,比较的是数值
System.out.println("i4=i5+i6   " + (i4 == i5 + i6)); //输出 i4=i5+i6 true
System.out.println("40=i5+i6   " + (40 == i5 + i6)); //输出 40=i5+i6 true

相关文章
相关标签/搜索