《深入理解JVM》第二章 JVM自动内存管理机制

版权声明:版权为ZZQ所有 https://blog.csdn.net/qq_39148187/article/details/80903841

概述

了解jvm 内存管理机制, 如果jvm 出现内存溢出,泄露的问题可以排查进行工作,

运行时数据区域

jvm 运行时会把内存进行划分,

程序计数器

是一块较小的内存,可以看作为,当前线程所执行字节码的一个行号指示器 ,字节码解释器就是通过改变计数器

来选择执行的字节码指令,分支循环,跳转,异常处理,线程恢复等功能,

    JVM 的多线程机制是通过线程的轮流执行来实现的,任何一个确定的时刻, 一个处理器(核)都会执行一条线程的指令,因此为了线程切换后,能恢复到正常的执行位置,每个线程都需要一个单独的计数器,各个线程之间互不影响,独立存储,我们称之为线程的私有内存

    如果一个线程执行的字节码文件为main方法那么他的计数器记录是执行的虚拟机字节码指令的地址,如果是Native方法, (通过JNI调用C C++) 代码,那么这个计数器指针指向空,此时内存区域是唯一一个在jvm 规范中没有规定的任何outofmemoryerror 情况的区域

JVM栈

jvm 对象的内存布局

对象在内存中存储分布可以分为三个区域,对象头,实例数据,对齐补充

对象头

第一部分用于存储对象自身的运行时数据, hashcode  gc分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳,这部分数据的长度在32 为和64 位的虚拟机中分别调式为32 bit 和64bit官网成为Mark W ord ,

对象头的另一部分是类型指针,对象指向他的类元素的指针,虚拟机通过这个指针判断这个对象是哪个类的实例化,并不是所有的实现类的信息都要存在类型指针上,查找对象的元数据,并不一定要经过对象本身,如果对象是一个java 数组,对象头中还要有一块记录数组长度的数据,因为虚拟机可以通过普通的对象的元数据信息,确定java 对象的大小, 但是数组元数据无法知道数组的大小

实例数据

对象存储有效信息,从父类继承的自己定义的都在这里面记录下来来,这部分的书讯都会受到分配策略参数和字段在java 源码中定义的顺序影响,hotspot 虚拟机默认的分配策略为longs/doubles ,ints,shorts/chars,bytes/booleans oops  相同矿都的字段总是会被分配到一起,在满足这个条件下, 父类中定义的变量可以出现在子类之前,

对其填充

不是必要存在的,占位符的作用, hotspotvm 的内存管理系统的要球对象的起始地址是8 字节的证书倍,如果没有 8 自己就用这个进行补充

对象访问定位

建立对象就是为了操作对象帮我们做事情,java 中我们操作对象,就是通过他在站内存中对堆内存中的引用操作,实现对象的操作, java程序需要通过reference 数据类型来操作堆上的具体对象,reference 值规定了一个对象的引用, 并没有定义这个应用应该通过什么方式去定位访问堆内存中的对象的具体位置, 所以对象的访问方式也是取决于虚拟机的实现而定的,比较流程的访问方式就是句柄和指针

句柄

使用句柄操作,java 堆中将分化出一块内存作为句柄池,reference中存放的就是对象的句柄地址,句柄中包含了对象的实例化数据和类型数据各自的具体地址信息

指针

如果使用指针访问,那么java 对象的布局中就要考虑到如何放置访问类型数据的相关信息,reference 中存储 的就是对象的地址

比较

句柄的好处: reference中存储的是文档的句柄地址,在对象被移动(垃圾收集时候移动对象非常普遍的行为)只会改变句柄中的实例数据指针,reference 本身不用修改

指针的好处: 访问速度块,节省了一次指针定位的时间开销,指针的定位在java 对象的访问中式非常频繁的,Hotspot 中使用的式第二种方式对对象进行访问,从软件开发的范围看,句柄的访问在各种语言框架使用也非常广泛

OutOfMemoryError 异常

java 虚拟机规范描述中,处理程序计数器之外,虚拟机的其他的几个运行时区域都可能发生OutOfMemoryError

https://zhidao.baidu.com/questio/714811846377929325.html

java 堆溢出

java 堆存储对象的实例化数据,不断的创建对象,保证gc roots 到对象之间有可能到达路径避免垃圾回收清除对象, 这样就能产生内存溢出

垃圾回收,内存分配策咯

Java 和C++ 之间又一堵高墙,墙外面的想进来,里面的想出去

GC不是java 语言的伴生产物,gc比java久远,MIT的Lisp 是一门真正使用内存动态分配和垃圾收集技术的语言,Lisp 在胚胎的时候, 就在考虑gc

那些内存要回收?

什么时候回收

如何回收

对象已死吗 ?

引用计数算法

给对象添加一个引用计数器,当有一个地方引用他就给他+1 , 如果有一个地方的引用失效就-1 ,实现简单,效率高,微软公司的COM(component object model) 技术,使用ActionScript3的Flashplaye ,python语言在游戏脚本领域被广泛使用的Squirrel 中都使用了引用计数器算法对内存进行管理,但是主流的java 虚拟机中没有采用这种算法,原因式,很难解决对象之间的互相循环引用问题

可达性算法

Java C# lisp 都是使用的可达性分析算法,判断对象是否存活,基本思路就式通过一系列成为  gc roots 的对象为起点,从节点开始往下搜索,搜索走过的路径成为引用链,当一个对象到gc root 没有任何的引用链相链接,呢就证明了这个对象是不可用的

Java 中gcroot对象包括

1. 虚拟机栈,(栈帧中的本地变量表) 中引用的对象

2. 方法区中的类静态属性引用的对象 (方法区中放常量,静态变量,类信息,编译器编译后的代码数据在hotstap 中叫做永久代)

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

本地方法栈中jni native方法引用的对象

再谈引用

在jdk1.2 以前,对象通过reference 类型的变量存储的数指,代表是另一块内存中其实的地址,这块内存代表着一个引用,这样一个对象只有两种状态引用和没有被引用,

我们要实现如果内存充足,把一些对象留在内存中,如果不充足,就回收

1.2后 java 对引用的概念扩充

Strong reference   强引用

Soft reference 软引用

Weak reference 软引用

Phantom reference  虚引用

引用强度一次减弱

1. Object oj = new object() 这种是强引用,gc永远不会回收调

2. 软引用soft reference 是描述一些还有用但并非必须用的对象,对于软引用的关联对象在系统将要发生内存溢出之前将会把这些对象进入回收范围之中进行二次回收,如果这次回收没有足够的内存会抛出内存溢出异常

3. 软引用,描述对象是非必须的,他的强度被soft 又弱一点,卑弱引用关联的对象,只能生存到下一次垃圾回收,gc 回收的时候,无论内存是否够用都给回收调

4. 虚引用,虚幻引用,是最弱的,一个对象那个是否有虚引用的存在完全不会对他的生存时间造成影响,也无法通过虚引用获取一个对象的实例化,为一个对象设置虚医用的目的是能在这个对象被收集器回收的时候收到一个系统通知

生存还是死亡?

在可达性算法中一个不可达的对象,不是非死不可,这个时候他们暂时处于缓刑截断,要真正宣布一个对象的死亡,要经过两次标记,如果这个对象在进行可达性分析之后发信啊没有于gcroots 相连接的引用链,那么他会被第一个次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize() 方法的时候,或者finalize方法被虚拟机调用过,虚拟机i将这两种情况视为没有必要执行

如果让对象视为没有必要执行的finalize方法,那么这个对象会被放在f-queue 队列中,在稍后的一个由虚拟机自动建立起来,优先级低的finalizer线程区执行他,这里的执行是指触发这个方法,但是并不承诺会等待运行结束,这样做的原因是,如果一个对象在finalize方法中执行缓慢,或者发生了死循环,会导致f-quecue 列队中的其他对象处于等待状态,这样内存回收系统就容易崩溃,finalize方法是对象逃离死亡命运的最后一次机会,稍后gc 会对f-queue中的对象进行二次小规模的标记,如果对象要在finalize 中成功拯救自己,重新于引用链链接上,自己被别的对象引用,那么他在第二次标记的时候它会被溢出列队中,如果对象没有逃脱就真的彻底被gc回收死亡

回收方法区

方法区是会被回收的,只是回收的条件比较苛刻, 常量池中的常量,类接口,方法,字段的符号引用如果没有被引用就会被清除到常量池外

方法区回收的苛刻的主要是无用类的回收

1. 该类的所有的实例都已经呗回收,在java 堆中不存在改类的任何实例

2. 加载该类的classloader类呗回收

3. 该类对应的java。Lang。Class 对象没有任何对象引用,无法在任何地方通过反射访问类的对象

满足上面的三个条件,才仅仅可以回收,不是像对象那样不使用就必然会被回收,是否堆类进行回收,jvm 提供了 参数进行操作

垃圾回收算法

标记--清除

首先年标记出需要回收的对象,在标记玩之后统一的进行回收所有被标记的对象,这个是最基础的收集算法, 之后的算法都是在他的基础上进行改造,这个算法有两个不足,效率不高,标记和清除的效率都不高,空间问题,清除之后会产生很多的不连续的内存空间碎片,这个样会导致,如果分配大的对象无法找到连续的内存进行分配,

复制算法

把内存分为两个区域,一个内存区域用完把存活的对象复制到另一个内存区域上,然后清空内存区域

现在商业虚拟机都采用这种方式回收新生代,ibm公司研究表明, 新生代的对象98%都是死的很快,所以不用按照1:1 方式进行分配内存

把内存分为一大块的Eden 和Survivor 2小块,每次使用eden 和其中一块Survivor,当回收的时候,把eden 和survivor 中存活的对象复制到另一块survivor 内存上,然后清除掉enden 和survivor 空间,hotstop默认eden和survivor 的大小比例是8:1  在整个新生代中可用内存是90% ,只有10%会被浪费,当survivor 内存不够用的时候还要依赖其他的内存进行分配担保,担保以后再说

标记整理

复制收集算法在对象存活率高的时候进行较多的复制操作,效率会变低,更关键的是,如果不想浪费50%空间,就需要有额外的空间进行份额担保,以应对使用的内存中所有的对象都是100%存货的极端条件,所以老年但不适用这种算法

根据老年代的特点有人提出另一种 标记--整理(mark-compact) 算法,标记过程和标记清除的过程算法一样, 标记之后不是清除,而是让所有的存活对象都向一端移动,然后清除端表姐以外的内存

分代收集

分代收集算法,只是根据对象存活的周期不同话分内存为几块,一般java 把堆划分为新生代,和老年代,这样就可以根据各个年代的不同选取适当的收集算法,新生代中每次垃圾收集的时候都有大批对象死去,很少数存活,所以使用复制算法,只需要付出少量存活对象的复制成本就能完成收集,老年代中因为对象存活率高,没有额外的空间对他进行分配担保,就使用标记--清理,  标记--- 整理

 

HotSpot 算法实现

枚举根节点

猜你喜欢

转载自blog.csdn.net/qq_39148187/article/details/80903841
今日推荐