java+内存分配及变量存储位置的区别

1、引题java

在java语言的全部数据类型中,String类型是比较特殊的一种类型,同时也是面试的时候常常被问到的一个知识点,本文结合java内存分配深度分析关于String的许多使人迷惑的问题。下面是本文将要涉及到的一些问题,若是读者对这些问题都了如指掌,则可忽略此文。

一、java内存具体指哪块内存?这块内存区域为何要进行划分?是如何划分的?划分以后每块区域的做用是什么?如何设置各个区域的大小?

二、String类型在执行链接操做时,效率为何会比StringBuffer或者StringBuilder低?StringBuffer和StringBuilder有什么联系和区别?

三、java中常量是指什么?String s = "s" 和 String s = new String("s") 有什么不同?

本文经多方资料的收集整理和概括,最终撰写成文,若是有错误之处,请多多指教!

2、java内存分配面试

一、JVM简介
  Java虚拟机(Java Virtual Machine 简称JVM)是运行全部Java程序的抽象计算机,是Java语言的运行环境,它是Java 最具吸引力的特性之一。Java虚拟机有本身完善的硬体架构,如处理器、堆栈、寄存器等,还具备相应的指令系统。JVM屏蔽了与具体操做系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就能够在多种平台上不加修改地运行。
        一个运行时的Java虚拟机实例的天职是:负责运行一个java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。若是同一台计算机上同时运行三个Java程序,将获得三个Java虚拟机实例。每一个Java程序都运行于它本身的Java虚拟机实例中。
    以下图所示,JVM的体系结构包含几个主要的子系统和内存区:
         垃圾回收器(Garbage Collection):负责回收堆内存(Heap)中没有被使用的对象,即这些对象已经没有被引用了。
         类装载子系统(Classloader Sub-System):除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。
         执行引擎(Execution Engine):负责执行那些包含在被装载类的方法中的指令。
         运行时数据区(Java Memory Allocation Area):又叫虚拟机内存或者Java内存,虚拟机运行时须要从整个计算机内存划分一块内存区域存储许多东西。例如:字节码、从已装载的class文件中获得的其余信息、程序建立的对象、传递给方法的参数,返回值、局部变量等等。



二、java内存分区
  从上节知道,运行时数据区便是java内存,并且数据区要存储的东西比较多,若是不对这块内存区域进行划分管理,会显得比较杂乱无章。程序喜欢有规律的东西,最讨厌杂乱无章的东西。 根据存储数据的不一样,java内存一般被划分为5个区域:程序计数器(Program Count Register)、本地方法栈(Native Stack)、方法区(Methon Area)、栈(Stack)、堆(Heap)。
  程序计数器(Program Count Register):又叫程序寄存器。JVM支持多个线程同时运行,当每个新线程被建立时,它都将获得它本身的PC寄存器(程序计数器)。若是线程正在执行的是一个Java方法(非native),那么PC寄存器的值将老是指向下一条将被执行的指令,若是方法是 native的,程序计数器寄存器的值不会被定义。 JVM的程序计数器寄存器的宽度足够保证能够持有一个返回地址或者native的指针。
        栈(Stack):又叫堆栈。JVM为每一个新建立的线程都分配一个栈。也就是说,对于一个Java程序来讲,它的运行就是经过对栈的操做来完成的。栈以帧为单位保存线程的状态。JVM对栈只进行两种操做:以帧为单位的压栈和出栈操做。咱们知道,某个线程正在执行的方法称为此线程的当前方法。咱们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧,这个帧天然成为了当前帧。在此方法执行期间,这个帧将用来保存参数、局部变量、中间计算过程和其余数据。从Java的这种分配机制来看,堆栈又能够这样理解:栈(Stack)是操做系统在创建某个进程时或者线程(在支持多线程的操做系统中是线程)为这个线程创建的存储区域,该区域具备先进后出的特性。其相关设置参数:

-Xss --设置方法栈的最大值算法

本地方法栈(Native Stack):存储本地方方法的调用状态。



      方法区(Method Area):当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,而后把这些类型信息(包括类信息、常量、静态变量等)放到方法区中,该内存区域被全部线程共享,以下图所示。本地方法区存在一块特殊的内存区域,叫常量池(Constant Pool),这块内存将与String类型的分析密切相关。


      堆(Heap):Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被全部线程共享的一块内存区域。在此区域的惟一目的就是存放对象实例,几乎全部的对象实例都是在这里分配内存,可是这个对象的引用倒是在栈(Stack)中分配。所以,执行String s = new String("s")时,须要从两个地方分配内存:在堆中为String对象分配内存,在栈中为引用(这个堆对象的内存地址,即指针)分配内存,以下图所示。



        JAVA虚拟机有一条在堆中分配新对象的指令,却没有释放内存的指令,正如你没法用Java代码区明确释放一个对象同样。虚拟机本身负责决定如何以及什么时候释放再也不被运行的程序引用的对象所占据的内存,一般,虚拟机把这个任务交给垃圾收集器(Garbage Collection)。其相关设置参数:

