java六大存储区域

java六大存储区域

参考资料:https://blog.csdn.net/qq_28009065/article/details/79087831

存储速度:

寄存器(register)

这是最快的存储区,因为它位于不同于其他存储区的地方——处理器内部。但是寄存器的数量极其有限,所以寄存器由编译器根据需求进行分配。java中,你不能直接控制,也不能在程序中感觉到寄存器存在的任何迹象。 C语言可以定义寄存器变量。

栈(stack)

存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中。

位于通用RAM(random-access memory )中,但通过它的“堆栈指针”可以从处理器哪里获得支持。堆栈指针若向下移动,入栈操作,则在栈顶分配新的内存;若向上移动,出栈操作,则在栈顶释放那些内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时候,JAVA编译器必须知道存储在堆栈内所有数据的确切大小和生命周期,因为它必须生成相应的代码,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些JAVA数据存储在堆栈中——特别是对象引用,但是JAVA对象不存储其中。

{
    int c=6; //at line1
    c++;//at line2
}
c++;//at line3

这是Java的普通代码块。编译器先在编译时在第一行的时候就在堆栈中分配一个储存数据为6的对象,当程序运行完这个花括后,存储为6的地址便会被销毁,于是第三行就会发生编译错误。所以这也为什么堆栈中的数据不能被多个线程共享的原因。

堆(heap)

存放所有new出来的对象。

一种通用性的内存池(也存在于RAM中(random-access memory )),用于存放所有的JAVA对象堆不同于栈的好处是:编译器不需要知道要从堆里分配多少存储区域,也不必知道存储的数据在堆里存活多长时间。因此,在堆里分配存储有很大的灵活性。当你需要创建一个对象的时候,只需要new写一行简单的代码,当执行这行代码时,会自动在堆里进行存储分配。当然,为这种灵活性必须要付出相应的代码。用堆进行存储分配比用堆栈进行存储存储需要更多的时间

堆内存和栈内存确实是我们常常用的东西,比如 Animal a = new Animal(); 这个时候相当于在堆内存中开辟了一个空间保存了Animal的信息以及着块空间的内存地址,然后在栈内存中划了一小快空间保存了堆中的内存地址,这个时候我们就可以说引用a指向Animal()了. 可是有时候,有个静态类.Animal,里面有个静态方法speak(); 那么可以这么直接调用Animal.sepak(); 这个时候既没有new,也没有Animal a=??; 所以既没有在堆中开辟空间也没有在栈内存中开辟空间 , 可是方法确实能执行,一切程序都运行在内存里,那么证明有新的内存区,就是静态空间了.

1. 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。
2. 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第3点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢 。

静态存储区(static storage)

又叫方法区:包含的都是在整个程序中永远唯一的元素,如class,static变量。

static修饰的成员变量存放在静态存储区中,但java对象本身不存放在静态存储区。

这里的“静态”是指“在固定的位置”。静态存储里存放程序运行时一直存在的数据。你可用关键字static来标识一个对象的特定元素是静态的,但JAVA对象本身从来不会存放在静态存储区里

值得深思的是,static能修饰成员变量,而不能修饰方法中的临时变量,所以static final修饰的是一个成员变量,这个变量放在常量存储区中。而仅用static修饰成员变量时放在静态存储区中。

常量存储区(constant storage)

常量值通常直接存放在程序代码内部,这样做是安全的,因为它们永远不会被改变。有时,在嵌入式系统中,常量本身会和其他部分分割离开,所以在这种情况下,可以选择将其放在ROM(read only memory)中

声明为final static的为常量,可以保存在常量储存区,还有String类型的对象都是常量,系统维护了一个String常量池。 String类型是final修饰的,无法被继承。

String s = new String("Hello world!");

上面这行代码一共创建了几个对象?答案是不确定的,因为不知道常量池中存不存在Hello world!字符常量。如果存在,则只创建一个String类型的对象在堆上,如果不存在则在堆上创建一个String对象,并在常量池创建一个字符常量Hello world!对象。需要注意的是,常量池在java类加载的过程中会有很多字符常量被创建,需要比对的字符常量可能已经存在,只是你不知道而已。

非RAM存储区

若数据完全活在程序外部,不依赖于程序时,它可以不受程序的任何控制,即使程序未运行依然可以存在,比如流对象和持久化对象。对于流对象来说,对象转化为字节流,发送给另一台机器;对于持久化对象来说,通常存放于磁盘上,即使程序终止,仍可以保持自己的状态。此存储方式的技巧在于:把对象转化为可以存放在任何其它媒介的事物上,在需要时,恢复为常规的、基于RAM的对象。 

非RAM存储器,主要就是磁带,磁盘等等。

static关键字

资料:Java中的static关键字解析

static表示‘唯一’这种特性,这种唯一体现在,修饰了成员变量或者成员方法,则在所有实例中唯一。

从存储区域来看,static可以修饰方法或者成员变量,如果仅用static修饰的成员变量,是存放在静态存储区中的,如果是static final修饰的成员变量,其实是一个唯一不变的常量,存放在常量存储区中。所以final强调‘不变’这一特性。

static方法

thinking in java中有一句话:“static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。”

简而言之就是:方便在没有创建对象的情况下来进行调用(方法/变量)

static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用

关于构造器是否是static方法可参考:http://blog.csdn.net/qq_17864929/article/details/48006835

构造方法不是静态方法,但是静态的。个人理解在静态方法中this是找不到实例的,但是在构造方法中可以使用this找到当前对象,而在没有实例化之前可以调用的方法是静态方法,所以构造方法是静态的。

static变量

static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

static成员变量的初始化顺序按照定义的顺序进行初始化。

static代码块

static关键字还有一个比较关键的作用就是,用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。

为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。下面看个例子:

class Person {
    private Date birthDate;

    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }

    boolean isBornBetween() {
        Date startDate = Date.valueOf("1946");
        Date endDate = Date.valueOf("1964");
        return birthDate.compareTo(startDate) >= 0
                && birthDate.compareTo(endDate) < 0;
    }
}

isBornBetween方法判断是否在1946-1964年出生,这样每次调用该方法都会创建两个Date对象,startDate和endDate,造成控件浪费。抓住判断出生的两个年份是固定的,是静态的,所以可以在类加载时初始化该固定部分一次,以后就不用初始化了,于是可以这样写:

class Person {
    private Date birthDate;
    private static Date startDate, endDate;

    static{
        startDate = Date.valueOf("1946-02-02");
        endDate = Date.valueOf("1964-02-02");
    }

    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }

    boolean isBornBetween() {
        return birthDate.compareTo(startDate) >= 0
                && birthDate.compareTo(endDate) < 0;
    }
}

public class TestStatic {
    public static void main(String[] args) {
        Person person = new Person(Date.valueOf("1948-02-02"));
        System.out.println(person.isBornBetween());
    }
}

抓住了判断年份的这两个对象是固定的这一特性,在类加载时仅初始化这两个对象一次。此外需要注意:static是不允许用来修饰局部变量

static修饰类

参考资料:static关键字修饰类静态内部类何时初始化java 内部类和静态内部类的区别

首先搞清楚一个问题:类何时被加载?答案是:静态内部类和普通类一样,在使用的时候才会被加载。在加载的过程中,如果发现有静态代码,先执行静态代码,且仅执行一次,下一次使用该类,如实例化一个对象,也不会再执行静态代码。静态内部类的单例就利用的这种原理。

java里面static一般用来修饰成员变量或函数。但有一种特殊用法是用static修饰内部类,普通类是不允许声明为静态的,只有内部类才可以。

被static修饰的内部类可以直接作为一个普通类来使用,而不需实例一个外部类,代码如下:

public class OutClassTest {
    static int a;

    int b;

    public static void test() {
        System.out.println("outer class static function");
    }

    public static void main(String[] args) {
        OutClassTest oc = new OutClassTest();
        // new一个外部类
        OutClassTest oc1 = new OutClassTest();
        // 通过外部类的对象new一个非静态的内部类
        OutClassTest.InnerClass no_static_inner = oc1.new InnerClass();
        // 调用非静态内部类的方法
        System.out.println(no_static_inner.getKey());

        // 调用静态内部类的静态变量
        System.out.println(OutClassTest.InnerStaticClass.static_value);
        // 不依赖于外部类实例,直接实例化内部静态类
        OutClassTest.InnerStaticClass inner = new OutClassTest.InnerStaticClass();
        // 调用静态内部类的非静态方法
        System.out.println(inner.getValue());
        // 调用内部静态类的静态方法
        System.out.println(OutClassTest.InnerStaticClass.getMessage());
    }

