一、Java中的内存分配
Java 程序在运行时,需要在内存中分配空间。为提高运算效率,对内存空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式与内存管理方式。
1、Class Loader 类加载器
类加载器的作用是加载类文件到内存,编程字节码。比如编写一个HelloWord.java程序,然后通过javac编译成class文件, Class Loader承担加载到内存的责任。
2、STACK 栈:存放局部变量。
- 数据用完就释放,一定是脱离了它的作用域。
- Java中,栈的大小通过-Xss来设置,当栈中存储数据比较多时,需要适当调大这个值,否则会出现java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息都是方法返回的记录点。
3、HEAP 堆:存储new()出来的东西。
- 每一个new出来的东西都有引用值。
- 每一个变量都有默认值
- byte、shot、int、long 0
- float、double、0.0
- char ‘\u0000‘ 代表空字符
- boolean false
- 引用类型 null
- 使用完毕就变成了垃圾,但是并没有立即回收。会在垃圾回收器空闲的时候回收。
- 堆内存分为三部分:
- Permanent Space 永久存储区 。
- 永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
- Young Generation Space 新生区 。
- 新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
- 新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace)。
- 所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。
- 当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。
- Tenure generation space养老区 。养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。
- Permanent Space 永久存储区 。
4、Method Area 方法区
- 方法区是被所有线程共享,该区域保存所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。
- static 变量存放于这边,用于共享。
- String 字符串常量池也在这边。
5、本地方法区(和系统相关)
6、寄存器(给CPU使用)
二、一个对象的内存分配
1、类加载器加载class文件变为字节码存入【方法区】,运行main方法进【栈】。
2、new一个对象,成员变量入【堆】,方法仍存放在【方法区】。
3、方法的调用,从【方法区】加载方法入【栈】,然后调用。执行完毕,从【栈】中被删除。
4、再new同一个对象,会在【堆】张开辟另一块位置,成员变量入【堆】,方法不动。
5、执行完毕,最后main方法,从【栈】中被删除。
6、内存中类的class文件只有一份,即字节码是唯一的。
7、static性质的也只有一份,所以多线程锁要同步static ,可以锁定字节码。
图1,1个对象
图2,2个对象
图3,对象赋值
三、一个对象的内存大小
1、基本数据的类型的大小是固定的,这里就不多说了。
2、对于非基本类型的Java对象,其大小就值得商榷。 在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。如
<span style="font-family:Arial;font-size:18px;">Object ob = new Object();</span>
3、这样在程序中完成了一个Java对象的生命,但是它所占的空间为:4byte+8byte。4byte是Java栈中保存引用的所需要的空间。而8byte则是Java堆中对象的信息。因为所有的Java非基本类型的对象都需要默认继承Object对象,因此不论什么样的Java对象,其大小都必须是大于8byte。
4、有了Object对象的大小,我们就可以计算其他对象的大小了。
<span style="font-family:Arial;font-size:18px;">Class NewObject { int count; boolean flag; Object ob; }</span>
其大小为:空对象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小 (4byte)=17byte。但是因为Java在对对象内存分配时都是以8的整数倍来分,因此大于17byte的最接近8的整数倍的是24,因此此对象的大小为24byte。
5、这里需要注意一下基本类型的包装类型的大小。因为这种包装类型已经成为对象了,因此需要把他们作为对象来看待。包装类型的大小至少是12byte(声明一个空Object至少需要的空间),而且12byte没有包含任何有效信息,同时,因为Java对象大小是8的整数倍,因此一个基本类型包装类的大小至少是16byte。这个内存占用是很恐怖的,它是使用基本类型的N倍(N>2),有些类型的内存占用更是夸张(随便想下就知道了)。因此,可能的话应尽量少使用包装类。在JDK5.0以后,因为加入了自动类型装换,因此,Java虚拟机会在存储方面进行相应的优化。
四、Java中参数传递问题
(一)论点:Java中只有值传递。
1、基本类型,传的是值,所以不会变。String的效果与基本类型一致。
2、引用类型,传的是地址值,所以会变化。
(二)分析:
1、Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。
- 基本类型的变量保存原始值,即:他代表的值就是数值本身;
- 基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress
- 引用类型的变量保存引用值(地址值)。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址位置。
- 引用类型包括:类类型,接口类型和数组。
2、【栈】是运行时的单位,而【堆】是存储的单位。
- 【栈】解决程序的运行问题,即程序如何执行,或者说如何处理数据,调用方法;
- 【堆】解决的是数据存储的问题,即数据怎么放、放在哪儿。
3、程序运行永远都是在【栈】中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。
4、Java在方法调用传递参数时,因为没有指针,所以它都是进行传值调用(这点可以参考C的传值调用)。
5、 在【运行栈】中,基本类型和引用的处理是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到【堆】中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是【堆】中的数据。所以这个修改是可以保持的了。
6、对象,从某种意义上说,是由基本类型组成的。可以把一个对象看作为一棵树,对象的属性如果还是对象,则还是一颗树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值本身都是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个对象引用),则可以修改这个节点下面的所有内容。
补充:方法重载
1、特点
1)与返回值类型无关,只看方法名和参数列表。
2)在调用时,虚拟机通过参数列表的不同来区分同名方法。