再探Java内存分配



自定义View系列教程00–推翻自己和过往,重学自定义View
自定义View系列教程01–常用工具介绍
自定义View系列教程02–onMeasure源码详尽分析
自定义View系列教程03–onLayout源码详尽分析
自定义View系列教程04–Draw源码分析及其实践
自定义View系列教程05–示例分析
自定义View系列教程06–详解View的Touch事件处理
自定义View系列教程07–详解ViewGroup分发Touch事件
自定义View系列教程08–滑动冲突的产生及其处理



探索Android软键盘的疑难杂症
深入探讨Android异步精髓Handler
详解Android主流框架不可或缺的基石
站在源码的肩膀上全解Scroller工作机制



Android多分辨率适配框架(1)— 核心基础
Android多分辨率适配框架(2)— 原理剖析
Android多分辨率适配框架(3)— 使用指南


版权声明


引子

这两天有个同事抓耳挠腮地纠结:Java到底是值传递还是引用传递。百思不得其姐,他将这个问题抛给大家一起讨论。于是,有的人说传值,有的人说传引用;不管哪方都觉得自己的理解是正确无误的。我觉得:要回答这个问题不妨先搁置这个问题,先往这个问题的上游走走——Java内存分配。一提到内存分配,我想不少人的脑海里都会浮现一句话:引用放在栈里,对象放在堆里,栈指向堆。嗯哼,这句话听上去没有错;但是我们继续追问一下:这个栈是什么栈?是龙门客栈么?非也!它其实是Java虚拟机栈。呃,到了此处,好学的童鞋忍不住要追问了:啥是Java虚拟机栈呢?不急,我们一起来瞅瞅。


JVM的生命周期

我们知道:每个Java程序都运行于在Java虚拟机上;也就是说:一个运行时的Java虚拟机负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就随之诞生了;当该程序执行完毕后这个虚拟机实例也就随之消亡。例如:在一台计算机上同时运行五个Java程序,那么系统将提供五个Java虚拟机实例;每个Java程序独自运行于它自己所对应的Java虚拟机实例中。

Java虚拟机中有两种线程,即:守护线程与非守护线程。守护线程通常由虚拟机自身使用,比如执行垃圾收集的线程。非守护线程,通常指的是我们自己的线程。当程序中所有的非守护线程都终止时,虚拟机实例将自动退出。


JVM运行时数据区

既然Java虚拟机负责执行Java程序,那我们就先来看看Java虚拟机体系结构,请参见下图:

在这里可以看到:class文件由类加载器载入JVM中运行。此处,我们重点关注蓝色线框中JVM的Runtime Data Areas(运行时数据区),它表示JVM在运行期间对内存空间的划分和分配。在该数据区内分为以下几个主要区域:Method Area(方法区),Heap(堆),Java Stacks(Java 栈),Program Counter Register(程序计数器),Native Method Stack(本地方法栈),现对各区域的主要作用及其特点作如下详细介绍。

Method Area(方法区)

Method Area(方法区)是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError(OOM)异常。为进一步了解Method Area(方法区),我们来看在该区域内包含了哪些具体组成部分。

(1) 运行时常量池

Class文件中除了有类的版本、字段、方法、接口等描述等与类紧密相关的信息之外,还有一个常量池用于存放编译期生成的各种字面量和符号引用;该常量池将在类加载后被存放到方法区的运行时常量池中。换句话说:在运行时常量池中存放了该类使用的常量的一个有序集合,它在java程序的动态连接中起着非常重要的作用。在该集合中包括直接常量(string,integer和,floating point等)和对其他类型、字段和方法的符号引用。外界可通过索引访问运行时常量池中的数据项,这一点和访问数组非常类似。当然,运行时常量池是方法区的一部分,它也会受到方法区内存的限制,当运行时常量池无法再申请到内存时也会抛出OutOfMemoryError(OOM)异常。

(2) 类型信息

在该部分中包括:

  • 类型的完全限定名
  • 类型的直接超类的全限定名
  • 类型是类类型还是接口类型
  • 类型的访问修饰符(public、abstract、final等)
  • 直接超接口的全限定名的有序列表

(3) 字段信息

字段信息用于描述该类中声明的所有字段(局部变量除外),它包含以下具体信息:

  • 字段名
  • 字段类型
  • 字段的修饰符
  • 字段的顺序

(4) 方法信息

