您现在的位置是:网站首页>javajava

java内存区域详解

deling2019年5月10日java454人已围观

简介java内存区域详解!

java内存区域详解

如果没有特殊说明,都是针对的是 HotSpot 虚拟机。

概述:

对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。

运行时数据区域

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同。

线程私有的:
  • 程序计数器

  • 虚拟机栈

  • 本地方法栈

线程共享的:

  • 方法区

  • 直接内存(非运行时数据区的一部分)

程序计数器

  程序计时器是一块很小的内存空间,可以看作是当线程所执行的字节码的行号指示器。 字节码解释器工作时通过改变这个程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、跳转、线程恢复等功能都是要需要依赖这个计数器来完成. 另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

java虚拟机栈

  与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

  Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)

局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
  • StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出

  • StackOverFlowError 异常。 OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。

  Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

栈中方法/函数调用

  Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。

Java 方法有两种返回方式:

  • return 语句。

  • 抛出异常。

无论那种返回方式都会导致栈帧被弹出

本地方法栈

本地方法栈和虚拟机栈的区别:

  虚拟机栈为虚拟机执行的是java方法,(字节码服务),而,本地方法栈则为虚拟机中使用native方法服务,在HotSpot虚拟机中和Java虚拟机栈合二为一

  本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

  方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。

  Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

  Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

  根据对象的生命周期不同,所以把Java堆中的GC可以分为年轻代,年老代,在细分为Eden区、formSurvivor区、toSurvivor区、old区。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

方法区

  方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。 方法区也叫永久代。

运行常量池

  运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)

  既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

  JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

直接内存

  直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。

HotSpot虚拟机对象

  通过上面的介绍我们大概知道了虚拟机的内存情况,下面我们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。

对象的创建

1、类加载检查 :

 虚拟机一旦接受到new关节指令时,首先回去检查这个指令的参数是否能在常量池中定位到这个类的符号引用, 并且检查这个符号引用是否被加载过,被解析过,被初始化过,如果没有,则必须先执行相应的类加载过程

2、分配内存 :

 在类加载检查后,接下来虚拟机为新生对象分配内存,对象所需的内存大小,在执行类加载完成后就可以确定,为对象分配空间的任务等同于把一块确定大小的内存从堆中划分出来.分配方式有:“指针碰撞”和"空闲列表"两种,选择那种分配方式由java堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

3、初始化零值:

 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在java代码中可以不赋值就直接使用,程序就能访问到这些字段的数据类型所对应的零值。

4、设置对象头:

 初始化零值后,虚拟机要对象进行必要的设置,例如这个对象是那个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC年龄分代等这些信息,这些信息我们可以存放在对象头中,另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

5、执行init方法:

 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的访问定位

建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:

句柄:

如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

jvm.jpg

直接指针

如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

jvm1.jpg

String类和常量池

    String创建对象的两种方式

//先检查常量池中有没有这个"abc",如果字符串常量池中没有,则创建一个,然后 str 指向字符串常量池中的对象,如果有,则直接将 str 指向"abc"";
String str  = "abc";// 因为在字符串常量池中,已经有"abc",所有会直接使用str中的"abc"String str2 = "abc";
//堆中创建一个新的对象 
String str3 = new String("abc");
// 堆中创建一个新的对象String str4 = new String("abc");
System.out.println(str==str2)  //true
System.out.println(str2==str3) // flase
System.out.println(str3==str4) // false

这两种不同的创建方法是有差别的。

  • 第一种方法是在常量池中拿对象

  • 第二种是直接在堆内存中空间创建了一个新的对象

记住一点:只要使用 new 方法,便需要创建新的对象。

注意:在比较两个字符串对象的内容,建议使用equals方法.

String 类型的常量池比较特殊。它的主要使用方法有两种:

  • 直接使用双引号声明出来的 String 对象会直接存储在常量池中。

  • 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。

String s1 = new String("计算机");
      String s2 = s1.intern();
      String s3 = "计算机";
      System.out.println(s2);//计算机
      System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象,
      System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象

字符串拼接:

      String str1 = "str"; String str2 = "ing";
      String str3 = "str" + "ing";//常量池中的对象
      String str4 = str1 + str2; //在堆上创建的新的对象      
      String str5 = "string";//常量池中的对象
      System.out.println(str3 == str4);//false  
      System.out.println(str3 == str5);//true      
      System.out.println(str4 == str5);//false

尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。

String str = new String("abc"); 这句话创建了几个对象?

对我来说,我觉得这个问题可以分为两种情况

1、第一种情况: 如果String常理池中,已经创建"abc",则不会继续创建,此时只创建了一个对象new String("abc");

2、第二种情况: 如果String常量池中,没有创建"abc",则会创建两个对象,一个对象值是:"abc",一个对象为new String("abc");

参考:《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》


Tags: java

很赞哦! (6)

留言

来说点儿什么吧...

您的姓名: *

选择头像: *

留言内容:

    2019年2月25日 13:35嘿嘿

    2019年2月26日 13:20ok

    可以可以!

    2019年3月18日 09:2311

    1

    2019年3月28日 09:24www.ikeguang.com

    可以可以

    2019年5月29日 18:47qwe

    666

    2019年5月30日 16:52BlankYk

    he,tui~

    2019年5月30日 17:04123

    321

    2019年6月26日 10:02周树人

    厉害厉害

    2019年6月26日 10:34sdlakrj

    sdaag

    2019年6月29日 15:31sdagafdbaf

    dgafdgdfh

    站长回复:你这是什么什么高级语言,我表示看不懂哈哈

    2019年7月6日 16:37啦啦

    写的真好!谢谢博主

    站长回复:谢谢!

    2019年8月14日 12:35傻傻

    厉害 小林

    2019年9月11日 20:05sdfw

    fgbhjksdgjdfhag

    2019年9月11日 22:18baba

    keke tui

    2019年11月5日 20:09666

    666