java虚拟机之初探

前言        

        我们使用java语言编写的代码是在java虚拟机上运行的,但是,java虚拟机不只是仅仅可以运行java编写的代码,还可以运行其他能够编译成Class文件的语言编写的代码,如JRuby, Groovy等,关于Class文件结构后面会有相关文章来讲。

虚拟机内存

        java虚拟机在运行java程序的时候,会把它所管理的内存划分为几个区域,各个区域都有自己的用途,以及创建和销毁的时间,java虚拟机将它所管理的内存划分为以下几个运行时数据区域:

其中,方法区,堆是所有线程共享的;而虚拟机栈,本地方法栈,程序计数器是线程私有的。下面将介绍每个区域的作用:

程序计数器

         程序计数器是一小块内存空间,它可以看做是当前线程所执行的字节码的行号指示器,在任意时刻,一个处理器只会执行一条指令,所以在多线程运行的时候,为了保证线程能恢复到原来的执行位置,每个线程都需要一个独立的计数器来标记当前执行的位置,以便下次能够恢复到该位置;

         比如两个线程A, B在执行一个java方法test,当线程A执行到该方法第10行的时候,虚拟机把处理器分配给了B,这时B在执行该方法,执行到第20行的时候,又把处理器分配给了A,这时A不可能重头执行,只是接着第10行往下执行,但是它怎么知道从第几行接着执行呢?这就是程序计数器所做的事情了,当把处理器分配给B的时候,程序计数器就会记下当前执行的行数,当下次获的处理器的时候,会接着程序计数器标记的位置往下执行。

请注意,程序计数器并不会记录代码行数,而是记录正在执行的虚拟机字节码指令的地址。各个线程之间的计数器相互不影响,独立存储,它是一个线程私有的内存。

虚拟机栈

        和程序计数器一样,java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量,操作数栈,动态链接和方法出口等信息。每个方法的调用过程,对应着一个栈帧在虚拟机栈中的入栈,出栈过程。

        在虚拟机栈中存放了编译期可知的各种基本类型(boolean, byte, char, short, int, long, float, double)和对象引用。在虚拟机栈中,可能会出现两种异常:StackOverflowError和OutOfMemoryError,当线程请求的栈的深度大于虚拟机所允许的深度的时候,就会抛出StackOverflowError异常,如递归的次数过多就会出现该异常;还有一种就是在申请内存时候,内存不够时就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈与虚拟机栈的作用非常相似,区别就是,虚拟机栈执行java方法,本地方法栈执行的是本地Native方法,而在虚拟机规范中对本地Native方法使用的语言,数据结构并没有强制的规定。具体的虚拟机可自由实现。

堆 Heap

java 堆是java虚拟机所管理的内存中最大的一块,java堆是所有线程共享的一块内存区域,它在虚拟机启动的时候创建,该区域唯一的目的就是存放对象的实例,几乎所有的对象实例都在这里分配。

扫描二维码关注公众号,回复: 1501741 查看本文章

此外,java堆还是垃圾收集器管理的主要区域,从内存回收的角度来看,由于现在的收集器基本都采用分代收集算法,所以java堆可以细分为新生代和老年代,新生代又可以分为Eden区和两个Survivor。从内存分配的角度来看,线程共享的java堆可能划分出多个线程私有的分配缓冲区(LTAB)。

java堆在物理上可以是不连续的,在逻辑上连续即可。

方法区

方法区和java堆一样,也是所有线程共享的,方法区主要存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

方法区和除了和java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集器,该区域的内存回收主要是针对常量池的回收和类型的卸载。

在方法区中有个称为运行时常量池的区域,该区域主要用于存放编译期生成的各种字面常量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

直接内存

直接内存并不是java虚拟机运行时数据区的一部分,这部分内存区域也会被频繁调用,也会出现OutOfMemoryError异常,直接内存的分配不会受到java堆大小的限制,但是会受到服务器内存的限制;在操作NIO的时候,可以直接分配直接内存,这样可以显著提高性能,因为避免了在java堆和Native堆中来回复制数据。