方法信息用于描述该类中声明的所有方法,它包含以下具体信息:

  • 方法名
  • 方法的返回类型
  • 方法输入参数的个数,类型,顺序
  • 方法的修饰符
  • 操作数栈
  • 在帧栈中的局部变量区的大小

(5) 类变量

该部分用于存放类中static修饰的变量。

(6) 指向类加载器的引用

类由类加载器加载,JVM会在方法区保留指向该类加载器的引用。

(7) 指向Class实例的引用

在类被加载器加载的过程中,虚拟机会创建一个代表该类的Class对象,与此同时JVM会在方法区保留指向该Class的引用。

Program Counter Register(程序计数器)

Program Counter Register(程序计数器)在Runtime Data Areas(运行时数据区)只占据非常小的内存空间,它用于存储下一条即将执行的字节码指令的地址。

Java Stacks(Java 栈)

Java Stacks(Java 栈)亦称为虚拟机栈(VM Stack),也就是我们通常说的栈。它用于描述的Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。Java Stacks(Java 栈)的生命周期与线程相同;当一个线程执行完毕那么该栈亦被清空。

Native Method Stack(本地方法栈)

Native Method Stack(本地方法栈)与Java Stacks(Java 栈)非常类似,它用于存储调用本地方法(C/C++)所涉及到的局部变量表、操作栈等信息。

Heap(堆)

Heap(堆)在虚拟机启动时创建,用于存放对象实例,几乎所有的对象实例都在这里分配内存。所以,Heap(堆)是Java 虚拟机所管理的内存中最大的一块,也是垃圾回收器管理的重点区域。

小结

在此对JVM运行时数据区做一个小结:

  • Method Area(方法区)和Heap(堆)是被所有线程共享的内存区域。
  • Java Stacks(Java 栈)和Program Counter Register(程序计数器)以及Native Method Stack(本地方法栈)是各线程私有的内存区域。
  • 创建一个对象,该对象的引用存放于Java Stacks(Java 栈)中,真正的对象实例存放于Heap(堆)中。这也是大家常说的:栈指向堆。
  • 除了刚才提到的JVM运行时数据区所涉及到的内存以外,我们还需要关注直接内存(Direct Memory)。请注意:直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError(OOM)异常出现。比如,在使用NIO时它可以使用Native 函数库直接分配堆外内存,然后通过存储在Java 堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。类似的操作,可避免了在Java 堆和Native 堆中来回复制数据,从而提高性能。

Java调用方法时的参数传递机制

在调用Java方法传递参数的时候,到底是传值还是传引用呢?面对众多的争论,我们还是来瞅瞅代码,毕竟代码不会说谎。我们先来看一个非常简单的例子:交换两个int类型的数据,代码如下:

