Java虚拟机总结(一)自动内存管理机制

自动内存管理机制

       C、C++没有自动内存管理机制,因此C、C++开发需要考虑每一个对象生命从开始到终结,而对于Java开发来说,虚拟机有一套自动内存管理机制,不需要手动释放对象,不容易出现内存泄漏与内存溢出。

      Java在执行程序的时候会把它管理的内存划分为若干个不同的数据区域,这些区域有的线程隔离,有的为所有线程共享。

      Java 虚拟机定义了若干种程序运行期间会使用的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁,另一些则与线程一一对应,随着线程的开始而创建,随着线程的结束而销毁。具体的运行时数据区如下图所示:

线程隔离的区域(也就是线程私有):虚拟机栈、本地方法栈、程序计数器

所有线程共享的区域:方法区、堆

     程序计数器

        程序计数器为什么是线程私有的呢,先说程序计数器的作用。它可以看作是当前线程所执行的字节码行号指示器,字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都需要程序计数器。

       Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,因此程序计数器所占的一小块内存空间为线程私有区域。

     若线程执行的是Java方法,计数器记录当前执行的虚拟机字节码指令的地址;若执行Native方法(本地机器码来执行),这个计数器值为空。

     程序计数器是唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域

     Java虚拟机栈

       Java虚拟机栈生命周期与线程相同,是Java方法执行的内存模型。每个方法在执行时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法从调用到执行完成的过程,就对应着栈帧在虚拟机栈入栈到出栈的过程。

       局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

       其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间(Slot),其余的数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

      Java 虚拟机栈既允许被实现成固定的大小,也允许根据计算动态来扩展和收缩,如果采用固定大小的话,每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。在 Java 虚拟机栈中会发生两种异常,这个在虚拟机规范中有指出:

  • 如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出 StackOverflowError 异常;
  • 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的 Java 虚拟机栈,那么虚拟机将会抛出 OutOfMemoryError 异常。
     

       本地方法栈与虚拟机栈类似,不过是为Native方法服务,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemory Error异常。

     Java堆

       Java堆是Java虚拟机所管理内存中最大的一块,这块区域被所有线程共享,在虚拟机启动时创建。此区域存放对象实例以及数组,是垃圾收集器管理的主要区域,也被称作“GC堆”。为了更好更快回收内存,Java堆还可细分为新生代和老生代。Java堆可以处于物理上不连续的内存空间中,只要是逻辑上连续的即可。根据垃圾回收器的规则,可以对Java堆进行进一步的划分,具体的Java堆内存结构如下图所示:

       Java堆可以进一步划分为新生代和老年代两个模块。在新生代中,可以分为Eden空间、From Survivor0空间(s0)To Survivor1空间(s1)Survivor空间有一个为空,用以存放发生GC时的活对象。老年代存放的是经过多次Minor GC仍然存活的对象或者是一些大对象,FGC就是发生在老年代。

       Java堆中的各空间大小是可以动态控制的:

  • -Xms: JVM启动时申请的初始Heap值,默认为操作系统物理内存的1/64,例如-Xms20m;
  • -Xmx: JVM可申请的最大Heap值,默认值为物理内存的1/4,例如-Xmx20m,我们最好将-Xms和-Xmx设为相同值,避免每次垃圾回收完成后JVM重新分配内存;
  • -Xmn: 设置新生代内存大小,-Xmn是将NewSize与MaxNewSize设为一致,也可以分别设置这两个参数。

Java堆会发生OOM(OutOfMemoryError)异常,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清楚这些对象,则在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

/**
* VM Args:-verbose:gc
           -Xms20m 
           -Xmx20m
*/
public class HeapOOM {
	
	static class OOMobject{};

	public static void main(String[] args) {
		List<OOMobject> list = new ArrayList<HeapOOM.OOMobject>();
		while(true)
		{
			list.add(new OOMobject());
		}

	}

}

运行结果图:

[GC (Allocation Failure)  5282K->3689K(19968K), 0.0048825 secs]
[GC (Allocation Failure)  9230K->8272K(19968K), 0.0059946 secs]
[Full GC (Ergonomics)  17069K->12834K(19968K), 0.1863872 secs]
[Full GC (Ergonomics)  16395K->16347K(19968K), 0.1798495 secs]
[Full GC (Allocation Failure)  16347K->16335K(19968K), 0.1495128 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.ArrayList.grow(Unknown Source)
	at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
	at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
	at java.util.ArrayList.add(Unknown Source)
	at HeapOOMTest04.HeapOOM.main(HeapOOM.java:14)

     方法区

       方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,这区域的内存回收目标主要针对常量池的回收和对类型的卸载。运行时常量池是方法区的一部分。Class文件除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量和符号引用的常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池具备动态性,也就是说常量不一定只有编译期才能产生,运行期间也可能将新的常量放入池中。

       方法区在虚拟机启动的时候被创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集与压缩,方法区在实际内存空间中可以不是连续的,方法区的容量,可以是固定的,也可以是随着程序的执行动态扩展,并且在不需要过多空间时自动收缩。

       对于常用的HotSpot虚拟机,在JDK1.8之前,方法区也被称为永久代,这个方法区会发生我们常见的 java .lang . OutOfMem oryError:PermGen space 异常,我们也可以通过启动参数来控制方法区的大小:

  •   -XX:PermSize 设置最小空间
  •   -XX:MaxPermSize 设置最大空间

      在 JDK1.8 之后,HotSpot 虚拟机对方法区进行了不小的改动,彻底移除了永久代,将原来存放在永久代的数据迁移至 Java 堆 或者 Metaspace,方法区被移至到了 Metaspace,字符串常量移至 Java Heap,换句话说就是 JDK1.8 开始,Metaspace 也就是我们所谓的方法区,为什么要做这个改变呢?也许是基于以下两点原因:

  1. 由于 PermGen 内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM
  2. 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。

     我们也可以通过设置参数来控制 Metaspace 的空间大小,主要有以下几个命令:

  • -XX:MetaspaceSize :分配给类元数据空间(以字节计)的初始大小。MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
  • -XX:MaxMetaspaceSize: 分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。
  • -XX:MinMetaspaceFreeRatio:表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最小比例,不够就会导致垃圾回收。
  • -XX:MaxMetaspaceFreeRatio:表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最大比例,不够就会导致垃圾回收。

对象的创建

       虚拟机遇到new指令时,先在常量池中找到该指令参数对应的一个类的符号引用,并检查该类是否已被加载、解析、初始化过。对象所需内存的大小在类加载后便可确定,虚拟机为对象分配内存有两种方式,一种是“指针碰撞”,假设堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是将指针向空闲空间挪动一段与对象大小相等的距离;另一种是“空闲列表”,假设堆中的内存不是规整的,已使用与空闲内存交错,虚拟机会维护一个列表,记录哪些内存块是可用的,分配内存时在列表中找到一块足够大的空间,并更新列表记录。

      内存分配完成,虚拟机需要将分配到的内存空间都初始化为零值,然后会对对象进行必要的设置,对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。根据虚拟机当前的与运行状态的不同,如是否启用偏向锁等。

对象的内存布局

       在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充。

       HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定它是哪个类的实例。如果对象是一个Java数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通对象的元数据信息确定对象的大小,但是从数组的元数据中无法确定数组的大小。

      实例数据是程序代码中所定义的各种类型字段内容;对齐填充起着占位符的作用,HotSpot的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。

对象的访问定位

      Java程序需要通过栈(虚拟机栈的局部变量表部分)上的reference引用数据来操作堆上的具体对象,有两种主流方式通过引用定位、访问堆中的对象的具体位置,分别为句柄直接指针

      句柄:Java堆中会划分出一块内存来作为句柄池,reference中存储对象的句柄地址,句柄中包含对象实例数据与类型数据各自具体地址信息。优势:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,reference本身不需要修改。

                             

      直接指针:reference中存储的直接就是对象地址,Java堆考虑如何放置访问类型数据的相关信息。优势:速度更快,节省了一次指针定位的时间开销。

                             

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值