对象的创建

在知道了java虚拟机内存的大致划分以后,接下来就应该了解对象在虚拟机是如何被创建的,是如何分配的,对象是如何被访问的。

对象的创建

当虚拟机遇到new指令的时候,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载,解析和初始化过,如果没有,则会进行类的加载过程,类的加载过程会面后详细讲解,

当类检查通过后,虚拟机就会为新生的对象分配内存,对象所需的内存大小在类加载完成后便可完全确定。内存的分配主要有两种方式:指针碰撞和空闲链表,采用哪种分配方法主要取决于java堆内存是否规整,而java堆内存是否规整又取决于java堆采用的垃圾收集器,如在使用Serial, ParNew等带有Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS等基于Mark-Sweep算法的收集器时,通常采用空闲链表的方式。

指针碰撞

当java堆中的内存是规整的,所有已分配的内存在一边,未分配的内存在一边,中间的分界点有个指针,为对象分配内存,就是把指针向空闲的内存移动一段与对象大小相等的距离即可。

空闲链表

当java堆中的内存是不规整的,已分配的内存和空闲的内存相互交错,空闲的内存在物理上不是连续的,这时虚拟机就必须维护一个列表,用于记录哪些内存是可用的,在分配的时候从列表中找到一块足够大的内存分给对象

当java虚拟机为对象分配内存后,还要对其进行必要的设置,如该对象是哪个类的实例,如何才能找到该类的元数据信息,对象的哈希吗,对象的GC分代年龄等信息。

对象的访问定位

当对象创建后,我们可以通过栈上的引用来操作堆上的具体对象,而不同的虚拟机实现有不同的操作方式,主要有两种:使用句柄和直接指针。

使用句柄

如果使用句柄访问,则java堆将会划分一块内存作为句柄池,栈中的引用存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如下图所示:

该方式的优点主要是栈中的引用存储的是稳定的句柄,在对象移动的时候,只会改变句柄中实例数据指针,栈中的引用不需要改变。

直接指针

使用直接指针时,java堆对象的布局中就必须考虑如何放置访问类型数据相关的信息,而栈中的引用存储的直接就是对象的地址,如下图所示:

使用直接指针的优点是速度快,它节省了一次指针定位的时间,对于HotSpot虚拟机而言,主要采用的是第二种方式。

垃圾收集器

在编写java代码的时候,我们不需要手动的进行内存的回收,java虚拟机在内存不足的情况下会自动进行回收,而虚拟机进行这项工作的就是垃圾收集器(Garbage Collection, GC)。

在上面分析了java虚拟机运行时数据区的内存分布可知,程序计数器,虚拟机栈,本地方法栈这三块内存是线程私有的,随着线程而生,随着线程而灭;栈中的栈帧随着方法的进入和退出有条不紊的执行出栈入栈操作,每一个栈帧中分配多少内存基本上在类结构确定下来时就是是已知的,因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑内存回收问题;而java堆和方法区则不一样了,一个接口的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序运行的时候才能知道创建哪些对象,这部分内存的分配和回收都是动态的。垃圾回收主要是回收这部分区域。

哪些内存需要回收

垃圾收集器在回收内存的时候,首先要考虑一个问题:哪些内存需要回收,怎么判断?

在java堆中存放的都是java实例对象,收集器在对堆回收之前,首先要判断那些的对象是活着的,哪些对象不再使用了,已经变成垃圾了,内存回收就是要清除这部分垃圾。

判断对象存活的方法

1.引用计数法

给对象添加一个引用计数器,每当对象被引用的时候,计数器就加1,当引用失效的时候,计数器减1,当计数器变为0的时候,该对象就变为垃圾对象,也就是即将被回收的对象,