package cn.com;
/**
 * 原创作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class TestMemory {

    public static void main(String[] args) {
        TestMemory testMemory=new TestMemory();
        int number1=9527;
        int number2=1314;
        System.out.println("main方法中,数据交换前:number1="+number1+" , number2="+number2);
        testMemory.swapData(number1, number2);
        System.out.println("main方法中,数据交换后:number1="+number1+" , number2="+number2);
    }

    private void swapData(int a,int b) {
        System.out.println("swapData方法中,数据交换前:a="+a+" , b="+b);
        int temp=a;
        a=b;
        b=temp;
        System.out.println("swapData方法中,数据交换后:a="+a+" , b="+b);
    }

}

我们在main方法中声明的两个变量number1=9527 , number2=1314;然后将这两个数作为参数传递给了方法swapData(int a,int b),并在该方法内交换数据。至于代码本身无需再过多的解释了;不过,请思考输出的结果是什么?在您考虑之后,请参见如下打印信息:

main方法中,数据交换前:number1=9527 , number2=1314
swapData方法中,数据交换前:a=9527 , b=1314
swapData方法中,数据交换后:a=1314 , b=9527
main方法中,数据交换后:number1=9527 , number2=1314

嗯哼,这和你想的一样么?为什么会是这样呢?还记得刚才讨论Java Stacks(Java 栈)时说的么:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。结合示例的代码:main( )方法在一个栈帧中,swapData( )在另外一个栈帧中;两者彼此独立互不干扰。在main( )中调用swapData( )传入参数时它的本质是:将实际参数值的副本(复制品)传入其它方法内而参数本身不会受到任何影响。也就是说,这number1和number2这两个变量仍然存在于main( )方法所对应的栈帧中,但number1和number2这两个变量的副本(即int a和int b)存在于swapData( )方法所对应的栈帧中。故,在swapData( )中交换数据,对于main( )是没有任何影响的。这就是Java中调用方法时的传值机制——值传递。

嗯哼,刚才这个例子是关于基本类型的参数传递。Java对于引用类型的参数传递一样采用了值传递的方式。我们在刚才的示例中稍加改造。首先,我们创建一个类,该类有两个变量number1和number2,请看代码:

package cn.com;
/**
 * 原创作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class DataObject {

    private int number1;
    private int number2;

    public int getNumber1() {
        return number1;
    }
    public void setNumber1(int number1) {
        this.number1 = number1;
    }
    public int getNumber2() {
        return number2;
    }
    public void setNumber2(int number2) {
        this.number2 = number2;
    }

}

好了,现在我们来测试交换DataObject类对象中的两个数据:

package cn.com;
/**
 * 原创作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class TestMemory {

    public static void main(String[] args) {
        TestMemory testMemory=new TestMemory();
        DataObject dataObject=new DataObject();
        dataObject.setNumber1(9527);
        dataObject.setNumber2(1314);
        System.out.println("main方法中,数据交换前:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());
        testMemory.swapData(dataObject);
        System.out.println("main方法中,数据交换后:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());
    }

    private void swapData(DataObject dataObject) {
        System.out.println("swapData方法中,数据交换前:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());
        int temp=dataObject.getNumber1();
        dataObject.setNumber1(dataObject.getNumber2());
        dataObject.setNumber2(temp);
        System.out.println("swapData方法中,数据交换后:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());

    }

}

简单地描述一下代码:在main( )中定义一个DataObject类的对象并为其number1和number2赋值;然后调用swapData(DataObject dataObject)方法,在该方法中交换数据。请思考输出的结果是什么?在您考虑之后,请参见如下打印信息:

main方法中,数据交换前:number1=9527 , number2=1314
swapData方法中,数据交换前:number1=9527 , number2=1314
swapData方法中,数据交换后:number1=1314 , number2=9527
main方法中,数据交换后:number1=1314 , number2=9527

嗯哼,为什么是这样呢?我们通过DataObject dataObject=new DataObject();创建一个对象;该对象的引用dataObject存放于栈中,而该对象的真正的实例存放于堆中。在main( )中调用swapData( )方法传入dataObject作为参数时仍然传递的是值,只不过稍微特殊点的是:该值指向了堆中的实例对象。好了,再结合栈帧来梳理一遍:main( )方法存在于与之对应的栈帧中,在该栈帧中有一个变量dataObject它指向了堆内存中的真正的实例对象。swapData( )收到main( )传递过来的变量dataObject时将其存放于其本身对应的栈帧中,但是该变量依然指向堆内存中的真正的实例对象。也就是说:main( )方法中的dataObject和swapData( )方法中的dataObject指向了堆中的同一个实例对象!所以,在swapData( )中交换了数据之后,在main( )会体现交换后的变化。在此,我们可以进一步的验证:在该swapData( )方法的最后一行添加一句代码dataObject=null ;我们发现打印信息并没有任何变化。因为这句代码仅仅使得swapData( )所对应的栈帧中的dataObject不再指向堆内存中的实例对象但不会影响main( )所对应的栈帧中的dataObject依然指向堆内存中的实例对象。

通过这两个示例,我们进一步验证了:Java中调用方法时的传递机制——值传递。当然,有的人说:基础类型传值,对象类型传引用。其实,这也没有什么错,只不过是表述方式不同罢了;只要明白其中的道理就行。如果,有些童鞋非纠缠着个别字眼不放,那我只好说:PHP是世界上最好的语言。


参考资料

时间: 2024-08-02 12:25:30

再探Java内存分配的相关文章

Android 优化二 Java内存分配机制及内存泄漏

Java内存分配机制及内存泄漏目录介绍 1.JVM内存管理 1.1 JVM内存管理图 1.2 Java采用GC进行内存管理. 2.JVM内存分配的几种策略 2.1 静态的 2.2 栈式的 2.3 堆式的 2.4 堆和栈的区别 2.5 得出结论 2.6 举个例子 2.7 调用 System.gc();进行内存回收 3.GC简单介绍 3.1 内存垃圾回收机制 3.2 关于GC介绍 3.3 如何监听GC过程 3.4 GC过程与对象的引用类型关系 4.内存泄漏简单介绍 4.1 内存泄漏的定义 4.2 内

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

Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天我们再次深入Java核心,详细介绍一下Java在内存分配方面的知识.一般Java在内存分配时会涉及到以下区域: ◆寄存器:我们在程序中无法控制 ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中(new 出来的对象) ◆堆:存放用new产生的数据 ◆静态域:存放在对象中用static定义的静态成员 ◆常量池:存放常量 ◆非RAM存储:硬盘等永久

Java内存分配、管理小结

首先是概念层面的几个问题: Java中运行时内存结构有哪几种?Java中为什么要设计堆栈分离?Java多线程中是如何实现数据共享的?Java反射的基础是什么? 然后是运用层面: 引用类型变量和对象的区别?什么情况下用局部变量,什么情况下用成员变量?数组如何初始化?声明一个数组的过程中,如何分配内存?声明基本类型数组和声明引用类型的数组,初始化时,内存分配机制有什么区?在什么情况下,我们的方法设计为静态化,为什么?(上次胡老师问文奇,问的哑口无言,当时想回答,却老感觉表述不清楚,这里也简单说明一下

java程序运行时内存分配详解_java

一. 基本概念    每运行一个java程序会产生一个java进程,每个java进程可能包含一个或者多个线程,每一个Java进程对应唯一一个JVM实例,每一个JVM实例唯一对应一个堆,每一个线程有一个自己私有的栈.进程所创建的所有类的实例(也就是对象)或数组(指的是数组的本身,不是引用)都放在堆中,并由该进程所有的线程共享.Java中分配堆内存是自动初始化的,即为一个对象分配内存的时候,会初始化这个对象中变量.虽然Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在栈中分配,也

java 字符串内存分配的分析与总结(推荐)_java

经常在网上各大版块都能看到对于java字符串运行时内存分配的探讨,形如:String a = "123",String b = new String("123"),这两种形式的字符串是存放在什么地方的呢,其实这两种形式的字符串字面值"123"本身在运行时既不是存放在栈上,也不是存放在堆上,他们是存放在方法区中的某个常量区,并且对于相同的字符串字面值在内存中只保留一份.下面我们将以实例来分析. 1.==运算符作用在两个字符串引用比较的两个案例: p

深入分析Java内存区域的使用详解_java

Java 内存划分:     在Java内存分配中,java将内存分为:方法区,堆,虚拟机栈,本地方法栈,程序计数器.其中方法区和堆对于所有线程共享,而虚拟机栈和本地方法栈还有程序计数器对于线程隔离的.每个区域都有各自的创建和销毁时间. 程序计数器:     作用是当前线程所执行的字节吗的行号指示器.Java的多线程是通过线程轮流切换并分配处理器执行时间方式来实现的.因此,每个线程为了能在切换后能恢复到正确的位置,每个线程需要独立的程序计数器. Java 虚拟机栈:     每个放在被执行的时候

Java中内存分配的几种方法_java

一.数组分配的上限 Java里数组的大小是受限制的,因为它使用的是int类型作为数组下标.这意味着你无法申请超过Integer.MAX_VALUE(2^31-1)大小的数组.这并不是说你申请内存的上限就是2G.你可以申请一个大一点的类型的数组.比如: 复制代码 代码如下: final long[] ar = new long[ Integer.MAX_VALUE ]; 这个会分配16G -8字节,如果你设置的-Xmx参数足够大的话(通常你的堆至少得保留50%以上的空间,也就是说分配16G的内存,

JAVA垃圾收集器与内存分配策略详解_java

引言 垃圾收集技术并不是Java语言首创的,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言.垃圾收集技术需要考虑的三个问题是: 1.哪些内存需要回收 2.什么时候回收 3.如何回收 java内存运行时区域的分布,其中程序计数器,虚拟机栈,本地方法区都是随着线程而生,随线程而灭,所以这几个区域就不需要过多考虑回收问题.但是堆和方法区就不一样了,只有在程序运行期间我们才知道会创建哪些对象,这部分内存的分配和回收都是动态的.垃圾收集器所关注的就是这部分内存. 一 对象

源码分析:Java对象的内存分配

Java对象的分配,根据其过程,将其分为快速分配和慢速分配两种形式,其中快速分配使用无锁的指针碰撞技术在新生代的Eden区上进行分配,而慢速分配根据堆的实现方式.GC的实现方式.代的实现方式不同而具有不同的分配调用层次.  下面就以bytecodeInterpreter解释器对于new指令的解释出发,分析实例对象的内存分配过程: 一.快速分配 1.实例的创建首先需要知道该类型是否被加载和正确解析,根据字节码所指定的CONSTANT_Class_info常量池索引,获取对象的类型信息并调 用is_