您的位置:首页 > 其它

JVM内核—JVM内存模型

2016-05-09 21:25 363 查看
目录:

1. OOM和Stack Over Flow

1.1 Stack Over Flow

1.2 Java Heap Space

2. JVM内存模型

2.1方法区

2.1.1字符串常量池

2.1.2基础数据类型包装类常量池

2.1.2.1 Integer常量池原理

2.2栈区

2.3堆区

2.4本地方法栈

2.5 JVM程序计数器

本季内容一共分为3个课时:JVM内存模型,JVM垃圾回收,JVM类加载机制。这三部分内容是JVM的核心。

1.OOM和Stack Over Flow

1.1Stack Over Flow

public class TestJava {
public static void main(String[] args) {
invoke();
}

public static void invoke(){
invoke();
}
}






1.2Java heap space

import java.util.ArrayList;
import java.util.List;

public class TestJava {
public static void main(String[] args) {
List<UserBean> l = new ArrayList<UserBean>();
for(int i=0;;i++){
System.out.println(i);
l.add(new UserBean());
}
}
}

class UserBean {
String name;
int age;
}






2.JVM内存模型



JVM内存一般划分为以下五个区域:方法区(Method Area),栈区(Stack),堆区(Heap),本地方法栈(Native Method Stack),程序计数器(program counter register)。
方法区和堆区为所有线程所共享,栈区、本地方法栈以及程序计数器为各个线程私有。

2.1方法区

JVM首先通过类的全限定类名获取此类的class文件字节流,然后将此字节流所代表的静态存储结构转化为方法区中的运行时数据结构,即class对象,作为该类的元数据访问入口。
为static变量和final常量在方法区分配存储空间,基础数据类型变量直接存储值,引用类型变量则存储对象地址。
一般来说,方法区等同于永久代区,但实际上,二者并不等同。
JDK8使用本地内存来存储类的元数据信息(Metaspace),也就是说方法区由本地内存所代替,JVM参数MaxMetaspaceSize用于限制所分配的本地内存的大小,若未制定,元数据空间会动态拓展。若使用量达到MaxMetaspaceSize所设定的值时,则触发GC。

2.1.1字符串常量池

public class TestJava {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "Hello";
String s3 = "He" + "llo";
String s4 = "He" + new String("llo");
String s5 = new String("Hello");//该句创建了两个对象,在常量池创建"Hello"字符串(若已存在则不会重复创建),使用"Hello"字符串的一份深度拷贝在堆区创建String对象
String s6 = new String("Hello").intern();
String s7 = "He";
String s8 = "llo";
String s9 = s7 + s8;

System.out.println(s1==s2);//true,“Hello”在编译期会直接写死入class文件的常量池,运行时载入内存中的方法区中的常量池,s1和s2均指向此地址,实现了复用。
System.out.println(s1==s3);//true,因为参与拼接的都是已知的字面常量,编译期间直接优化为"Hello"写入class文件的常量池,所以s3也指向方法区中的“Hello”
System.out.println(s1==s4);//false,因为new String("llo")的地址需要运行时在堆中分配,无法编译优化。实际上s4指向堆中的一个全新的String对象
System.out.println(s1==s6);//true,intern方法会首先查找常量池中是否存在“Hello”(通过调用equals方法判断),有则直接返回其在常量池中的地址,否则将“Hello”字符串添加进常量池,并返回地址
System.out.println(s1==s9);//false,因为需要在运行时才能确定 s7和s8所指向的常量池字符串,所以无法进行编译优化。实际上s9指向堆中的一个全新的String对象
System.out.println(s4==s5);//false,s4和s5分别指向两个完全不同的堆中地址
}

下面将带领大家一起看一下class文件中的常量池区域。
package com.baidu;

public class Test{
String str = "Hello";
}
编译该类,并使用winhex工具打开Test.class文件:



字符串常量以01开头(1个字节),接着用两个字节记录字符串长度(00 05),然后就是字符串的实际内容(ASC码值,48 65 6C 6C 6F)。
若想了解String类的intern方法及常量池的更详细信息,请看深入解析String#intern

2.1.2基础数据类型包装类常量池

package com.baidu;

public class TestJava {
public static void main(String[] args){
Integer i1 = new Integer(1);
Integer i2 = 1;
Integer i3 = 1;
int i4 = 1;
Integer i5 = 128;
Integer i6 = 128;
Double d1 = 1.0;
Double d2 = 1.0;

System.out.println(i1==i2);//false,i1指向堆区对象,i2指向常量池的对象
System.out.println(i2==i3);//true,享元模式,i2和i3指向常量池中的同一对象
System.out.println(i1==i4);//true,i1对象自动拆箱为基础数据类型的比较
System.out.println(i2==i4);//true,同上
System.out.println(i5==i6);//false,-128到127之间的整数才从常量池获取
System.out.println(d1==d2);//false,Double和Float类型并没有实现常量池
}
}


八种基础数据类型的包装类(Byte、Short、Integer、Long、Character、Boolean)都实现了常量池技术,Float和Double则没有实现。
Byte、Short、Integer、Long、Character也只有在对应值在-128~127区间内才使用常量池技术。

2.1.2.1 Integer常量池原理

当执行Integer i = 1;时,其实内部调用了valueOf方法。
Integer类的常量池技术其实是通过静态数组实现的,如下为JDK中Integer类的常量池实现源码:

public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
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;
}

private IntegerCache() {}
}

2.栈区



JVM是进程,进程是静态的概念,实际运行的是JVM内部的一个个线程。而栈区与线程在同一时间被创建。
栈区中存储的是一个个栈帧,每个方法执行时都会创建一个栈帧来存储方法的局部变量表、操作数栈、动态链接方法、方法的返回值以及方法的返回地址。
一般来说,栈区的大小是固定的(可以通过-Xss参数设置每个线程的栈区大小),导致栈的深度是固定的,一旦超出,则抛出StackOverFlowError。

3.堆区

堆区是JVM管理的最大的一块内存空间,主要用于存放类的实例对象。
堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。

默认情况下每个区的大小以及所占比例如下图所示:



有关堆区垃圾回收等更进一步的讨论,我们将放在下节介绍。

4.本地方法栈

本地方法栈和JVM栈区结构和功能相似,但管理的不是Java方法,而是本地方法,本地方法是用C语言实现的。

5.JVM程序计数器

JVM内部的每个线程都有一个独立的程序计数器,记录下一条要执行的指令的地址。
如果执行的是JAVA指令,计数器记录正在执行的java指令的下一条指令的地址;
如果执行的是native方法,则计数器为空。

结语:本节详细介绍了JVM内存模型,下节将带领大家一起学习JVM GC 机制。



更多大数据相关技术和教程,请扫描以上二维码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: