JAVA SE 基础复习-虚拟机

一、Java内存模型

                                                           

  内存主要被分为以下四个部分:

1、PC计数器

  保存虚拟机正在执行的字节码的地址。字节码解释器在运行时就是通过改变PC寄存器来选取下一条需要执行的字节码指令,分支,循环,抛出异常,返回等需要依赖计数器完成。

2、栈

  栈分为Java栈(虚拟机栈)和本地方法栈,其中本地栈和Java栈的功能类似,只是执行的是非JAVA方法。

  对java栈简单的理解就是存储栈帧,在线程运行时创建,以栈帧为单位保存线程状态。存放局部变量中的八种基本类型和对象引用

  每当启用一个线程时,JVM就为他分配一个Java栈,栈是以帧为单位保存当前线程的运行状态。

  栈帧是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间分配在 Java 虚拟机栈之中,每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用。局部变量表和操作数栈的容量是在编译期确定,并通过方法的 Code 属性保存及提供给栈帧使用。因此,栈帧容量的大小仅仅取决于 Java 虚拟机的实现和方法调用时可被分配的内存。

  栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一条线程的栈帧。

  某个线程正在执行的方法称为当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。当线程执行一个方法时,它会跟踪当前常量池。每当线程调用一个Java方法时,JVM就会在该线程对应的栈中压入一个帧,这个帧自然就成了当前帧。当执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等等

  Java栈上的所有数据都是私有的。任何线程都不能访问另一个线程的栈数据.

  方法中的引用类型实例的引用保存在栈中,实例对象保存在堆中。

  比如

  public void helloworld()

  {

     Test test=new Test();

  }

  调用这个方法,JVM就会创建一个栈帧用于保存局部变量,压入Java 栈,

  第一句创建了一个对象,这个对象是存储在堆中,而test这个引用会存储在栈中,这个test是该线程私有的。

  假如再将一个成员变量test2指向这个新建的对象,那么这个对象在函数执行完成后就不会因为方法结束栈帧销毁而销毁,这也可以说明堆区是各个线程共享的内存区域。

3、堆

  供动态内存分配,是JAVA虚拟机管理内存中最大的一块,也是被各个线程共享的内存区域。所有new出来的对象和数组对象都保存在堆区。Heap在32位的操作系统上最大为2G,在64位的操作系统上则没有限制,其大小通过-Xms和-Xmx来控制,-Xms为JVM启动时申请的最小Heap内存,默认为物理内存的1/64但小于1G,-Xmx为JVM可申请的最大Heap内存,默认为物理内存的1/4,默认当空余堆内存小于40%时,JVM会增大Heap的大小到-Xmx指定的大小。数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要原因。

