深入理解Java虚拟机之Java内存区域随笔

1、java内存区域与内存溢出异常

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域:1.程序计数器,2.栈(虚拟机栈和本地方法栈 ),3.堆,4.方法区(包含运行时常量池)。程序计数器和栈(虚拟机栈和本地方法栈 )为线程私有的,堆和方法区(包含运行时常量池)为线程共享的。

  1.1程序计数器

  程序计数器是一块儿较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码

  指令,分支、循环、跳转异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  另外为了线程切换后能恢复到正确位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类区域为“线程私有”的内存。

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

  由以上可知程序计数器的两个作用:

    1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的执行流程控制

    2.多线程情况下,程序计数器记录当前线程的执行位置,线程切换时能够找到上次的执行位置

  1.2Java虚拟机栈

  与程序计数器一样,Java虚拟机栈也是线程私有的,生命周期和线程相同,描述的是方法执行的内存模型。

  Java内存可以粗糙的区分为堆内存和栈内存,其中栈就是虚拟机栈,或者说是虚拟机栈中局部变量表部分。实际上,Java虚拟机栈是由一个个栈帧组成,每个栈帧中都拥有:局

  部变量表、操作数栈、动态链接、方法出口信息。

  局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个

  指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)

  Java虚拟机会出现两种异常:StackOverFlowError 和 OutOfMemoryError

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

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

  1.3本地方法栈

  本地方法栈和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。HotSpot虚拟机则直

  接将虚拟机栈和本地方法栈合二为一。本地方法栈也会抛出StackOverFlowError 和 OutOfMemoryError异常。

  1.4堆

  堆是Java虚拟机所管理内存中最大的一块儿 ,且堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例和

  数组都在堆中分配内存。堆可以实现成固定大小,也可以是可扩展的,如果在堆中没有完成实例分配,并且堆也无法再扩展时,会抛出OutOfMemoryError异常。

  Java堆是垃圾收集器管理的重要区域,因此也被称作GC堆。从垃圾回收角度,由于现在的垃圾收集器基本都采用分代收集算法,所以Java堆还可以分为新生代和老年代,再细

  致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是为了更好的回收内存,或者更快的分配内存。

  在JDK1.8中移除整个永久带,取而代之的是一个叫元空间的区域。永久带使用的是JVM的堆内存空间,而元空间使用的是物理内存,受到本机物理内存的限制。取而代之的目的

  总结一下几点:

    1.字符串存在永久带中,容易出现性能问题和内存泄露

    2.类及方法的信息等比较难确定其大小,因此对于永久带的大小指定比较困难,太小容易出现永久带溢出,太大容易导致老年代溢出。

    3.永久带会为GC带来不必要的复杂度,并且回收效率偏低。

  1.5方法区

  与Java堆一样,是各个线程共享的区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。HotSpot虚拟机中方法区被称为“永久带”,

  其实两者并不等价,仅仅是HotSpot虚拟机把GC分代收集扩展至方法区,垃圾收集行为在这个区域是比较少见的,但并非数据进入方法区之后就“永久”存在了,方法区的内存回

  收主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。

  1.6运行时常量池

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

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

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

  

  1.7直接内存

  直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁的使用,当配置虚拟机参数时,如果忽略掉直接内存,使得

  各个内存区域的总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

  JDK1.4中加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O形势,它可以直接使用Native函数库直接分配堆外内存,然后通过

  存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆和Native堆之间来回复制数据,在一些场景中显著提高了性能。

 

2、补充内容

  2.1String对象的两种创建方式

  两种创建方式如下

String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1==s2);//false,因为s1是在常量池创建的对象,s2是在堆内存中创建对象

  那么 String s2 = new String("abc");创建了几个对象?两个。

  解释:现有字符串"abc"放入常量池,然后new了一份字符串"abc"放入Java堆中,所以是常量池一份"abc"对象和堆中一份"abc"对象(字符串常量"abc"在编译期就已经确定

  放入常量池,而Java堆上的"abc"是在运行期初始化阶段才确定)。s2是String对象的引用,根据Java虚拟机栈的知识可以知道,这个引用(s2)存在于Java虚拟机栈中,

  并且s2指向Java堆中的对象"abc"。

  2.2String类型的常量池的使用方法

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

    2、如果不是用双引号声明的String对象,可以使用String对象提供的 intern 方法。String.intern() 是一个Native方法,它的作用是:如果运行时常量池中已经包含了一个

    等于此String对象内容的字符串,则返回常量池中改字符串的引用;如果没有,则在常量池中创建与此String内容相同的字符串,并返回常量池中此字符串的引用。

  代码如下

        String s1 = new String("abc");
        String s2 = s1.intern();
        String s3 = "abc";
        System.out.println(s2);//abc,s1调用intern方法,会在常量池中创建"abc",且s2指向它
        System.out.println(s1==s2);//false,s1指向堆内存中的"abc",s2指向常量池中的"abc"
        System.out.println(s3==s2);//true,s2和s3都指向常量池中的"abc"

  2.3字符串拼接

  代码如下

        String s1 = "ab";
        String s2 = "cd";
        String s3 = "ab" + "cd";//常量池中的对象
        String s4 = s1 + s2;//堆中创建对象,相当于String s4 = new String("abcd");
        String s5 = "abcd"; //常量池中的对象
        System.out.println(s3==s4);//false s3指向常量池中的对象,s4指向堆中的对象
        System.out.println(s3==s5);//true 都指向常量池中的对象
        System.out.println(s4==s5);//false

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

   2.4八种基本类型的包装类和常量池

  Byte、Short、Integer、Long、Charactor、Boolean这六种包装类都实现了常量池技术,前五种包装类默认创建了数值 [-128,127]的相应类型的数据缓存,但是超出此范围

  仍然会去创建新的对象。两种浮点类型的包装类Float、Double并没有实现常量池技术。

  Integer类型比较代码如下

        Integer i1 = 40;
        Integer i2 = 40;
        System.out.println(i1==i2);//true
        Integer i3 = 300;
        Integer i4 = 300;
        System.out.println(i3==i4);//false i3和i4会各自在堆上创建对象
        Double i5 = 1.6;
        Double i6 = 1.6;
        System.out.println(i5==i6);//false 浮点类型没有常量池,都会创建对象

  更丰富的例子如下

        Integer i1 = 40;
        Integer i2 = 40;
        Integer i3 = 0;
        Integer i4 = new Integer(40);
        Integer i5 = new Integer(40);
        Integer i6 = new Integer(0);
        System.out.println(i1==i2);//true 常量池
        System.out.println(i1==i2+i3);//true 常量池
        System.out.println(i1==i4);//false i1常量池,i4堆
        System.out.println(i4==i5);//false 两个堆对象
        System.out.println(i4==i5+i6);//true
        System.out.println(40==i5+i6);//true

  i4==i5+i6为true,因为运算符 + 不适合Integer对象,首先i5和i6会进行自动拆箱操作,进行数值相加,即i4==40,然后Integer对象 i4无法与数值40进行比较,所以i4进行

  自动拆箱转为int值40,最后这条语句转为 40==40 进行数值的比较。40==i5+i6同理。

转载于:https://www.cnblogs.com/audience7510/p/10450327.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值