最近的项目中,出现了内存和性能的问题,需要优化,所以趁着这个机会,把自己关于java虚拟机的东整理一下,不对的地方,欢迎指出。
数据类型,因为在java的优化的过程中,检测到的数据类型一般比较的基础,毕竟复杂的数据类型就是有基础的组合而来的。
Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,表示的是数据本身的值,是数据中的最基础的部分,一般包含:
byte,short,int,long,char,float,double,Boolean,returnAddress
其中的byte,short,int,long,char,float,double,Boolean 就不多说了,returnAddress 是会被Java虚拟机的jsr、ret和jsr_w指令所使用。return Address类型的值指向一条虚拟机指令的操作码。与前面的几种类型不同,returnAddress类型在Java语言之中并不存在相应的类型,也无法在程序运行期间更改returnAddress类型的值。
引用类型的变量保存引用的值,这个就是门牌,对应一个屋子,但是保存的是屋子的地址,也就是变量在内存中存储的首地址,一般包括:
类引用,接口引用,数组
堆和栈 这个比较的重要的概念,很有必要说一下。
程序运行的角度来说,一个程序能够运行起来,主要分为数据和逻辑,对应的就是堆和栈的对应关系。
栈,是程序运行时的逻辑存储的东西,比较标识方法的栈帧,表示程序运行时的单元。
堆,是程序运行时数据存储的地方,比如新建的对象,表示程序存储的单元。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;
堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等组合而成的栈帧;而堆只负责存储对象信息。
为什么这么设计?当初设计的JVM规范的那些大牛,为什么会把这两者区分开,我们只能根据自己的推测,来猜想一二了。
① 符合面向对象的理念,把变化的和和不变得分离(总感觉有太极中阴阳的感觉),我们可以知道面向对象和面向过程的程序在执行上或者说针对需求的满足上面没有什么区别,但是面向对象的引用更加的贴近自然的思考方式。我们在编写程序的时候,仔细的想想能够知道我们编写的类的成员的变量,对象的属性就是存放在堆中,但是方法就是存储在栈中,这样我们在编写对应对象的数据结构的时候,实际上就是处理了数据和对应的逻辑,实现了太极中统一。仔细想想面向对象的这种设计比较的美。
② 从软件设计的角度讲:栈代表的处理逻辑,而堆代表的是处理的数据,这样就把处理什么,和怎么处理分开了,是的处理的逻辑更加的清晰,这种模块化的,隔离的思想在软件设计的方方面面都有体现。
③ 堆和栈的分离,不仅使栈对应的线程能够更好的关注自己的处理逻辑,还能够使堆中的内容可以被对多个栈共享,可以理解多个线程访问同一个对象,这种情况提供了一种有效的数据交互的方式,比如共享数据。另一方面也节约了空间,当然线程安全也是一个问题。
④ 栈因为运行的需要,需要保存运行时的上下文,需要进行地址段的划分,栈中对数据的引用只是一个内存的首地址,所以这样的话,堆中的对象就能够根据需要动态的增长。
堆和栈分离了,那么堆中存储的是什么,栈中存储的又是什么?
堆中存的是对象,栈中存储的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说可以动态变化的,但是栈中对应的只存了一个引用,4个byte,所以说java程序理论上面有实例数的限制。
为什么把基本类型放在栈中,一个原因是因为占有的空间小,本身占用的空间就小,放在堆中有需要加上四个字节的引用,划不来,再说基本类型的的长度是固定的,不会出现动态的增长。
现在基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在逻辑处理的时候,能够保持一致,但是基本类型和对象引用,对象本身是有区别的,这个时候对应的一个基本的问题是,java中参数的传递问题:
Java中的参数传递时传值呢?还是传引用?
要说明这个问题,先要明确两点:
1. 不要试图与C进行类比,Java中没有指针的概念。
2. 程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。
因此java的方法的调用,都是传值调用,但是传引用调用的错觉是怎么造成的:
在运行栈中,基本类型和引用的处理是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是堆中的数据。所以这个修改是可以保持的了。
对象,从某种意义上说,是由基本类型组成的。可以把一个对象看作为一棵树,对象的属性如果还是对象,则还是一颗树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值本身都是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个对象引用),则可以修改这个节点下面的所有内容。
这一段是摘抄自网上的,感觉说的比较的好。
java中,栈的大小可以通过-Xss来设置,当栈中存储的数据比较多时,可以适当的调大这个值,否则会出现 java.lang.stackoverflowerror的异常,常见的出现的异常是循环递归的情况。堆的大小可以通过-Xmx3/ –Xms设置,后面我们详细的说。
说完了存储的结构,再说一下一个java对象到底有多大?
基本的数据类型是固定的,就不说了,在java中,一个空的Object对象的大小是8byte,并且这个大小事保存堆中一个没有任何属性对象的大小,如果包括引用,也就是Object instance = new Object(); 就是12byte,其中的4byte就是引用所占的空间。
Class Student{
int nmber;
boolean isGood;
Object teacher;
}
大小为:8(空的Object) + 4(int的大小)+1(boolean的大小)+4(teacher引用的大小) = 17byte,因为java中对对象内存分配的时候都是以8的整数倍来分的,依次分配为24,这个对象的大小就是24byte。
另外关于引用的,就是java中引用的分类:
强引用,软引用,弱引用,虚引用
各个类别,就不详细说了,因为在优化的过程中,我们的代码一般采取的就是强引用,软引用和弱引用一般的在处理缓存的逻辑中用到。