JVM(二)内存

来源:互联网 发布:windows远程登录工具 编辑:程序博客网 时间:2024/06/05 00:36

上一篇文章中,我在讲到jvm虚拟机的时候,想到了其中比较重要的几个内存的概念,觉得在这里有必要说一下。

1.jvm内存
JAVA的JVM的内存可分为3个区:栈(stack)、堆(heap)和方法区(method)

2.栈(stack)和堆(heap)
栈的概念常见的有一下3种
1).在数据结构中它是一种数据的存放方式,遵循后进先出的原则。
2).就是调用栈,首先调用main方法,里面需要生成一个Student的实例,于是又调用Student构造函数。在构造函数中,又调用到setName方法,实际的执行顺序是从上到下的。直至完成整个调用栈,返回最后的结果
调用栈LIFO
栈中的每一个元素就被称为栈帧

3).第三个就是这里要说的,用来存放数据的一种内存区域,程序在跑起来的时候需要的堆栈区。

其中栈堆的主要区别是:stack是有结构的,每个区块按照一定次序存放,可以明确知道每个区块的大小;heap是没有结构的,数据可以任意存放。因此,stack的寻址速度要快于heap

public void Method1(){    int i=4;    int y=2;    class1 cls1 = new class1();}

三个变量:i, y 和 cls1。
i和y的值是整数,内存占用空间是确定的,而且是局部变量,只用在Method1区块之内,不会用于区块之外。
cls1也是局部变量,类型为指针变量,指向一个对象的实例。指针变量占用的大小是确定的,但是对象实例以目前的信息无法确知所占用的内存空间大小。
(这里也就是说cls1 变量会分配在栈中,但是具体的实例化对象会在堆中,但是cls1 这个变量的大小是可以确定的,尽管它所指向的对象并不确定)

对象实例在内存

方法运行结束,整个stack被清空,i、y和cls1这三个变量消失,因为它们是局部变量,区块一旦运行结束,就没必要再存在了。而heap之中的那个对象实例继续存在,直到系统的垃圾清理机制(garbage collector)将这块内存回收。因此,一般来说,内存泄漏都发生在heap,即某些内存空间不再被使用了,却因为种种原因,没有被系统回收。

栈存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(int, byte, short, long, double,float,boolean, char)和对象引用。

可见,垃圾回收GC是针对堆Heap的,而栈因为本身是FILO - first in, last out. 先进后出,能够自动释放。 这样就能明白到new创建的,都是放到堆Heap!

还有一点就是需要特别注意的就是,栈的一个特殊性:

int a = 3;int b = 3

在第一行的时候会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值…在这一点上和对象的引用是很不一样的。

另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间


栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

堆区:
1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

方法区:
1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
为了更清楚地搞明白发生在运行时数据区里的黑幕,我们来准备2个小道具(2个非常简单的小程序)。

一般来说,每个线程分配一个stack,每个进程分配一个heap,也就是说,stack是线程独占的,heap是线程共用的。此外,stack创建的时候,大小是确定的,数据超过这个大小,就发生stack overflow错误,而heap的大小是不确定的,需要的话可以不断增加

3.三者的一个例子说明
AppMain.java

 public   class  AppMain{//运行时, jvm 把appmain的信息都放入方法区public   static   void  main(String[] args)  {//main 方法本身放入方法区Sample test1 = new  Sample( " 测试1 " );   //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面Sample test2 = new  Sample( " 测试2 " );test1.printName();test2.printName();}} 

Sample.java

public   class  Sample{//运行时, jvm 把appmain的信息都放入方法区/** 范例名称 */private  name;//new Sample实例后, name 引用放入栈区里,  name 对象放入堆里    /** 构造方法 */    public  Sample(String name){        this .name = name;    }    /** 输出 */    public   void  printName(){//print方法本身放入 方法区里。        System.out.println(name);    }} 

执行过程

系统收到了我们发出的指令,启动了一个Java虚拟机进程,这个进程首先从classpath中找到AppMain.class文件,读取这个文件中的二进制数据,然后把Appmain类的类信息存放到运行时数据区的方法区中。这一过程称为AppMain类的加载过程。
接着,Java虚拟机定位到方法区中AppMain类的Main()方法的字节码,开始执行它的指令。这个main()方法的第一条语句就是:
Sample test1=new Sample(“测试1”);
语句很简单啦,就是让java虚拟机创建一个Sample实例,并且呢,使引用变量test1引用这个实例。貌似小case一桩哦,就让我们来跟踪一下Java虚拟机,看看它究竟是怎么来执行这个任务的:
1、 Java虚拟机一看,不就是建立一个Sample实例吗,简单,于是就直奔方法区而去,先找到Sample类的类型信息再说。结果没找到,这会儿的方法区里还没有Sample类。Java虚拟机立马加载了Sample类,把Sample类的类型信息存放在方法区里。
2、 资料找到就开始工作了。Java虚拟机做的第一件事情就是在堆区中为一个新的Sample实例分配内存, 这个Sample实例持有着指向方法区的Sample类的类型信息的引用。这里所说的引用,实际上指的是Sample类的类型信息在方法区中的内存地址。
3、 在JAVA虚拟机进程中,每个线程都会拥有一个方法调用栈,用来跟踪线程运行中一系列的方法调用过程,栈中的每一个元素就被称为栈帧,每当线程调用一个方法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、局部变量和运算过程中的临时数据。OK,原理讲完了,就让我们来继续我们的跟踪行动!位于“=”前的Test1是一个在main()方法中定义的变量,可见,它是一个局部变量,因此,它被会添加到了执行main()方法的主线程的JAVA方法调用栈中。而“=”将把这个test1变量指向堆区中的Sample实例,也就是说,它持有指向Sample实例的引用。
OK,到这里为止呢,JAVA虚拟机就完成了这个简单语句的执行任务。参考我们的行动向导图,我们终于初步摸清了JAVA虚拟机的一点点底细了!
接下来,JAVA虚拟机将继续执行后续指令,在堆区里继续创建另一个Sample实例,然后依次执行它们的printName()方法。当JAVA虚拟机执行test1.printName()方法时,JAVA虚拟机根据局部变量test1持有的引用,定位到堆区中的Sample实例,再根据Sample实例持有的引用,定位到方法去中Sample类的类型信息,从而获得printName()方法的字节码,接着执行printName()方法包含的指令。

参考如下:
http://www.cnblogs.com/dingyingsi/p/3760730.html
http://www.ruanyifeng.com/blog/2013/11/stack.html

0 0
原创粉丝点击