深入剖析Java内存分配、管理

来源:互联网 发布:web二手交易网站源码 编辑:程序博客网 时间:2024/05/17 08:14


一.首先提出几个问题

(1)概念层面的几个问题:

    Java中运行时内存结构有哪几种?

    Java中为什么要设计堆栈分离?

    Java多线程中是如何实现数据共享的?

    Java反射的基础是什么?

(2)运用层面的问题:

     引用类型变量和对象的区别?

     什么情况下用局部变量,什么情况下用成员变量?

     数组如何初始化?声明一个数组的过程中,如何分配内存?

     声明基本类型数组和声明引用类型的数组,初始化时,内存分配机制有什么区别?

     在什么情况下我们的方法设计为静态化,为什么?

二.问题详解

1.Java中运行时的内存结构:方法区和Java栈

1.1方法区:方法区是系统分配的一个内存逻辑区域,是JVM在装载类文件时,用于存储类型信息的(类的描述信息)。在其中存放的信息包括:

1.1.1类的基本信息:

       1.每个类的全限定名

       2.每个类的直接超类的全限定名(可约束类型转换)

       3.该类是类还是接口的标志

       4.该类型的访问修饰符

       5.直接超接口的全限定名的有序列表

1.1.2已装载类的详细信息

       1.运行时常量池:在方法区中,每个类型都对应一个常量池,存放该类型所用到的所有的常量。常量池中存储了诸如文字字符串、final变量值、类名和方法名常量。它们以                                          数组的形式通过索引被访问,是外部条用与类联系及类型对象化的桥梁。(存的可能是普通字符串,然后经过常量池解析,则变成指向某个类的引用)

       2.字段信息:字段信息存放类中声明的每一个字段的信息,包括字段名、类型、修饰符。字段名称指的是类或者接口的实例变量或类变量,字段描述符是一个指示字段类型                                   的字符串,如:private A a=null;则a为字段名,A为描述符,private为修饰符。

       3.方法信息:类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。(在编译的时候,就已经将方法的局部变量、操作数栈                                 大小等确定并存放在字节码中,在装载的时候,随着类一起装入方法区)

       4.静态变量:即为类变量。类的所有实例都共享,我们只需知道,在方法区有个静态区,用来专门存放静态变量和静态块。

       5.到类classloader的应用:到该类的类装载器的引用。

       6.到类class的应用:虚拟机为每一个被装载的类型创建一个class实例,用来代表这个被装载的类。

        因此可以知道反射的基础:在装载类的时候,加入方法区中的所有信息,最后都会形成Class类的实例,代表这个被装载的类,方法区中的所有信息,都是可以通过这个Class类对象反射得到。我们都知道对象是类的实例。类是相同结构的对象的一种抽象。同类的各个对象之间,其实拥有相同的结构(属性),拥有相同的功能(方法),各个对象的区别只在于属性值的不同。同样,所有的类都是Class类的实例。他们都拥有相同的结构------Field数组、Method数组。而各个类中的属性都是Field属性的一个具体的属性值,方法都是Method属性的一个具体属性值。

        在运行时,JVM从常量池中获得符号引用,然后在运行时解析成引用项的地址,最后通过常量池中的全限定名、方法和字段描述符,把当前类或者接口中的代码与其他类或者接口中的代码联系起来。

1.2 Java栈

       JVM栈是程序运行时单位,决定了程序如何执行,或者说数据如何处理。在Java中每一个线程都会有一个线程的JVM栈与之相对应,因为不同的线程执行的逻辑显然不同,因此都需要一个独立的JVM栈来存放该线程的执行逻辑。

       方法被调用时,Java栈内存都是以帧的形式存放本地方法的调用状态,包括方法调用的参数、局部变量、中间结果等(方法都是以方法帧的形式存放在方法区的),每调用一个方法就将对应该方法的方法帧压入java栈,成为当前帧,当调用结束之后,就弹出该帧。

        这就意味着在方法中定义的一些基本类型的变量和引用变量都在方法的栈内存中分配。当在一段代码块定义一个变量时,Java就会在栈中为这个变量分配内存空间,当超过变量的作用域后(方法执行完成后),Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作它用。同时,因为变量被释放,该变量对应的对象也就失去了引用,也就变成了可以被gc对象回收的垃圾。

        由此我们就可以知道成员变量和局部变量的区别:局部变量,在方法内部声明,当该方法运行完成时,内存即被释放。成员变量,只要对象还在,哪怕某一个方法运行完了,还是存在,从系统的角度看,声明局部变量有利于内存空间的更高效利用(方法执行完之后就立即回收),成员变量可用于各个方法间进行数据共享。

