Java

java虚拟机

2018-07-17  本文已影响2人  Java架构技术分享

Java内存模型


程序计数器

内存中较小的内存空间,通过计数器的值可以选取下一条执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

线程私有,生命周期跟线程相同。

如果正在执行一个Native方法,那么这个计数器值将为空。

虚拟机栈

线程私有,生命周期跟线程相同。

每个方法在执行同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

在Java虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;

如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

跟虚拟机栈所发挥的作用相似,区别在于虚拟机栈为虚拟机执行Java(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

Java堆

用于存放对象实例,是Java虚拟机所管理的内存中最大的一块,同时也是所有线程共享的一块内存区域。

因为Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC"堆。由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为

新生代

老年代

永久代(永久代是Hotspot虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西。在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间。)

当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。

新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间:

Eden

From Survivor

To Survivor

方法区

与Java堆一样,各个线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。

运行时常量池

方法区的一部分,用于存放编译器生成的各种字面量和符号引用。

运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

直接内存

在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方法,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

堆外内存之 DirectByteBuffer 详解

HotSpot虚拟机对象

对象的创建

在语言层上,创建对象通常仅仅是一个new关键字而已,而当虚拟机遇到一条new执行时,将由一下步骤:

检查类是否加载、解析、初始化过,没有则先执行相应的类加载过程。

在堆中分配内存

划分可用空间:

指针碰撞:堆内存规整

空闲列表:堆内存不规整

并发问题

同步:采用CAS配上失败重试的方式保证更新操作的原子性

把内存分配动作按照线程划分在不同的空间之中进行

将分配到的内存空间都初始化零值

设置对象的类实例、元数据、哈希码、GC分代年龄等信息。

执行方法

对象的内存布局

对象在内存中储存的布局可以分为3块区域:

对象头

对象运行时数据、哈希码、GC分代年龄、锁状态标记、线程持有的锁、偏向线程ID等

类型执行:即对象执向它的类元数据的指针,指明对象数据哪个类的实例。

实例数据

对象真正存储的有效信息

对齐填充

占位符作用

对象的访问定位

句柄定位

直接指针

垃圾收集器

程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。

判断对象是否死亡

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1;引用时效时,计算器值就减1;当计数器值为0的对象就是不可能再被使用的。

当两个对象相互引用时,此时引用计数器的值永远不为0,导致无法对它们进行垃圾回收。

publicclass ReferenceCountingGC { publicObject instance = null; public staticvoid testGC() {            ReferenceCountingGC objA = new ReferenceCountingGC();            ReferenceCountingGC objB = new ReferenceCountingGC();            objA .instance = objB ;            objB .instance = objA ;            objA = null;            objB = null;                        System.gc();        }    }

可达性分析算法

以GC Roots为起始点,从这些节点开始向下搜索,能够搜索到的对象都是存活的,不可达的对象则为不可用。

在Java语言中,可作为GC Roots的对象包括下面几种:

虚拟机栈中引用的对象

方法区中静态属性引用的对象

方法区中常量引用的对象

本地方法栈中Native方法引用的对象

引用类型

无论是引用计数算法还是可达性分析算法判断对象是否存活都与引用有关。在JDK1.2之后,Java对引用的概念进行了扩充,划分为强度不同的四个的引用类型。

强引用

通过new来创建对象的引用类型,被强引用的对象永远不会被垃圾收集器回收。

Object obj = new Object();

软引用

通过SortReference类来实现,只有在内存不足的时候才会被回收。

Objectobj =newObject();    SoftReference sr =newSoftReference(obj);    obj =null;

弱引用

通过WeakReference类来实现,只能存活到下一次垃圾收集发生之前。

Objectobj =newObject();    WeakReference wr =newWeakReference(obj);    obj =null;

WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。

private static class Entry extends WeakReference implements Map.Entry

Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采取的是分代缓存,经常使用的对象放入 eden 中,而不常用的对象放入 longterm。eden 使用 ConcurrentHashMap 实现,longterm 使用 WeakHashMap,保证了不常使用的对象容易被回收。

publicfinalclassConcurrentCache{privatefinalint size;privatefinalMap eden;privatefinalMap longterm;publicConcurrentCache(int size) {this.size = size;this.eden = new ConcurrentHashMap<>(size);this.longterm = new WeakHashMap<>(size);    }publicVget(K k) {        V v =this.eden.get(k);if(v ==null) {            v =this.longterm.get(k);if(v !=null)this.eden.put(k, v);        }returnv;    }publicvoid put(K k, V v) {if(this.eden.size() >= size) {this.longterm.putAll(this.eden);this.eden.clear();        }this.eden.put(k, v);    }}

虚引用

也称为幽灵引用或者幻影引用,是最弱的一种引用关系。

通过PhantomReference类来实现,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

Objectobj =newObject();    PhantomReference wr =newPhantomReference(obj,null);    obj =null;

垃圾收集算法

标记清除

算法分为“标记”跟“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象。

不足:

效率问题,标记跟清除两个过程的效率都不高

空间问题,标记清除之后会产生大量不连续的内存碎片。

复制算法

将内存分为大小相等的两块,每次只使用其中的一块,当这块内存用完了,就将还存活的对象负责到另一块上面,然后再把一是要难过过得内存空间一次清理掉。

不足:

代价太高,只使用一半内存。

标记整理算法

首先标记出所有需要回收的对象,然后将所有存活的对象都向一端移动,最后清理掉端边界以外的内存。

分代收集算法

根据对象的存活周期将内存划分为几块。一般将Java堆分为新生代跟老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

新生代:复制算法

老年代:标记整理或标记清除算法。

上一篇下一篇

猜你喜欢

热点阅读