JVM内存分配以及存储总结

最近看了一下JVM的内存分配,还是比较复杂的。这里做个总结,首先一个common sense就是操作系统会为每个java进程实例化一个jvm实例。jvm然后再来运行java程序,具体的过程就不多说了,简单来说就是核心classloader如bootstrap, extention, System对类的加载(一定是此顺序,jvm对类的加载采取的是代理委托方式,防止核心类被hack),找到对应的main入口来运行。

这里主要是想总结一下,每个java进程对应的jvm对内存的分配,运行时是什么样的。我们都知道jvm内存分为
1. PC计数器
2. 堆
3. 栈(jvm栈,本地方法栈)
4. 方法区(包括class信息,静态变量,class文件常量池,运行时常量池,JIT缓存代码)。

其中,PC计数器,栈是线程私有的,而堆和方法区是线程共享的。操作系统分配给jvm的内存的总大小是有限的,这和操作系统以及是32bit还是64bit,具体的jvm实现都有关系。另外,堆和方法区内存的大小是可以在jvm启动参数中配置的。我们程序员平时经常说的内存操作,OOM都是指的堆内存。

这里顺便说一下JVM的GC机制,JVM的GC不是采用的引用计数算法,而是可达性分析算法。堆根据GC回收的分代算法,又可以分为新生代Eden+2*Survivor, 和老年代。对新生代进行标记-复制算法,进行一次Minor GC,而对老年代进行标记-整理算法,进行一次Major/Full GC,当然肯定有一次Minor GC。程序中绝大部分new的对象放置在Eden区,少数比如大型对象,数组,和长期存活对象直接进入老年代。进入新生代的根据对象年龄计数器可以逐步晋升至老年代中。

那么对于不同的区域,什么时候存放什么东西都是有规范的。

总结如下:
1. 堆: 1) 类的成员变量引用,这条实际上是对2)的说明。 2)new且只有new出来的对象放在堆里。
2. 栈:运行时,成员函数的局部变量引用及其字面量。
3. 方法区:1) class 文件常量池,存放成员变量里的字面量,字符串常量。 2) 静态成员变量。 3)运行时常量池,存放成员函数运行时的常量。

现在举例说明:

class A{
    int i1 = 1; //i1在堆中A对象里,1在方法区的class文件常量池中
    String s1 = "abc"; //s1在堆中A对象里,"abc"在方法区的class文件常量池中
    String s2 = new String("abc"); //s2在堆中A对象里,"abc"在方法区的class文件常量池中,只有一份,new出来的String对象在堆里

    static int i2 = 2; //i2在方法区,2在方法区的class文件常量池中
    //s3在方法区,"xyz"在方法区的class文件常量池中
    static String s3 = "xyz"; 
    //s4在方法区,"xyz"在方法区的class文件常量池中,只有一份,new出来的String对象在堆里
    static String s4 = new String("xyz");

    public void func(){
        int i3 = 3; //i3在栈中,字面量3也在栈中
        int i4 = 3; //i4在栈中,字面量3已经存在,此时只有一份
        String s5 = "china"; //s5在栈中,"china"在方法区的运行时常量池中
        String s6 = new String("china"); //s6在栈中,"china"在方法区的运行时常量池中已经有一份相同拷贝,不再存, new出来的对象在堆中
    } 
}

那么,当A被classloader装载并且调用func函数的时候,其所在的jvm内存中不同的地方都有哪些关于A的信息呢?分析如下:

  1. 首先jvm第一次碰到A时,比如new A()时,会查看方法区里是否已经存放过关于此类的信息,如果没有,则先调用classloader(还是按照那个Bootstrap, extention…的顺序),最后装载A,此时,方法区里就有关于A类的Class信息了,并且由于在编译期间就能确定成员变量所引用的常量,因此,此时class文件常量池也会有信息,i1所引用的1,s1所引用的”abc”, i2所引用的2,s3所引用的”xyz”。
  2. new A()紧接着会导致在堆中分配关于A的内存,其中包括成员变量i1, s1, s2。其实s2所引用的new String(“abc”)中”abc”也是编译期间就能够确定,因此这里的”abc”也会存在class文件常量池,于是会先去class文件常量池找是否已经有一份相同的,由于之前已经有一份,于是不再存第二份。而new出来的String(“abc”)对象会在堆中有一份。
  3. 由于i2, s3, s4都是静态变量,因此它们存在方法区,2和”xyz”存在class文件常量池,注意”xyz”也只有一份,道理同2。另外s4引用的new对象也会在堆中有一份。
  4. 当程序运行调用A的func函数时,此时,jvm栈就开始工作了,会有一个关于func函数的栈帧(statck Frame),入栈,里面有i3, i4变量引用和常量或者说字面量3,注意3此时在栈中只有一份!如果后期i4被赋值为4,则栈会开辟新的空间存一个4,i3不变仍然为3。s5,s6也在栈中,”china”由于是在运行时才确定,因此存放在方法区的运行时常量池中,s6所引用的new的String(“china”)中的“china”也只在运行时常量池中保存一份,另外new会在堆中开辟一个新的对象空间存放此对象。

因此,对于equals相等的字符串,在常量池(class文件常量池或者运行时常量池)中永远只有一份,在堆中有多份。因为String类重写/覆盖了Object类的equals方法,只要字符串内容相等即为true,而Object是必须同一个对象的引用地址才为true。但是String并没有重写/覆盖==操作符,所以String对象的==还是只有同一个对象的地址引用才为true。

并且,延伸出来很多面试题的答案,比如:

1) String s = new String(“xyz”); 产生几个对象?

一个或两个。如果常量池中原来没有 ”xyz”, 就是两个。如果原来的常量池中存在“xyz”时,就是一个。

2) String作为一个对象来使用

例子一:对象不同,内容相同,”==”返回false,equals返回true

String s1 = new String(“java”);
String s2 = new String(“java”);

System.out.println(s1==s2); //false
System.out.println(s1.equals(s2)); //true

例子二:同一对象,”==”和equals结果相同

String s1 = new String(“java”);
String s2 = s1;

System.out.println(s1==s2); //true
System.out.println(s1.equals(s2)); //true
String作为一个基本类型来使用

如果值不相同,对象就不相同,所以”==” 和equals结果一样

String s1 = “java”;
String s2 = “java”;

System.out.println(s1==s2); //true
System.out.println(s1.equals(s2)); //true
如果String缓冲池内不存在与其指定值相同的String对象,那么此时虚拟机将为此创建新的String对象,并存放在String缓冲池内。

如果String缓冲池内存在与其指定值相同的String对象,那么此时虚拟机将不为此创建新的String对象,而直接返回已存在的String对象的引用。

你可能感兴趣的:(java技术)