2.Java栈内存

        Java栈内存的组成:局部变量区、操作数栈、帧数据区组成。

2.1局部变量区:是一个以字节为单位的数组,每一个数组元素对应一个局部变量的值。调用方法时,将方法的局部变量组成一个数组,通过索引来访问。若为非静态方法,则加入一个隐含的引用参数this,该参数指向调用这个方法的对象。而静态方法则没有this参数。因此,对象无法调用静态方法。

由此我们可以知道,方法什么时候设计为静态,什么时候为非静态?

对象是类的实例,各个对象结构相同,只是属相不同。而静态方法是对象无法调用的,所以,静态方法适合那些工具类中的工具方法,这些类只是用来实现一些功能,也不需要产生对象,通过设置对象的属性来得到各个不同的个体。

2.2 操作数栈:是一个数组,但是通过栈操作来访问。所谓操作数是那些被指令操作的数据。当需要对参数操作时,如:a=b+c,就将即将被操作的参数压栈,如将b和c压栈,然后又操作指令将它们弹出,并执行操作。虚拟机将操作数栈作为工作区。

2.3 帧数据区:主要用于常量池解析、异常处理等。

3.Java堆

        Java堆是一个运行时的数据区,用来存储数据的单元,存放通过new关键字新建的对象和数组,对象从中分配内存。

        在堆中声明的对象是不能直接访问的,不需通过在栈中声明的指向改用的变量来调用。引用变量就相当于是为数组或者对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。

        由此就可以知道,引用类型变量和对象的区别:声明的对象是在堆内存中初始化的,真正用来存储数据的,不能直接访问。引用类型变量是保存在栈当中的,一个用来引用对中对象的符号而已。

4.堆与栈的区别:

4.1从存放数据的角度:栈中存放的基本类型的变量或者是引用类型的变量,堆中存放的是对象或者数组。在栈中引用变量的大小为32位,基本类型为1-8个字节,但是对象和数组的大小是动态的,这也决定了堆中数据的动态性,因为它是运行时动态分配内存的,生存期也不必在编译时确定,Java的垃圾收集器会自动收走这些不再使用的数据。

4.2从数据共享的角度:

4.2.1 在单线程中,栈中的数据可共享

如:定义:

        int a=3;

        int b=3;

编译器先处理int a=3;首相它会在栈中创建一个变量a的引用,然后查找栈中是否有3这个值,如果没找到就将3存进来,然后将a指向3,接着处理b=3;在创建完b的引用变量后,因为栈中已经有3这个值了,便直接将b指向3,这样就出现了a和b同时指向3的情况。

但是如果定义:

        Integer a=new Integer(3);

        Integer b=new Integer(3);

这个时候的执行过程:在执行Integera=new Integer(3);时,现在栈中创建一个变量a,然后在在堆中实例化一个对象,并且将a指向这个实例化的对象,执行Integer b=new Integer(3);时,过程与上面的一样,这时在堆内存中,会有两个Integer类型的对象存在。

4.2.2 在进程的多个线程之间,数据的共享是通过堆来实现


如上图所示,堆中的数据是所有的线程栈所共享的,我们可以通过参数传递,将一个堆中的数据传入各个栈的工作内存中,从而实现多个线程间的数据共享。

4.3从程序设计的角度

JVM栈代表了处理逻辑,而JVM堆代表了数据。这样分开,使得处理逻辑更加清晰。分而治之的思想。

4.4 值传递和引用传递的真相

        程序永远都是在JVM栈中进行的,因而参数传递时,只存在基本类型和对象引用的问题,不会直接传对象本身。在运行JVM栈中,基本类型和引用的处理是一样的,都是值传递,所以,如果是传引用的方法调用。也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到JVM堆中的对象,这个时候才对应真正的对象。如果这个时候进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是JVM堆中的数据,所以这个修改是可以保持的。  

0 0
原创粉丝点击