关闭

java虚拟机分析以及GC

标签: 虚拟机java内存GC
371人阅读 评论(0) 收藏 举报
分类:

今天看《深入理解java虚拟机》有点小体会,特写这篇博客
首先话不多说,看图
虚拟机你内存分析图
由图可以知道,java内存分为:方法区、虚拟机栈一、本地方法栈、堆、程序计数器这几个大块
我将从这几部分介绍内存模块:1、功能;2、异常;3、线程独立

程序计数器:是一块较小的内存空间
—–作用:当前线程所执行的字节码文件行号的指示器,字节码解释器工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,循环、跳转、异常处理、线程恢复等基础功能需要这个计数器来完成
—–异常:是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError(内存溢出错误)
—–线程独立:每条线程都需要有一个独立的程序计数器


java虚拟机栈
—–作用:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用至执行完成的过程,就对应着一个栈帧在虚拟机中入栈道出栈的过程
局部变量表介绍
—–异常:1、如果线程请求的栈深度大于虚拟机所允许的深度(在方法里面再调用方法,如此迭代调用方法),将抛出StackOverflowError
————-2、虚拟机栈可以实现动态扩展、如果无法申请到足够的空间进行扩展,将抛出OutOfMemoryError
———-异常描述:这里把异常分成两种情况,看似更加严谨,但却存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的描述而已

测试栈溢出代码:
public class JavaVMStackSOF {

    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) throws Exception {
        JavaVMStackSOF JVMSS = new JavaVMStackSOF();

        try {
            JVMSS.stackLeak();
        }catch(Exception e) {
            throw e;
        }finally {
            System.out.println("stackLength = "+ JVMSS.stackLength);
        }


    }

}

运行结果:
这里写图片描述
—–线程:线程私有
这里写图片描述


本地方法栈
—–作用:为虚拟机使用到的Native方法服务的
—–异常:同虚拟机栈一样
—–线程:线程私有


java堆
—–详细介绍:java堆是java虚拟机所管理的内存中最大的一块,被所有线程共享的一块内存区域
—–作用:存放对象实例,几乎所有的对象实例都在这里分配内存,是垃圾收集器管理的主要区域(下文讲GC会将到)
—–异常:如果堆中没有内存完成实例分配,并且堆也无法再扩展,将抛出OutOfMemeoryError异常
—–线程:是所有线程共享的一块区域


方法区
—–介绍:虽然java虚拟机规范把描述为堆中一个逻辑部分,但是它有一个别名Non-Heap(非堆),目的与java堆区分开来
—–作用:它用于存储已被虚拟机加载的类信息、常亮、静态变量、即时编译器编译后的代码等数据,也是java回收器管理的重要一部分
—–异常:当方法区无法满足内存分配需求是,将抛出OutOfMemoryError异常
—–线程:线程共享的一块区域

运行时常量池
—– Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常亮池,用于存放编译期间生成的各种字面量和符号引用。
—–作用:存放类加载后的常量
—–异常:运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常
—–线程:线程共享


讲完了java内存的分析,我们简单谈谈对象的创建
—–虚拟机遇到一条new指令时,首先会去检查这个指令的参数是否能再常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那必须先执行相应的类加载的过程(该过程在后面的博客会讲到)
—–分配内存的方式
——— 一:指针碰撞,假设java堆中内存空间是绝对规整的,所有用过的内存都在一边,没有用过的内存方法另一变,用一个指针作为一个分界点的指示器!,如果有新的对象被创建则将指针忘未用过内存方向移动相应对象大小的空间,有人会说如果在已使用的内存中有的内存已经被垃圾回收器回收了,这样已使用的内存部分就会有垃圾碎片(这种情况不存在,下文将java垃圾回收机制原理的时候会将到)
这里写图片描述
——— 二:空闲列表:使用一个列表管理内存中空闲的部分,如果有新创建了一个对象,则从空闲列表中拿出足够大的一块划分给对象实例
—–分配内存安全性方案:
———-描述:正在给A对象分配内存,指针还没有来的及修改,对象B又同时使用了原来的指针来分配内存,就会出现线程不安全的问题
———-解决方案一:分配失败重试方法,也就是说如果这个分配出现了错误,再试一次
———-解决方案二:把内存分配按照线程划分不同的空间,每个线程在自己的空间中进行对象实例分配(这部分内存称为TLAB:Thread Local Allocation Buffer),只有TLAB用完并分配新的TLAB时,才需要同步锁定这里写图片描述