-Xms -- 设置堆内存初始大小数组

-Xmx -- 设置堆内存最大值多线程

-XX:MaxTenuringThreshold -- 设置对象在新生代中存活的次数架构

-XX:PretenureSizeThreshold -- 设置超过指定大小的大对象直接分配在旧生代中app

Java堆是垃圾收集器管理的主要区域,所以又称为“GC 堆”(Garbage Collectioned Heap)。如今的垃圾收集器基本都是采用的分代收集算法,因此Java堆还能够细分为:新生代(Young Generation)和老年代(Old Generation),以下图所示。分代收集算法的思想:第一种说法,用较高的频率对年轻的对象(young generation)进行扫描和回收,这种叫作minor collection,而对老对象(old generation)的检查回收频率要低不少,称为major collection。这样就不须要每次GC都将内存中全部对象都检查一遍,以便让出更多的系统资源供应用系统使用;另外一种说法,在分配对象遇到内存不足时,先对新生代进行GC(Young GC);当新生代GC以后仍没法知足内存空间分配需求时, 才会对整个堆空间以及方法区进行GC(Full GC)。


     

    在这里可能会有读者表示疑问:记得还有一个什么永久代(Permanent Generation)的啊,难道它不属于Java堆?亲,你答对了!其实传说中的永久代就是上面所说的方法区,存放的都是jvm初始化时加载器加载的一些类型信息(包括类信息、常量、静态变量等),这些信息的生存周期比较长,GC不会在主程序运行期对PermGen Space进行清理,因此若是你的应用中有不少CLASS的话,就极可能出现PermGen Space错误。其相关设置参数:

-XX:PermSize --设置Perm区的初始大小jvm

-XX:MaxPermSize --设置Perm区的最大值ide

新生代(Young Generation)又分为:Eden区和Survivor区,Survivor区有分为From Space和To Space。Eden区是对象最初分配到的地方;默认状况下,From Space和To Space的区域大小相等。JVM进行Minor GC时,将Eden中还存活的对象拷贝到Survivor区中,还会将Survivor区中还存活的对象拷贝到Tenured区中。在这种GC模式下,JVM为了提高GC效率, 将Survivor区分为From Space和To Space,这样就能够将对象回收和对象晋升分离开来。新生代的大小设置有2个相关参数:

-Xmn -- 设置新生代内存大小。函数

-XX:SurvivorRatio -- 设置Eden与Survivor空间的大小比例

老年代(Old Generation): 当 OLD 区空间不够时, JVM 会在 OLD 区进行 major collection ;彻底垃圾收集后,若Survivor及OLD区仍然没法存放从Eden复制过来的部分对象,致使JVM没法在Eden区为新对象建立内存区域,则出现"Out of memory错误"  。

Java内存分配与管理是Java的核心技术之一,以前咱们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天咱们再次深刻Java核心,详细介绍一下Java在内存分配方面的知识。通常Java在内存分配时会涉及到如下区域:

  ◆寄存器:咱们在程序中没法控制

  ◆栈:存放基本类型的数据和对象的引用,但对象自己不存放在栈中,而是存放在堆中(new 出来的对象)

  ◆堆:存放用new产生的数据

  ◆静态域:存放在对象中用static定义的静态成员

  ◆常量池:存放常量

  ◆非RAM存储:硬盘等永久存储空间

Java内存分配中的栈

  在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。

  当在一段代码块定义一个变量时,Java就在栈中 为这个变量分配内存空间,当该变量退出该做用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间能够当即被另做他用。栈中的数据大小和生命周期是能够肯定的,当没有引用指向数据时,这个数据就会消失。

Java内存分配中的堆

  堆内存用来存放由new建立的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

  在堆中产生了一个数组或对象后,还能够 在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。 引用变量就至关因而 为数组或对象起的一个名称,之后就能够在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就至关因而为数组或者对象起的一个名称。

  引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其做用域以外后被释放。而数组和对象自己在堆中分配,即便程序 运行到使用 new 产生数组或者对象的语句所在的代码块以外,数组和对象自己占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍 然占据内存空间不放,在随后的一个不肯定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的缘由。

  实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针!