    private class InnerClass {
        // 只有在静态内部类中才能够声明或定义静态成员
        // private static String tt = "0";
        private int flag = 0;

        public InnerClass() {
            // 三.非静态内部类的非静态成员可以访问外部类的非静态变量和静态变量
            System.out.println("InnerClass create a:" + a);
            System.out.println("InnerClass create b:" + b);
            System.out.println("InnerClass create flag:" + flag);
            //
            System.out.println("InnerClass call outer static function");
            // 调用外部类的静态方法
            test();
        }

        public  String getKey() {
            return "no-static-inner";
        }
    }

    private static class InnerStaticClass {
        // 静态内部类可以有静态成员,而非静态内部类则不能有静态成员。
        private static String static_value = "0";

        private int flag = 0;

        public InnerStaticClass() {
            System.out.println("InnerClass create a:" + a);
            // 静态内部类不能够访问外部类的非静态成员
            // System.out.println("InnerClass create b:" + b);
            System.out.println("InnerStaticClass flag is " + flag);
            System.out.println("InnerStaticClass tt is " + static_value);
        }

        public int getValue() {
            // 静态内部类访问外部类的静态方法
            test();
            return 1;
        }

        public static String getMessage() {
            return "static-inner";
        }
    }

    public OutClassTest() {
        // new一个非静态的内部类
        InnerClass ic = new InnerClass();
        System.out.println("OuterClass create");
    }

}

/**
 * 总结: 
 * 1.静态内部类可以有静态成员(方法,属性),而非静态内部类则不能有静态成员(方法,属性)。
 * 2.静态内部类只能够访问外部类的静态成员,而非静态内部类则可以访问外部类的所有成员(方法,属性)。
 * 3.实例化一个非静态的内部类的方法:
 *  a.先生成一个外部类对象实例
 *  OutClassTest oc1 = new OutClassTest();
 *  b.通过外部类的对象实例生成内部类对象
 *  OutClassTest.InnerClass no_static_inner = oc1.new InnerClass();
 *  4.实例化一个静态内部类的方法:
 *  a.不依赖于外部类的实例,直接实例化内部类对象
 *  OutClassTest.InnerStaticClass inner = new OutClassTest.InnerStaticClass();
 *  b.调用内部静态类的方法或静态变量,通过类名直接调用
 *  OutClassTest.InnerStaticClass.static_value
 *  OutClassTest.InnerStaticClass.getMessage()
 */

常见面试题

初始化顺序总结 - 静态变量、静态代码块、成员变量、构造函数

子类继承父类时的初始化顺序:

1.首先初始化父类的static变量和块,它们的级别是相同的,按照代码中出现的顺序初始化

2.初始化子类的static变量和块,它们的级别是相同的,按照代码中出现的顺序初始化

3.初始化父类的普通成员变量,调用父类的构造函数

4.初始化子类的普通成员变量,调用子类的构造函数

看题如下:

package test_static;

/**
 * 装载Test类的时候,先判断有无父类,没有父类。
 * 在装载时,先初始化,且只初始化一次静态代码。
 * 静态代码包括静态成员变量和静态代码块,按代码顺序初始化。
 * 然后初始化普通成员变量紧接着构造方法。
 * 这里需要开始装载Person1类,先静态代码块
 * 装载Person1类后,先初始化Person1类的普通成员变量,然后构造方法
 * @author Administrator
 *
 */
public class Test {
    Person1 person = new Person1("Test");//3
    static{
        System.out.println("test static");//1打印test static
    }

    public Test() {
        System.out.println("test constructor");//6打印test constructor
    }

    public static void main(String[] args) {
        //程序入口
        //MyClass类加载
        new MyClass();
    }
}

class Person1{
    static{
        System.out.println("person static");//4打印person static
    }
    public Person1(String str) {
        System.out.println("person "+str);//5打印person Test,7打印person MyClass
    }
}