4、方法区

  方法区与传统语言中的编译代码储存区( Storage Area Of Compiled Code)或者操作系统进程的正文段( Text Segment)的作用非常类似,它存储了每一个类的结构信息,例如运行时常量池( Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容(所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息),还包括一些在类、实例、接口初始化时用到的特殊方法。

  这里的字段可以理解为静态变量的类型。比如类A有一个静态变量 static  String c="C";那么这个字段里面就保存着这个变量的数据类型,引用变量c和值"C"都保存在常量池中

  每个类都有对应的方法区,在类被加载的时候创建。是被各个线程共享的内存区域。方法区是堆的逻辑组成部分,但是简单的虚拟机不会对该区域进行内存回收。

  它在虚拟机启动时在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代,默认为64M,可通过-XX:PermSize以及-XX:MaxPermSize来指定其大小。

  运行时常量池( Runtime Constant Pool)是每一个类或接口的常量池( Constant_Pool )的运行时表示形式,是方法区的一部分。

  虽然每个class类文件中都有一个constant pool,但是被加载到内存中,所有类共享一块运行时常量池,而并非每个类维护一个常量池

  可以用字符串证明,比如类A有一个静态字符串变量a="abc",类B也有一个静态字符串变量b="abc"   ,此时A.a==B.b   true。

  常量池保存的是字符串的字面量  比如  "abc"。

  另外很多文章说类A的Integer a=100,类B的Integer b=100   A.a==B.b这也是因为常量池,

  个人理解这个不是虚拟机在内存上的管理,而是代码级的实现,

  在Byte,Character,Short,Integer,Long,这五个包装类里面包含一个Cache  包含[-128,127]之间的数值 ,Integer a=10;这句话会编译成 Integer a=Integer.valueOf(10);其实是一个装箱的过程,假如Cache中包含就直接返回,所以上面的就会相等,假如a=200,b=200,那么结果就不同了

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

  而Double和Float并没有这个Cache。

    public static Double valueOf(String s) throws NumberFormatException {
        return new Double(FloatingDecimal.readJavaFormatString(s).doubleValue());
    }

  它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。静态变量的引用或者字面量保存在常量池中。比如字符串,基本类型的值,还有引用。比如 String abc="abc";在编译器间就会将"abc"这个字符串放进class文件的常量池。


二、堆与栈的区别与联系

 1、栈的存取速度比堆块仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

2、栈中的数据在线程中是共享的。比如,int  a=3;int  b=3;这句话会先创建一个值为3的int类型引用,然后查找有没有有没有字面值为3的地址,没有加开辟一块内存存放3,并将这块内存地址指向a;创建b的引用,发现已经有了字面值为3的内存,就直接将该引用指向这块内存。

 

三、实例

1、新建对象保存在堆区,而常量池在方法区

    public static void main(String[] args) {
        List<String> list=new ArrayList<>();
        while(true)
        {
            list.add(new String("asdasdadasd"));
        }
    }

抛出异常:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

   new出来的对象都是在保存在堆区,这里还有个问题是对new String("abc")  这句话创建了几个对象?

   答案是1个或者2个,首先"abc"本身就是一个字面量,假如常量池中没有这个字面量,就会在其中创建一个字面量对象,然后再堆中也创建一个;假如常量池中有这个对象,那么"abc"直接引用常量池的这个对象,并在堆中创建一个对象,这时之创建了一个对象。

  由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。前面提到JDK 1.7开始逐步“去永久代”的事情,在此就以测试代码观察一下这件事对程序的实际影响。

  String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

  在JDK 1.6及之前的版本中,由于常量池分配在永久代内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量,代码如下所示。

public static void main(String[] args) {  
// 使用List保持着常量池引用,避免Full GC回收常量池行为  
List<String> list = new ArrayList<String>();  
// 10MB的PermSize在integer范围内足够产生OOM了  
int i = 0;   
while (true) {  
list.add(String.valueOf(i++).intern());  
}  
}

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

  从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。

  而使用JDK 1.7运行这段程序就不会得到相同的结果,while循环将一直进行下去。关于这个字符串常量池的实现问题,还可以引申出一个更有意思的影响,代码如下:

    public static void main(String[] args) {  
         String str1 = new StringBuilder("计算机").append("软件").toString();  
         System.out.println(str1.intern() == str1);  
         
         String str2 = new StringBuilder("ja").append("va").toString();  
         System.out.println(str2.intern() == str2); 
         
        String s1=new String("kail");        
        System.out.println( s1==s1.intern() );
        
        String s2=new String(new char[]{'x','i','a','o'});
        System.out.println(s2==s2.intern());
    } 

  输出

true
false
false
true

  这段代码在JDK 1.6中运行,会得到四个false,而在JDK 1.7中运行,会得到上面的结果。产生差异的原因是:在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。而JDK 1.7(以及部分其他虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。

2、各个变量保存的区域 

public class Main{
    int ig1=5;
    String str1="abc";
    public static void main(String[] args) {
        int i1=5; 
        String str2="abc";
        Main main=new Main();
        main.start();
        System.out.println(main.str1==str2);
    } 
    public void start()
    {
        new Thread(new TestThread()).start();
    }
}
class TestThread implements Runnable
{

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println("hello world");
    }
    
}

输出

true
hello world

首先看Main的成员变量 ig1保存在堆中,字面量"abc"保存在常量池中,引用变量保存在堆中,

  调用main方法,在主线程栈A中创建一个栈帧,局部变量i1保存在栈中,"abc"在常量池中已经存在,就直接引用,引用变量保存在栈中。

  又调用start()方法,则将main()方法压栈,创建新栈帧,这个方法里面创建了一个新的线程,那么虚拟机会创建一个新的java栈B

  当线程A获得执行时间时,就会打印结果true,线程B获得执行时间时,打印结果hello world

参考:http://www.cnblogs.com/dingyingsi/p/3760447.html

    http://developer.51cto.com/art/201010/229836.htm

    http://www.cnblogs.com/dolphin0520/p/3780005.html

    http://blog.csdn.net/sells2012/article/details/18656263

        http://blog.csdn.net/raintungli/article/details/38595573

原文地址:https://www.cnblogs.com/maydow/p/4584361.html