现在进入对象的回收讲解
—–java最吸引人的地方之一就是不用手动回收内存。这样做的优点就是方便用户编程,减少了内存溢出的风险,缺点就是如果不知道java的回收机制就很难对OutOfMemoryError异常进行修改
—–在java的自动回收机制前,需要了解的知识就是垃圾回收机制的类别有哪些?该类别的实现原理是什么?该类别的优缺点是什么?我也这几点讲
一:垃圾回收机制类别:
———-跟踪回收:
—————1、标记清除
—————2、复制收集
———-引用计数
二:实现原理
——–跟踪回收:定期运行来检查那个是垃圾
——–标记清除:需要对程序的对象进行两次扫描,第一次从Root开始扫描,被根引用了的对象标记不是垃圾(为什么标记不是垃圾的部分,而不是标记垃那个是垃圾:据IBM的测试:在新生对象中,98%的对象只引用一次就是垃圾,所以这样节省空间),不是垃圾引用的对象同样不是垃圾,所以第一次没有标记的对象就是垃圾,然后进行第二次扫描,将没有标记的对象进行回收(涉及到可达性分析见下文描述一)
————优点:能解决循环引用的问题(可以自行百度该问题)
————缺点:中断时间较长,效率较低;导致许多空间碎片
——–复制收集:复制收集的方式只需要对象进行一次扫描,准别一个新的空间,从根开始对对象进行扫描,如果存在这个对象的引用,就把它复制到新的空间中(为什么复制不是垃圾的对象到新的空间中,与上文讲的IBM的测试有关),一次扫描后,回收这个空间,新的空间就是非垃圾的对象
————优点:能解决循环引用的问题,效率比标记清除要高
————缺点:消耗大量空间,中断时间较长
——–引用计数:针对每一个对象,保存一个该对象的引用计数器,如果对该对象增加了一个引用,引用计数器加一,否则就减一
————优点:容易实现,垃圾回收中断时间断,垃圾存在时间段
————缺点:不是解决循环引用的问题

描述一:以GC ROOT的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径为引用链,当一个对象在最后都没有被引用,则证明对象不存在引用,可以被回收
—-> 可以作为GC ROOT对象可能有
———-*虚拟机栈中引用的对象
———-*方法区中类静态属性引用的对象
———-*方法区中常量引用的独享
———-*本地方法栈中JNI引用的对象


讲完了回收算法的实现原理,我们来将下衍生算法:分代回收,这也是java用到的算法
—–分代回收:大部分对象会从产生开始在很短的时间内变成垃圾,而存在很长时间的对象往往都有较长的生命周期,高频对新生成的对象回收,称为小回收,低频对所有对象回收,称为大回收,每一次回收旧把存活下来的对象归为老生代,小回收时跳过老生代
—–存在的问题:在某个新生代的对象中,存在老生代的对象对它的引用,怎么制止小回收对其回收
—–解决方法:采用写入屏障:如果存在被老生代引用,则将新生代加入到记录集中,取消对新生代该对象的回收


