java 内存分配 final关键字

来源:互联网 发布:linux怎么安装apt get 编辑:程序博客网 时间:2024/05/27 18:17

1. java中内存的分配

java程序在运行时,内存结构分为:方法区(method),栈内存(stack),堆内存(heap),本地方法栈(java中的jni调用)等。

jvm为每一个运行的线程分配一个堆栈(方法栈),堆栈以帧为单位来保存线程的运行的状态,java中,我们将当前正在运行的方法称为当前方法,当java激活(执行)一个方法时,就会向堆栈中压入一个帧,堆栈中的这一帧就代表这个方法在运行时jvm为它分配的内存,这一帧叫做当前帧,帧里可以存放方法的参数,中间结果和局部变量等。当当前方法执行完时,当前帧也会出栈,即该内存区域被释放。方法栈中只会存放基本类型数据和对象的引用。

基础数据类型直接在栈空间分配,方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收。引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量 。方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收。局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。方法调用时传入的literal参数,先在栈空间分配,在方法调用完成后从栈空间分配。字符串常量在DATA区域分配,this在堆空间分配。数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小!

 

image

这个图是错误的,帧并不包括堆内存,存放帧的方法栈是线程私有的,其他线程无法访问的,而存放对象和数组的对内存是多个线程共享的,整个jvm中只有一份。

image

以我目前的水平,只能画到这个程度,还有很多问题没有弄明白。

 

在进行debug调试的时候,会有一个更直观的表现内存分配的图

image

每一个线程一个自己的堆栈,方法以帧的形式压入栈中。

一个类的全局变量,全局常量有存放在哪呢?

这些信息存放在一个叫做方法区的地方,在每一个jvm的内部,都有一个称为方法去的逻辑存储区。

方法区存放装载的类数据信息包括:
    (1):基本信息:
           1)每个类的全限定名
           2)每个类的直接超类的全限定名(可约束类型转换)
           3)该类是类还是接口
           4)该类型的访问修饰符
           5)直接超接口的全限定名的有序列表
    (2):每个已装载类的详细信息:
           1)运行时常量池: 
             jvm为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string,
            integer, 和floating point常量)和对类型,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。
             因为常量池存储了一个类型所使用到的所有类型,域和方法的符号引用,所以它在java 程序 的动态链接中起了核心的作用。

           2)字段信息:
              类中声明的每一个字段的信息(字段名,类型,修饰符)。
           3)方法信息:
              类中声明的每一个方法的信息(名,返回类型,参数类型,修饰符,方 法的字节码和异常表)。
           4)静态变量    也就是类变量(class variables)用关键字 static 修饰,在类加载的时候,分配类变量的内存,以后在生成类的实例对象时,将共享这块内存(类变量)  ,任何一个对象对类变 
               量的修改,都会影响其它对象。外部有两种访问方式:通过对象来访问或通过类名来访问。

           5)到类 classloader 的引用:即到该类的类装载器的引用。
           6)到类 class 的引用:
                 虚拟机为每一个被装载的类型创建一个 class 实例, 用来代表这个被装载的类。

方法区是被多个线程所共享的,因此会有线程安全的问题。

编译器将源代码编译成字节码(.class)时, 就已经将各种类型的方法的局部变量, 操作数栈大小确定并放在字节码中,随着类一并装载入方法区。当调用方法时,通过访问方法区中的类的信息,得到局部变量以及操作数栈的大小。 
也就是说: 在方法中定义的一些基本类型的变量和对象的引用变量都在方法的栈内存中分配。 当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量所分配的内存空间,

该内存空间可以立即被另作它用。

 

问题:方法区的内存在哪分配?

单独的一块内存,他是和堆内存,栈内存平级的内存区域,在一个jvm中,只有一个方法区,多个线程共享。例如,有个set方法,被线程a和线程b执行,set方法存在哪呢,很显然,存放在方法区。那么为什么还要有方法栈呢,也就是存放帧的内存区域,因为方法在执行时,就会被调入线程所在的方法栈呢内,为变量分配空间,计算逻辑等,需要一个区域来存放方法执行时的中间结果啊,需要到的参数啊等等。

问题:类中的属性成员的信息在哪存储?

栈内存和堆内存比较
    栈与堆都是 Java 用来在内存中存放数据的地方。与 C++不同,Java 自动管理栈和堆,程序员不能直接地设置栈或堆。
    Java 的堆是一个运行时数据区,对象从中分配空间。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java 的垃圾收
集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度 较慢。
    栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的, 缺乏灵活性。栈中主要存放一些基本类型的变量 (int,short, long, byte, float, double, boolean, char)和对象句柄。
    栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:
    int a = 3;
    int b = 3;
    编译器先处理 int a = 3;首先它会在栈中创建一个变量为 a 的引用,然后查找栈中是 否有 3 这个值,如果没找到,就将 3存放进来,然后将 a 指向 3。接着处理 int b = 3;在创建完 b 的引用变量后,
    因为在栈中已经有 3 这个值,便将 b 直接指向 3。这样,就出现了a 与 b 同时均指向 3 的情况。
 
    这时,如果再令 a=4;那么编译器 会重新搜索栈中是否有 4 值,如果没有,则将 4 存 放进来,并令 a 指向 4;如果已经有了,则直接将 a 指向这个地址。因此 a 值的改变不会影响到 b 的值。
    要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况 a 的修改并不会影响到 b, 它是由编译器完成的,它有利于节省空间。此时的内存分配示意图如下:

    而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

String 的内存分配
    String 是一个特殊的包装类数据。可以用:
    String str = new String("abc");
    String str = "abc";
    两种的形式来创建,第一种是用 new()来新建对象的,它会在存放于堆中。每调用一次 就会创建一个新的对象。
    而第二种是先在栈中创建一个对 String 类的对象引用变量 str,然后查找栈中有没有 存放"abc", 如果没有, 则将"abc"存放进栈, 并令 str 指向”abc”,如果已经有”abc” 则直接令 str 指向“abc”。
    比较类里面的数值是否相等时,用 equals()方法;当测试两个包装类的引用是否指向 同一个对象时,用==,下面用例子说明上面的理论。
    String str1 = "abc";
    String str2 = "abc";
    System.out.println(str1==str2); //true
    可以看出 str1 和 str2 是指向同一个对象的。
       String str1 = new String ("abc");
       String str2 = new String ("abc");
       System.out.println(str1==str2); // false
       用 new 的方式是生成不同的对象。每一次生成一个。
       因此用第一种方式创建多个”abc”字符串,在内存中其实只存在一个对象而已。 这种写法有利于节省内存空间。同时它可以在一定程度上提高程序的运行速度,因为 JVM 会自动根据栈中数据的实际情况来决定是否有必要创建新对象。
       而对于 String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否 有必要创建新对象,从而加重了程序的负担。
       另一方面, 要注意: 我们在使用诸如 String str = "abc";的格式时,总是想当然 地认为,创建了 String 类的对象 str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。
       只有通过 new()方法才能保证每次都创建一个新的对象。 由于 String 类的值不可变性(immutable)  ,当 String 变量需要经常变换其值时,应该考虑使用 StringBuffer 或 StringBuilder 类,以提高程序效率。

java程序的执行流程

这两天看了一下深入浅出JVM这本书,推荐给高级的java程序员去看,对你了解JAVA的底层和运行机制有
比较大的帮助。
废话不想讲了.入主题:
先了解具体的概念:
JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)
堆区:
1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
方法区:
1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
为了更清楚地搞明白发生在运行时数据区里的黑幕,我们来准备2个小道具(2个非常简单的小程序)。
AppMain.java
public   class  AppMain                //运行时, jvm 把appmain的信息都放入方法区
{
public   static   void  main(String[] args)  //main 方法本身放入方法区。
{
Sample test1 = new  Sample( " 测试1 " );   //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面
Sample test2 = new  Sample( " 测试2 " );
test1.printName();
test2.printName();
}
} Sample.java
public   class  Sample        //运行时, jvm 把appmain的信息都放入方法区
{
/** 范例名称 */
private  name;      //new Sample实例后, name 引用放入栈区里,  name 对象放入堆里
/** 构造方法 */
public  Sample(String name)
{
this .name = name;
}
/** 输出 */
public   void  printName()   //print方法本身放入 方法区里。
{
System.out.println(name);
}
} OK,让我们开始行动吧,出发指令就是:“java AppMain”,包包里带好我们的行动向导图,Let’s GO!