/**
 * MyClass类加载的过程中
 * 是否有继承自父类:
 * 如果继承自父类,先装载父类,于是先装载Test
 * 父类装载完之后,装载子类,先初始化静态代码。
 * 父类和子类的静态代码都初始完成后,初始化父类普通成员变量,紧接着构造方法。
 * 然后初始化子类普通成员变量和构造方法,这里又装载一个Person1类
 * 由于Person1类已经装载过了,所以不会走静态方法
 * @author Administrator
 *
 */
class MyClass extends Test {
    Person1 person = new Person1("MyClass");
    static{
        System.out.println("myclass static");//2打印 myclass static
    }

    public MyClass() {
        System.out.println("myclass constructor");//8打印myclass constructor
    }
}

运行结果:

test static
myclass static
person static
person Test
test constructor
person MyClass
myclass constructor

总结:先初始化父类的静态代码—>初始化子类的静态代码–>初始化父类的非静态代码(普通成员变量)—>初始化父类构造函数—>初始化子类非静态代码(普通成员变量)—>初始化子类构造函数

精简版:静态代码–>普通成员变量–>构造方法,如果有父类先按此规则初始化父类。

final关键字

参考资料:浅析Java中的final关键字

基本用法

在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。下面就从这三个方面来了解一下final关键字的基本用法。

  1. 修饰类:

    当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

    在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。

  2. 修饰方法:

    下面这段话摘自《Java编程思想》第四版第143页:

      “使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“

    因此,如果只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为final的。

    注:类的private方法会隐式地被指定为final方法

  3. 修饰变量:

    修饰变量是final用得最多的地方,首先了解一下final变量的基本语法:

    对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象

    举个例子:

深入理解

  1. 类的final变量和普通变量有什么区别?

    1. public class TestFinal {
      public static void main(String[] args) {
             String a = "hello2"; 
             final String b = "hello";
             String d = "hello";
             String c = b + 2; 
             String e = d + 2;
             System.out.println((a == c));
             System.out.println((a == e));
      }
      }

    为什么第一个比较结果为true,而第二个比较结果为fasle。这里面就是final变量和普通变量的区别了,当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。这种和C语言中的宏替换有点像。因此在上面的一段代码中,由于变量b被final修饰,因此会被当做编译器常量,所以在使用到b的地方会直接将变量b替换为它的值。而对于变量d的访问却需要在运行时通过链接来进行。想必其中的区别大家应该明白了,不过要注意,只有在编译期间能确切知道final变量值的情况下,编译器才会进行这样的优化,比如下面的这段代码就不会进行优化:

    public class Test {
       public static void main(String[] args)  {
           String a = "hello2"; 
           final String b = getHello();
           String c = b + 2; 
           System.out.println((a == c));
    
       }
    
       public static String getHello() {
           return "hello";
       }
    }
  2. 被final修饰的引用变量指向的对象内容可变吗,引用可变吗?

    public class Test {
    public static void main(String[] args) {
        final int[] array = new int[]{1, 2, 3};
        //改变array的内容:允许
        array[1] = 4;
        for(int i=0; i<array.length; i++){
            System.out.println(array[i]);
        }
        //改变array的引用,使其指向一个新的对象,不允许
        //这里报错:
        //The final local variable array cannot be assigned. 
        //It must be blank and not using a compound assignment
        array = new int[3];
    }
    }

    内容可变,引用不可变(=, new 操作都不可行)

  3. 形参用final修饰可以达到防止实参被修改的目的吗?

    class Test1{
    public void test1(final int a){
        //The final local variable a cannot be assigned. 
        //It must be blank and not using a compound assignment
        a = 2;
    }
    
    public void test2(final StringBuffer sb){
        //The final local variable a cannot be assigned. 
        //It must be blank and not using a compound assignment
        sb = new StringBuffer("456");
    
        sb.append("456");
    }
    }
    
    public class TestFinalParameter {
    public static void main(String[] args) {
        Test1 test1 = new Test1();
        int a = 1;
        test1.test1(1);
    
        StringBuffer sb = new StringBuffer("123");
        test1.test2(sb);
        System.out.println(sb);
    }
    }

    得到的结果是:123456

    很显然,如果形参是基本类型变量,值确实不可变了。但如果形参是引用变量,那么同上,引用不可变,仍旧只能指向实参那个对象,但是内容却可变,可以使用append方法改变其内容。

猜你喜欢

转载自blog.csdn.net/qq_32127759/article/details/80725115