堆与栈

  Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象经过new、newarray、 anewarray和multianewarray等指令创建,它们不须要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优点是能够动态地分配内存 大小,生存期也没必要事先告诉编译器,由于它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些再也不使用的数据。但缺点是,因为要在运行时动态 分配内存,存取速度较慢。

  栈的优点是,存取速度比堆要快,仅次于寄存器,栈数据能够共享。但缺点是,存在栈中的数据大小与生存期必须是 肯定的,缺少灵活性。栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象句柄(引用)。

栈有一个很重要的特殊性,就是存在栈中的数据能够共享。假设咱们同时定义:

  Java代码

  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, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另外一个对象引用变量。

Java代码 1.int i1 = 9;
2.int i2 = 9;
3.int i3 = 9;
4.public static final int INT1 = 9;
5.public static final int INT2 = 9;
6.public static final int INT3 = 9;

对于成员变量和局部变量:成员变量就是方法外部,类的内部定义的变量;局部变量就是方法或语句块内部定义的变量。局部变量必须初始化。 形式参数是局部变量,局部变量的数据存在于栈内存中。栈内存中的局部变量随着方法的消失而消失。 成员变量存储在堆中的对象里面,由垃圾回收器负责回收。 如如下代码: Java代码 1.class BirthDate {
2. private int day;
3. private int month;
4. private int year;
5. public BirthDate(int d, int m, int y) {
6. day = d;
7. month = m;
8. year = y;
9. }
10. 省略get,set方法………
11.}
12.
13.public class Test{
14. public static void main(String args[]){
15.int date = 9;
16. Test test = new Test();
17. test.change(date);
18. BirthDate d1= new BirthDate(7,7,1970);
19. }
20.
21. public void change1(int i){
22. i = 1234;
23. }
对于以上这段代码,date为局部变量,i,d,m,y都是形参为局部变量,day,month,year为成员变量。下面分析一下代码执行时候的变化:

  1. main方法开始执行:int date = 9; date局部变量,基础类型,引用和值都存在栈中。
  2. Test test = new Test(); test为对象引用,存在栈中,对象(new Test())存在堆中。
  3. test.change(date); i为局部变量,引用和值存在栈中。当方法change执行完成后,i就会从栈中消失。
  4. BirthDate d1= new BirthDate(7,7,1970);
    d1 为对象引用,存在栈中,对象(new BirthDate())存在堆中,其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,所以它们的数据也存储在栈中。 day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。当BirthDate构造方法执行完以后,d,m,y将从栈中消失。 5.main方法执行完以后,date变量,test,d1引用将从栈中消失,new Test(),new BirthDate()将等待垃圾回收。 常量池 (constant pool)

  常量池指的是在编译期被肯定,并被保存在已编译的.class文件中的一些数据。

除了包含代码中所定义的各类基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,好比:

  ◆类和接口的全限定名;

  ◆字段的名称和描述符;

  ◆方法和名称和描述符。

若是是编译期已经建立好(直接用双引号定义的)的就存储在常量池中,若是是运行期(new出来的)才能肯定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。

String是一个特殊的包装类数据。能够用:

  Java代码

  String str = new String("abc");

  String str = "abc";

  两种的形式来建立,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会建立一个新的对象。而第二种是先在栈中建立一个对 String类的对象引用变量str,而后经过符号引用去字符串常量池 里找有没有"abc",若是没有,则将"abc"存放进字符串常量池 ,并令str指向”abc”,若是已经有”abc” 则直接令str指向“abc”。

  比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==,下面用例子说明上面的理论。

  Java代码

  String str1 = "abc";

  String str2 = "abc";

  System.out.println(str1==str2); //true

  能够看出str1和str2是指向同一个对象的。

  Java代码

  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常量池问题的几个例子

示例1:

Java代码

  String s0="kvill";

  String s1="kvill";

  String s2="kv" + "ill";

  System.out.println( s0==s1 );

  System.out.println( s0==s2 );

  结果为:

  true

  true

分析:首先,咱们要知结果为道Java 会确保一个字符串常量只有一个拷贝。

  由于例子中的 s0和s1中的”kvill”都是字符串常量,它们在编译期就被肯定了,因此s0==s1为true;而”kv”和”ill”也都是字符串常量,当一个字 符串由多个字符串常量链接而成时,它本身确定也是字符串常量,因此s2也一样在编译期就被解析为一个字符串常量,因此s2也是常量池中” kvill”的一个引用。因此咱们得出s0==s1==s2;

示例2:

示例:

  Java代码

  String s0="kvill";

  String s1=new String("kvill");

  String s2="kv" + new String("ill");

  System.out.println( s0==s1 );

  System.out.println( s0==s2 );

  System.out.println( s1==s2 );

  结果为:

  false

  false

  false

分析:用new String() 建立的字符串不是常量,不能在编译期就肯定,因此new String() 建立的字符串不放入常量池中,它们有本身的地址空间。

s0仍是常量池 中"kvill”的应用,s1由于没法在编译期肯定,因此是运行时建立的新对象”kvill”的引用,s2由于有后半部分 new String(”ill”)因此也没法在编译期肯定,因此也是一个新建立对象”kvill”的应用;明白了这些也就知道为什么得出此结果了。

示例3:

Java代码

  String a = "a1";

  String b = "a" + 1;

  System.out.println((a == b)); //result = true

String a = "atrue";

  String b = "a" + "true";

  System.out.println((a == b)); //result = true

String a = "a3.4";

  String b = "a" + 3.4;

  System.out.println((a == b)); //result = true

分析:JVM对于字符串常量的"+"号链接,将程序编译期,JVM就将常量字符串的"+"链接优化为链接后的值,拿"a" + 1来讲,经编译器优化后在class中就已是a1。在编译期其字符串常量的值就肯定下来,故上面程序最终的结果都为true。

示例4:

Java代码

  String a = "ab";

  String bb = "b";

  String b = "a" + bb;

  System.out.println((a == b)); //result = false

分析:JVM对于字符串引用,因为在字符串的"+"链接中,有字符串引用存在,而引用的值在程序编译期是没法肯定的,即"a" + bb没法被编译器优化,只有在程序运行期来动态分配并将链接后的新地址赋给b。因此上面程序的结果也就为false。

示例5:

Java代码

  String a = "ab";

  final String bb = "b";

  String b = "a" + bb;

  System.out.println((a == b)); //result = true

分析:和[4]中惟一不一样的是bb字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到本身的常量 池中或嵌入到它的字节码流中。因此此时的"a" + bb和"a" + "b"效果是同样的。故上面程序的结果为true。

示例6:

Java代码

  String a = "ab";

  final String bb = getBB();

  String b = "a" + bb;

  System.out.println((a == b)); //result = false

  private static String getBB() { return "b"; }

分析:JVM对于字符串引用bb,它的值在编译期没法肯定,只有在程序运行期调用方法后,将方法的返回值和"a"来动态链接并分配地址为b,故上面 程序的结果为false。

关于String是不可变的

经过上面例子能够得出得知:

  String s = "a" + "b" + "c";

  就等价于String s = "abc";

  String a = "a";

  String b = "b";

  String c = "c";

  String s = a + b + c;

  这个就不同了,最终结果等于:

  Java代码

  StringBuffer temp = new StringBuffer();

  temp.append(a).append(b).append(c);

  String s = temp.toString();

  由上面的分析结果,可就不难推断出String 采用链接运算符(+)效率低下缘由分析,形如这样的代码:

  Java代码

  public class Test {

   public static void main(String args[]) {

   String s = null;

   for(int i = 0; i < 100; i++) {

   s += "a";

   }

   }

  }

  每作一次 + 就产生个StringBuilder对象,而后append后就扔掉。下次循环再到达时从新产生个StringBuilder对象,而后 append 字符串,如此循环直至结束。若是咱们直接采用 StringBuilder 对象进行 append 的话,咱们能够节省 N - 1 次建立和销毁对象的时间。因此对于在循环中要进行字符串链接的应用,通常都是用StringBuffer或StringBulider对象来进行 append操做。

  因为String类的immutable性质,这一说又要说不少,你们只 要知道String的实例一旦生成就不会再改变了,好比说:String str=”kv”+”ill”+” “+”ans”; 就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,而后”kvill”又和” ” 生成 “kvill “存在内存中,最后又和生成了”kvill ans”;并把这个字符串的地址赋给了str,就是由于String的”不可变”产生了不少临时变量,这也就是为何建议用StringBuffer的原 因了,由于StringBuffer是可改变的。

String中的final用法和理解

  Java代码

  final StringBuffer a = new StringBuffer("111");

  final StringBuffer b = new StringBuffer("222");

  a=b;//此句编译不经过

  final StringBuffer a = new StringBuffer("111");

  a.append("222");// 编译经过

  可见,final只对引用的"值"(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会致使编译期错误。至于它所指向的对象 的变化,final是不负责的。

总结

  栈中用来存放一些原始数据类型的局部变量数据和对象的引用(String,数组.对象等等)但不存放对象内容

  堆中存放使用new关键字建立的对象.

  字符串是一个特殊包装类,其引用是存放在栈里的,而对象内容必须根据建立方式不一样定(常量池和堆).有的是编译期就已经建立好,存放在字符串常 量池中,而有的是运行时才被建立.使用new关键字,存放在堆中。

相关文章
相关标签/搜索