11-18 工作总结 - 老百姓 - 老百姓 的博客
系统收到了我们发出的指令,启动了一个Java虚拟机进程,这个进程首先从classpath中找到AppMain.class文件,读取这个文件中的二进制数据,然后把Appmain类的类信息存放到运行时数据区的方法区中。这一过程称为AppMain类的加载过程。
接着,Java虚拟机定位到方法区中AppMain类的Main()方法的字节码,开始执行它的指令。这个main()方法的第一条语句就是:
Sample test1=new Sample("测试1");
语句很简单啦,就是让java虚拟机创建一个Sample实例,并且呢,使引用变量test1引用这个实例。貌似小case一桩哦,就让我们来跟踪一下Java虚拟机,看看它究竟是怎么来执行这个任务的:
1、 Java虚拟机一看,不就是建立一个Sample实例吗,简单,于是就直奔方法区而去,先找到Sample类的类型信息再说。结果呢,嘿嘿,没找到@@,这会儿的方法区里还没有Sample类呢。可Java虚拟机也不是一根筋的笨蛋,于是,它发扬“自己动手,丰衣足食”的作风,立马加载了Sample类,把Sample类的类型信息存放在方法区里。
2、 好啦,资料找到了,下面就开始干活啦。Java虚拟机做的第一件事情就是在堆区中为一个新的Sample实例分配内存, 这个Sample实例持有着指向方法区的Sample类的类型信息的引用。这里所说的引用,实际上指的是Sample类的类型信息在方法区中的内存地址,其实,就是有点类似于C语言里的指针啦~~,而这个地址呢,就存放了在Sample实例的数据区里。
3、 在JAVA虚拟机进程中,每个线程都会拥有一个方法调用栈,用来跟踪线程运行中一系列的方法调用过程,栈中的每一个元素就被称为栈帧,每当线程调用一个方法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、局部变量和运算过程中的临时数据。OK,原理讲完了,就让我们来继续我们的跟踪行动!位于“=”前的Test1是一个在main()方法中定义的变量,可见,它是一个局部变量,因此,它被会添加到了执行main()方法的主线程的JAVA方法调用栈中。而“=”将把这个test1变量指向堆区中的Sample实例,也就是说,它持有指向Sample实例的引用。
OK,到这里为止呢,JAVA虚拟机就完成了这个简单语句的执行任务。参考我们的行动向导图,我们终于初步摸清了JAVA虚拟机的一点点底细了,COOL!
接下来,JAVA虚拟机将继续执行后续指令,在堆区里继续创建另一个Sample实例,然后依次执行它们的printName()方法。当JAVA虚拟机执行test1.printName()方法时,JAVA虚拟机根据局部变量test1持有的引用,定位到堆区中的Sample实例,再根据Sample实例持有的引用,定位到方法去中Sample类的类型信息,从而获得printName()方法的字节码,接着执行printName()方法包含的指令。

 

 

2. final关键字的使用总结

     2. 1 final关键字修饰的变量的作用域

             final关键字无非修饰两种变量,一是全局变量,当修饰全局变量的时候,作用域自然是全局,另一种是修饰局部变量,这里要注意,java中不同于c语言中,即时使用final修饰了局部变量,并没有改变该变量的作用域,当这个方法执行完毕之后,就会释放掉这个方法的帧内存。变量就被销毁了,堆里的对象等待gc回收。

    2.2 final类型的初始化

           第一种情况:作为全局常量来使用,定义的时候就要进行显示初始化,如果定义的时候不进行显示初始化,则必须在构造函数里进行初始化。

class Student {
    String name;
    int age;
    final Teacher tc;
    public void study(){
    }
    public Student(String name,int age){
        tc = new Teacher();
    }
    public Student(){
        this(null, 0);
    }
   
}
class Teacher{
    int num;
    String name;
    int age;
}

第二种情况,在函数中使用,

public void startThread(){
        final Student stumain = new Student();
        final Teacher teacher;
        new Thread(new Runnable(){
            @Override
            public void run() {
                stumain.study();
                teacher = new Teacher();//这行代码是报错的
            }
           
        }).start();
    }

2.3 在函数中什么情况下要使用final修饰局部变量?

当在一个函数中,你需要使用全局变量时,可以新建一个final类型的变量,并将全局变量的引用赋给这个final变量,因为局部变量时存放在方法栈里的,寻址速度比较快。

此外,当在函数A中调用函数B时,如果函数B的函数体不是很大,将函数B声明为final类型的,编译器在编译的时候,会直接将函数B的函数体内嵌在函数A中调用函数B的地方。

例如:

见下面的测试代码,我会执行五次:

  1. public class Test  
  2. {  
  3. public static void getJava()  
  4.     {  
  5.         String str1 = "Java ";  
  6.         String str2 = "final ";  
  7. for (int i = 0; i < 10000; i++)  
  8.         {  
  9.             str1 += str2;  
  10.         }  
  11.     }  
  12. public static final void getJava_Final()  
  13.     {  
  14.         String str1 = "Java ";  
  15.         String str2 = "final ";  
  16. for (int i = 0; i < 10000; i++)  
  17.         {  
  18.             str1 += str2;  
  19.         }  
  20.     }  
  21. public static void main(String[] args)  
  22.     {  
  23. long start = System.currentTimeMillis();  
  24.         getJava();  
  25.         System.out.println("调用不带final修饰的方法执行时间为:" + (System.currentTimeMillis() - start) + "毫秒时间");  
  26.         start = System.currentTimeMillis();  
  27.         String str1 = "Java ";  
  28.         String str2 = "final ";  
  29. for (int i = 0; i < 10000; i++)  
  30.         {  
  31.             str1 += str2;  
  32.         }  
  33.         System.out.println("正常的执行时间为:" + (System.currentTimeMillis() - start) + "毫秒时间");  
  34.         start = System.currentTimeMillis();  
  35.         getJava_Final();  
  36.         System.out.println("调用final修饰的方法执行时间为:" + (System.currentTimeMillis() - start) + "毫秒时间");  
  37.     }  

结果为:
第一次:
调用不带final修饰的方法执行时间为:1732毫秒时间
正常的执行时间为:1498毫秒时间
调用final修饰的方法执行时间为:1593毫秒时间
第二次:
调用不带final修饰的方法执行时间为:1217毫秒时间
正常的执行时间为:1031毫秒时间
调用final修饰的方法执行时间为:1124毫秒时间
第三次:
调用不带final修饰的方法执行时间为:1154毫秒时间
正常的执行时间为:1140毫秒时间
调用final修饰的方法执行时间为:1202毫秒时间
第四次:
调用不带final修饰的方法执行时间为:1139毫秒时间
正常的执行时间为:999毫秒时间
调用final修饰的方法执行时间为:1092毫秒时间
第五次:
调用不带final修饰的方法执行时间为:1186毫秒时间
正常的执行时间为:1030毫秒时间
调用final修饰的方法执行时间为:1109毫秒时间 
由以上运行结果不难看出,执行最快的是“正常的执行”即代码直接编写,而使用final修饰的方法,不像有些书上或者文章上所说的那样,速度与效率与“正常的执行”无异,而是位于第二位,最差的是调用不加final修饰的方法。

2.4 函数中使用final修饰的变量的声明周期

当函数体结束后,该变量是否依旧存在?

public static void main(String[] args) {

    final Integer x = 50;
    new Thread(new Runnable() {
        @Override
        public void run() {
            while (x > 0) {
    断点2:   System.out.println("the x is: " + x);
                try {
                    Thread.sleep(1*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }).start();

   断点1: System.out.println("the main is over: *********************************************************"
                + Thread.currentThread().getId() + "");
   
}

打印结果:

the main is over: *********************************************************1
the x is: 50
the x is: 50
the x is: 50
the x is: 50
the x is: 50
the x is: 50
the x is: 50
the x is: 50
the x is: 50
the x is: 50

main方法执行完后,x应该销毁了啊,那么自然子线程也应该停止,但是为什么子线程没有停止呢?

打断点,进行debug调试,在断点1首先停了下来,查看变量

image

F8跳转到断点2,这时候主线程已经结束

image

对比两张图会发现,主线程和子线程中的两个x根本不是一个,也就是说匿名内部类中自己维护了一个变量的引用。和方法中的变量x指向同一个对象。

匿名内部类把需要访问的外部变量作为一个隐藏的字段,这样得到了一个变量的引用拷贝 ,使用final修饰符不仅会保持对象不会改变,而且编译器还会持续维护这个对象在回调方法中的生命周期。

 

2.5 方法中的匿名内部类使用到了方法中的局部变量

编译器规定,方法中的匿名内部类在使用方法内的局部变量的时候,局部变量必须由final关键字修饰

我们先来看GC工作原理,JVM中每个进程都会有多个根,每个static变量,方法参数,局部变量,当然这都是指引用类型. 基础类型是不能作为根的,根其实就是一个存储地址. GC在工作时先从根开始遍历它引用的对象并标记它们,如此递归到最末梢,所有根都遍历后,没有被标记到的对象说明没 有被引用,那么就是可以被回收的对象(有些对象有finalized方法,虽然没有引用,但JVM中有一个专门的队列引用它们直到finalized方法被执行后才从该队列中移除成为真正没有引用的对象,可以回收,)

       但是在内部类的回调方法中,s既不可能是静态变量,也不是方法中的临时变量,也不是方法参数,它不可能作为根,在内部类 中也没有变量引用它,它的根在内部类外部的那个方法中,如果这时外面变量重指向其它对象,则这个对象就失去了引用, 可能被回收,而由于内部类回调方法大多数在其它线程中执行,可能还要在回收后还会继续访问它.这将是什么结果?
       匿名内部类把需要访问的外部变量作为一个隐藏的字段,这样得到了一个变量的引用拷贝 ,使用final修饰符不仅会保持对象不会改变,而且编译器还会持续维护这个对象在回调方法中的生命周期。所以这才是final 变量和final参数的根本意义。