当A到B的引用失效后,B的计数器变为0,因此B就可以被回收了,且B连接上了空闲链表,能够再次被利用;而又产生了新的引用A到C,所以C的计数器变为2.

优点

可立即回收垃圾,每个对象都知道自己被引用的次数,当引用次数变为0的时候,对象马上就会把自己作为空闲空间连接到空闲链表中去,能够被再次分配利用,也就是在对象变成垃圾的同时就立刻被回收,这样一来内存空间就不会被垃圾占领。

在其他的GC算法中,即使对象变成了垃圾,虚拟机也无法立刻判断,只有当在虚拟机进行内存分配时,发现内存不够后进行GC,才知道哪些对象是垃圾对象,哪些对象不是垃圾对象,也就是说在进行GC之前,都会有一部分内存被垃圾占用。

缺点

1.计数器增减处理繁重

2.计数器需要占用很多位,降低内存空间的利用率

3.实现复杂

4.循环引用无法回收。

2.可达性分析算法

可达性算法的基本思想:通过一系列的称为“GC Roots”的对象作为起点,从这些起点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链的时候,则该对象是可回收的。如下图所示:

Object5,Object6, Object7虽然是相连的,当时没有与GC Roots连同,所有这几个对象被判定为可回收的。

java中,可作为GC Roots的对象:

1.虚拟机栈中引用的对象

2.方法区中,类静态属性引用的对象

3.方法区中,常量引用的对象

4.本地方法中JNI(Native方法)引用的对象

java中的引用

java中对象的引用分为四种:强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference)和虚引用(Phantom Reference),引用强度依次减弱。

强引用(Strong Reference)

当我们在new 一个对象的时候,该引用就是一个强引用,只要强引用存在,垃圾收集器永远不会回收被引用的对象。

软引用(Soft Reference)

软引用用来描述一些还有用但非必须的对象,软引用关联的对象,在系统发生内存溢出之前,会把这些对象列回回收范围进行二次回收,如果这次回收后,内存还是不够,则才会发生内存溢出;可以使用 SoftReference类来实现软引用。可以使用软引用来实现缓存。

在内存充足的情况下,可是获取到值:

	public static void main(String[] args) {
		
		SoftReference<Person>[] softRefer = new SoftReference[10000];
		for(int i = 0; i < softRefer.length; i++)
		{
			softRefer[i] = new SoftReference<Person>(new Person("name " + i));
		}
		System.out.println(softRefer[100].get()); //name 100
		System.out.println(softRefer[200].get()); //name 200

	}

当加入 虚拟机参数 “-Xms1m -Xmx1m”时,在运行,就会发现,获取的为null:

	public static void main(String[] args) {
		
		SoftReference<Person>[] softRefer = new SoftReference[10000];
		for(int i = 0; i < softRefer.length; i++)
		{
			softRefer[i] = new SoftReference<Person>(new Person("name " + i));
		}
		System.out.println(softRefer[100].get()); //null
		System.out.println(softRefer[200].get()); //null
	}

弱引用(Weak Reference)

弱引用用来描述非必须的对象,被若引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器进行垃圾回收的时候,无论内存是否足够,都会回收被弱引用关联的对象。可以使用 WeakReference来实现弱引用。

		String name = "zhangsan";
		
		WeakReference<String> wr = new WeakReference<String>(name);
		System.out.println(wr.get());
		System.gc();
		System.out.println(wr.get());

虚引用(Phantom Reference)

为一个对象设置虚引用关联的唯一目的,就是在这个对象被垃圾器回收的时候收到一个系统通知,可以使用 PhantomReference来实现虚引用,一个对象是否有虚引用的存在,全完不对其生存时间有影响,也无法通过虚引用来获取一个实例。

在实际程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多。

下一篇将介绍虚拟机的垃圾收集器及其算法

     

猜你喜欢

转载自my.oschina.net/mengyuankan/blog/1825562
今日推荐