开始讲java的GC了,我将会从这几方面讲:GC的基本步骤?GC的对象的引用类型?GC的分代回收分为几代?以及一代的GC的特点?
—–GC的过程的基本步骤
———-1、首先确定这个对象不可达
———-2、如果对象有finalize方法,那么对象被添加到finalization队列中,然后在某个时间点调用finalize方法,用于释放finalize中的资源
———-3、回收对象所占用的内存
—–对象的引用类型
———-StrongReference(强引用类型):如果一个对象具有强引用,那就类似于不可少的生活用品,垃圾回收器绝对不会回收它,当内存空间不足时,java虚拟机情愿抛出OutOfMemoryError错误,使程序异常终止,也不会随意回收具有强引用的对象来解决内存不足问题。例如:StringBuffer buffer = new StringBuffer();其中buffer就是强引用类型
———-SoftReference(软引用类型):被这个引用指向的对象,如果没有被其他StrongReference引用的话,在任何时候都可能被GC,一般在抛出OutOfMemoryError之前被回收
———-WeakReference(弱引用类型):如果没有被StrongReference和SoftReference引用的话,立即会被GC,与SoftReference的区别是:WeakReference引用对象是被eagerly Collected收集,即一旦没有被任何StrongReference和SoftReference引用的对象,马上就能够被发现
———-PhantomReference(幻影引用):当没有被任何StrongReference和SoftReference、WeakReference引用时,随时被GC,通常和ReferenceQuere联合使用,管理和清除被引用对象相关的本地资源,没有finalize方法


GC分代回收分为:新生代、老生代、永久代


—–新生代:存放新生成的对象,新生代的目标是尽可能的快速回收掉那些生命周期短的对象,我将从新生代的分区?使用的回收算法?算法的思路是什么
———-新生代的分区:Eden区和两个survivor区(系统默认的分配比例是8:1:1),这是我用Jconsole检测我运行的一的项目,其中可以看出Eden的波动非常的大
这里写图片描述
———-主要采用的算法是复制收集算法
———-采用的思路是:
—————1、先将新生对象分配到Eden区
—————2、如果申请失败,则触发Scavenge GC对新生代的Eden区进行回收(回收掉垃圾对象),存活对象就保留在Survivor区域(其中的一个,并且永远在同一时刻是能用一个,要保留一个)。
—————3、如果一个Survivor区域满了,就启动复制回收,将存活对象保留到另一个Survivor区中,这样将先前用到了Surivor进行回收(又空出了一个Surivor区)
—————4、如果新生代的Survivor相互交换了15次(系统默认,也可以自己设置),则还存活的对象就开始保留到老生代
为了加快Eden的回收速度,系统采用了两个方式
—–Bump-the-pointer:内存连续分配,所以只要在这个区域中的,判断最后一个对象后面的区域是否满足新产生的对象的大小,如果不满足则启动Scavenge GC对对象进行回收
这里写图片描述
—–Thread-Local-Allocation-Buffer:给每个线程都分配一个Eden的一块区域,具体的为什么可以看上文的描述


老生代:存储的对象比新生代的对象多得多,一般存储到老生代的对象有两种:一个上文所讲的经过15次系统变换的对象,二是如果遇到对象暴增的情况下,新生代的90%(为什么不是100%,请看新生代的分配方式)的区域不能存储(Eden不能扩容)的情况下,部分对象会直接进行老生代(也可以进行配置)。同样是一个项目一个检测软件,看出老生代的对象基本没有变动
这里写图片描述
—–使用的算法为:标记-整理,具体的实现请看上文,这里不做过多介绍,值得注意的是:标记过程仍然与“标记-清除”算法一样,但是后续的步骤不再是直接对可回收的对象进行整理,而是让所有存活对象都向一段移动,然后直接清理掉边界以外的内存
这里写图片描述
—–这里采用的是full GC:如果一次移动到老年代的对象大于剩余区域的大小,则触发Full GC,当然也可以设当老生代的还剩余多少就触发(这样可以避免对象暴增引发的OutOfMemoryError)


持久代(好多人说方法区就是持久代、也称永久代)
—–回收两种对象:常量池中的常量、无用的类信息
—–采用的算法是:引用计数的方法回收


讲到这里基本讲我知道的虚拟机分区与GC的原理讲完了,下篇博客我会从类的加载及自定义类加载器讲解

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:3924次
    • 积分:306
    • 等级:
    • 排名:千里之外
    • 原创:24篇
    • 转载:6篇
    • 译文:2篇
    • 评论:2条
    最新评论