• 深入理解JVM虚拟机


    一、JVM运行时数据区域


    1、程序计数器

    唯一一块没有内存溢出的区域.java中的多线程,实际上是通过切换线程来实现的,所以说任何一个时刻只有一个线程在执行,因此为了线程切换后能回到正确的位置,每条线程就必须独立存储之前执行到的位置,所以这块区域是线程私有的.

    2、虚拟机栈

    线程私有,生命周期与线程相同.虚拟机栈描述的是java执行方法的线程模型:每个方法在执行时都会在栈上创建一个栈帧,用于存放局部变量表、操作数栈、动态链接、返回出口等.每个方法的调用和结束(正常执行完或异常退出)实际上就是一个栈帧在虚拟机栈的入栈和出栈.
    当线程请求的栈的深度大于虚拟机所允许的最大深度,将抛出StackOverflowError,如果虚拟机栈容量可以扩展,当栈扩展扩展时申请不到足够内存就会OutOfMemoryError

    3、本地方法栈

    本地方法栈与虚拟机栈功能类似,虚拟机栈是为虚拟机执行java程序服务,本地方法栈是虚拟机栈执行本地方法服务.

    4、堆

    **线程共有,存放java实例对象的地方.**堆经常被划分为老年代、年轻代等等,都只是为了更好的进行回收,并不是真的有这些划分

    方法区:

    线程共有,主要存放已经被虚拟机加载的类信息、静态变量、常量等数据.
    说到方法区也就需要解释一下“永久代”的概念,其实这只是一种习惯称呼,是因为虚拟机的设计团队将分代设计扩展到了方法区,或者说是用永久代实现了方法区,这样就不用特意为方法区设计内存管理,永久代并不像其名字进入里面的对象就不会被回收,只是回收的条件比较苛刻;
    设计团队已经放弃了永久代,改用本地内存的方式,jdk1.7将字符串常量池、静态变量移出,到了jdk1.8彻底删除了永久代,改为本地内存实现的元空间代替.

    二、虚拟机对象

    1、对象的创建

    在类被加载检查通过后,接下来虚拟机就需要为新对象分配内存,内存的大小在类加载完成后就可以确定,那么分配空间实际就是在“堆”上获取一块指定大小的内存.
    假设堆是绝对规整的,已经使用的内存在一边,没有使用的在另一边,中间放一个指针当作分界点,当需要多少内存是就只需要将指针移动一段大小相同的内存距离就行,称这个为“指针碰撞”;但是如果java堆不是规整的,那么虚拟机就需要维护一个列表,上面记录了哪些内存块是可用的,称这个是“空闲列表”.
    使用哪种分配方式是由java堆是否规整,而这又是由其垃圾收集器是否有压缩整理的能力决定的.因为Serial、ParNew使用的是标记-复制算法,所以采用的是“指针碰撞”,而CMS这种是基于标记-清除算法实现的理论上就是“空闲列表”(实际上CMS为了实现更快分配,设计了一种分配缓存区,就是通过空闲列表拿到一大块内存之后,然后使用指针碰撞来实现)

    2、对象的内存布局

    对象在堆内存中的存储布局可以分为三个部分:对象头、实例数据和对齐填充.

    3、对象的访问定位

    三、OutOfMemoryError

    1、java堆溢出

    异常堆栈信息:java.lang.OutOfMemoryError,并且进一步提示“java heap space”.
    内存泄漏:
    1、打开堆转储快照文件
    2、通过工具查看泄漏对象的GC Roots引用链,找到泄漏对象是通过怎样的引用路径、与那些GC Root相关联,才导致垃圾收集器无法回收他们.
    **内存溢出:**内存中的对象确实需要存在
    1、检查堆内存的设置与机器实际内存对比,是否有上调的空间.
    2、再从代码上检查,这些对象的生命周期是否过长、持有状态过长等等.

    2、虚拟机栈和本地方法栈溢出

    1、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError.
    2、HotSpot虚拟机不允许栈的动态扩展,所以除非在创建线程时就无法获得足够的内存而出现OutOfMemoryError,否则在线程运行时是不会出现内存溢出的,只会因为栈容量无法容纳新的栈帧而抛出StackOverflowError.
    使用-Xss减少栈的内存容量;抛出StackOverflowError,并且输出的栈深度减小
    ![image.png](https://img-blog.csdnimg.cn/img_convert/c0e766657be946183f368dad84a5fa33.png#clientId=uc880d338-855c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=239&id=ud6471269&margin=[object Object]&name=image.png&originHeight=604&originWidth=858&originalType=binary&ratio=1&rotation=0&showTitle=false&size=93268&status=done&style=none&taskId=ueb16aeea-83b2-4d9c-bf39-374d0bf1e26&title=&width=339)

    public class JavaVMStackSOF {
    
        private int stackLength = 1;
    
        private void stackLeak() {
            stackLength++;
            stackLeak();
        }
    
        public static void main(String[] args) {
            JavaVMStackSOF sof = new JavaVMStackSOF();
            try {
                sof.stackLeak();
            } catch (Throwable e) {
                System.out.println(sof.stackLength);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    定义了大量本地变量,实际就是增大了本方法栈帧中本地变量表的大小

    3、方法区和运行时常亮池溢出

    使用“永久代”和“元空间”实现方法区的不同表现.
    String::intern()方法,如果字符串常量池存在一个等于该字符串的,就返回池中该对象的应用,否则就向常量池中添加该对象

    // 通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其 中常量池的容量
    // -XX:PermSize=6M -XX:MaxPermSize=6M
    public class RuntimeConstantPoolOOM { 
        public static void main(String[] args) { 
            // 使用Set保持着常量池引用,避免Full GC回收常量池行为 
            Set<String> set = new HashSet<String>(); 
            // 在short范围内足以让6MB的PermSize产生OOM了 
            short i = 0; 
            while (true) { 
                set.add(String.valueOf(i++).intern()); 
            } 
        } 
    }
    
    //运行结果,出现PerGen space说明常量池属于方法区,jdk1.8之后就不会出现
    Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    四、垃圾回收算法与垃圾收集器

    1、判断对象已死

    引用计数算法:在对象中添加一个引用计数器,每当一个地方引用就+1,当引用失效就-1,任何时刻为0就表示不可能再被使用.
    优点:虽然占用了一些内存,但是原理简单,判定效率高.
    缺点:java并未使用,因为无法解决循环引用问题,
    可达性分析算法:通过一系列的“GC Roots”的跟对象做为起始点,根据引用关系向下搜索,搜索过程的路径称为“引用链”,如果某个对象和GC Root不可连时,就认为该对象不再使用.
    GC Roots包括以下几种:
    1、虚拟机栈(栈帧中的本地变量表)中引用的对象
    2、方法区中类的静态属性引用的对象
    3、方法区中常量引用的对象
    4、本地方法栈中JNI引用的对象
    …(还有一些“临时性”加入的对象,老年代对年轻代中对象存在引用)

    2、分代收集理论基础

    “分代收集”理论是建立在两个假说之上:
    弱分代假说:绝大数对象都是朝生夕灭的;
    强分代假说:熬过多次垃圾回收的对象越难以回收;
    在java堆划分成不同区域后,才有了针对不同区域的各种垃圾回收算法,

    3、标记-清除算法

    分为“标记”和“清除”两个步骤,先标记出所有需要回收的对象,标记完成后再统一回收掉所有对象,也可以返回来;它是最基础的回收算法,后续的算法,都是在其缺点上进行改进,其两个主要缺点:
    1、执行效率不稳定,如果java中需要标记的对象很多,大部分都需要回收,那么标记和清除会随着对象的增多而 降低效率;
    2、内存碎片化,标记清除之后产生大量不连续的内存空间,导致后面需要分配较大对象时不得不进行一次回收;

    4、标记-复制算法

    为了解决标记-清除算法中大量可回收对象执行效率低的问题.复制算法是将可用内存一分为二,当一块内存用完后,将还存活的对象一次性复制到另一块内存中,让后把已使用的内存一次性清除掉.这个算法是基于弱分代假说的基础上,也就是大部分对象都是朝生夕灭的,否则就会存在大量对象需要来回复制的问题.
    优点:不会随着内存对象的增多而降低效率;因为是针对整个半区回收,只需要移动指针,不会存在内存碎片;
    缺点:浪费一半内存

    5、标记-整理算法

    为了解决标记-复制算法可能出现的存活对象过多而导致的执行效率降低的问题;同时如果不想浪费50%的内存,就需要有额外空间进行担保,以应对对象100%存活的极端情况,所以老年代一般不能使用这种算法.
    标记-整理算法标记过程一样,只是在最后清除的时候不是对所有对象直接清楚,而是将存活对象移动到空间一端,然后再直接清理掉边界以外的内存.但是移动对象并更新引用是极其繁重的任务,必须暂停全部用户线程,即“Stop The World”

    五、算法实现细节

    1、跟节点枚举

    我们在可达性分析算法中需要找到所有的GC Roots根,虽然目标明确,但是想要高效完成却是很困难的.现在Java引用越来越大,即使是方法区也已经达到几百上千兆,里面的类、常量更是恒河沙数,如果逐个遍历肯定很难.
    目前,所有的垃圾回收器在根结点枚举这块都无法做到与用户线程并行,那么就会面临“Stop The World”,现在可达性分析算法耗时最长的查找引用连可以和应用线程并行,但是根节点枚举必须在一个**“一致性快照”**基础上进行,也就是说在跟节点枚举的过程中引用链不发生变化,否则可达性分析的准确性不能保证.
    实际上当引用线程暂停时不需要一个不漏检查完所有的GC Roots,虚拟机是有办法直接获取到的(类加载过程结束引用关系会被存在OopMap的数据结构中)

    2、安全点

    在OopMap数据结构的协助下,我们能够很快定位到GC Roots跟节点,但是可能导致其引用关系变的指令很多,如果每条指令都去存储一个OopMap那么所耗费的内存是不可接受的.实际上并不需要记录每一条变化的指令,只需要在“特定位置上”记录就可以,这个特定位置就是安全点.有了这个安全点那么用户线程就不能在任意一个指令上停止,必须到达这个安全点才可以暂停.因此安全点数量不能太少,导致收集器等待时间过久,也不能太多耗费太多内存.安全点通常选择方法调用、循环跳转、异常跳转等.
    另一个问题垃圾收集发生后如何让所有线程跑到最近的安全点,然后停顿下来.
    抢断式中断:发生垃圾收集时,直接暂停所有用户线程,发现不在安全点再恢复这条线程;
    主动式中断:设置一个标志位,所有线程去轮训这个标识位,一旦发现就到最近的安全点暂停.

    3、安全区域

    安全点的前提是程序在执行不太长的时间后可以到达这个地方.但是,程序“不执行”的时候怎么办?所谓不执行就是没有分配处理器时间,例如线程处于sleep状态或block等,虚拟机不可能一直等着,所以就引入了安全区域.
    安全区域是指线程在某一段代码片段中,引用关系不会发生变化.
    当线程执行到安全区域时,会标记自己进入,这时发生垃圾收集时不需要管这些线程,当这些线程准备离开安全区域时,会检查是否已经完成GC Root枚举.

    4、可达性分析

    六、垃圾收集器

    1、Serial收集器

    单线程收集器,主要体现在进行垃圾回收时必须暂停所有用户线程.
    对于单核或者核数少的环境,因为是单线程没有线程切换的开销,反而性能较好,客户端首选
    ![image.png](https://img-blog.csdnimg.cn/img_convert/001b70174f3b6c213d00ee6aba727649.png#clientId=uc880d338-855c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=315&id=u824c11be&margin=[object Object]&name=image.png&originHeight=630&originWidth=2350&originalType=binary&ratio=1&rotation=0&showTitle=false&size=372391&status=done&style=none&taskId=uccd67144-f3fc-42e6-a08b-c9d8973167d&title=&width=1175)

    2、ParNew收集器

    Serial收集器的多线程版本,除了收集时采用多线程收集,其它基本一致.
    除了Serial以外只有ParNew可以与CMS配合使用
    ![image.png](https://img-blog.csdnimg.cn/img_convert/42fe45449a4226ebc4fc150fac380288.png#clientId=uc880d338-855c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=262&id=u05db5a9a&margin=[object Object]&name=image.png&originHeight=524&originWidth=2370&originalType=binary&ratio=1&rotation=0&showTitle=false&size=378413&status=done&style=none&taskId=uee2506a1-648b-4c43-ae0a-797b4b3e39c&title=&width=1185)

    3、CMS收集器

    CMS是以最短回收停顿时间为目标的收集器,大部分应用都会关注服务的响应速度,CMS很符合这一点.
    标记-清除算法为基础,分为四步:
    1、初始标记:”Stop The World“,标记GCRoot直接关联的对象,速度快;
    2、并发标记:标记整个对象图的过程,最耗时,但是可以与用户线程并行,产生浮动垃圾;
    3、最终标记:”Stop The World“因为并发标记阶段用户线程也在执行,为了找出这部分对象;
    4、并发清除:不需要移动对象,与用户线程并行,产生内存碎片.

    ![image.png](https://img-blog.csdnimg.cn/img_convert/4f265e8a9ae0069bcfc50f029e7c36f6.png#clientId=uc880d338-855c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=338&id=u633df3d2&margin=[object Object]&name=image.png&originHeight=676&originWidth=2318&originalType=binary&ratio=1&rotation=0&showTitle=false&size=571972&status=done&style=none&taskId=u3b1da1c5-b79c-4a9b-b300-c3a5e7925ff&title=&width=1159)

    三个明显缺点:
    **1、处理器资源敏感:**并发阶段与用户线程一起,一部分线程处理垃圾,必然会导致吞吐量下降
    2、浮动垃圾:可能出现Concurrent Mode Failure,而发生“Stop The World”,在并发阶段用户线程继续执行,那么就会产生垃圾,但是这部分是出现在标记之后,所以只能到下次才能回收,这部分垃圾就是“浮动垃圾”.同时在垃圾收集阶段程序还要运行所以需要预留一些内存给用户线程使用,CMS老年代内存使用达到92%;但是这就会带来另一个问题,预留内存无法满足新分配的对象就会出现“并发失败”,就会使用预备方案,采用Serial Old
    **3、内存碎片:**标记-清除算法所带来的后果,CMS提供一个参数,在垃圾收集若干次后进行一次整理(默认是0)

    4、Garbage First收集器

    收集器发展史上里程碑式成果,他开创了面向局部收集的设计思路和基于Region的内存布局形式,建立了一款可预测停顿时间模型的收集器.
    在G1之前,垃圾收集的目标要么事年轻代(Minor GC)、要么事老年代(Major GC)、或者是整个java堆(Full GC).G1则跳出这个牢笼,面向堆内存任何部分来组成回收集(Collection Set),衡量标准不在是哪个分代,而是那块内存垃圾最多,回收效益最大,这就是Mixed GC.
    G1依然遵循分代收集,但是不再坚持固定大小和固定数量的分代,而是把整个java堆划分成大小相等的区域(Region,取值1-32MB,2的N次幂),每个Region都可以扮演Eden空间,Survion空间或者老年代空间.收集器对扮演不同角色的Region采用不同策略去处理;同时对于大对象(超过一个Region的一半)存放在Humengous区域.
    之所以能做到可预测的停顿时间,因为将Region做为单次回收的最小单元,记录每个Region的回收价值,也就是回收所获的空间和所需要的时间的经验值,维护成一个优先级列表,每次根据用户设定的时间去回收哪些收益最大的Region.
    ![image.png](https://img-blog.csdnimg.cn/img_convert/cc600a2720849b094cfbf273567650ca.png#clientId=uc880d338-855c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=771&id=u791f414d&margin=[object Object]&name=image.png&originHeight=1542&originWidth=2314&originalType=binary&ratio=1&rotation=0&showTitle=false&size=165253&status=done&style=none&taskId=u0ab93e86-6277-4caf-afaa-1eeee2b4134&title=&width=1157)

    G1大概分为四个阶段
    **1、初试标记:**停顿线程,标记GCRoot直接关联的对象,修改TAMS指针的值,让下一阶段可以正确的在Region上分配对象;
    **2、并发标记:**用户线程并行,遍历整个GCRoot图,重新处理SATB引用变更;
    **3、最终标记:**停顿线程,处理并发阶段产生的
    **4、筛选回收:**更新Region统计记录,对每个Region回收价值和成本排序,根据用户的期望时间,自由的选择回收集;然后把决定回收的Region中存活的对象,复制到空的Region中(复制算法),再清理掉整个旧Region,这里涉及到对象移动,必须暂停线程.

    ![image.png](https://img-blog.csdnimg.cn/img_convert/ac5a0b72167c481af71458958564ffef.png#clientId=uc880d338-855c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=323&id=u47e521c5&margin=[object Object]&name=image.png&originHeight=646&originWidth=2292&originalType=binary&ratio=1&rotation=0&showTitle=false&size=607886&status=done&style=none&taskId=ua56e6acf-5cb5-4cd8-b6e1-d6dddb61ca0&title=&width=1146)

    5、G1与CMS比较

    回收算法上:G1整体采用“标记-整理”算法,而从两个Region来看又是“标记-复制”算法,所以相比CMS使用的“标记-清除”算法来看,没用内存碎片,垃圾收集完成后,保留规整的对象空间,有利于程序长久的执行;
    内存比较:G1占用的额外内存比CMS要多,因为G1的卡表实现很复杂,每个Region都有一份,而CMS只有一份;
    执行负载:写屏障(搞不懂,没有深入了解)
    如何选择:小内存CMS优于G1,内存界限大概在6-8G.

    七、内存分配策略

    1、对象优先在Eden分配:

    一般情况对象优先在Eden分配,当Eden不够,就会进行一次Minor GC

    2、大对象直接进入老年代:

    -XX:PretenureSizeThreshold参数设置大对象的阈值.超过直接放入老年代
    a、大对象会导致内存明明还有但是提前触发垃圾回收已获取一段连续的内存来存放大对象;
    b、如果大对象在年轻代
    来回复制
    也是很大的开销

    3、长期存活的对象进入老年代:

    虚拟机给对象定义了一个年龄计数器,存在对象头中,初始为1,每复制一次就+1,默认到到15就移动到老年代;

    4、动态对象年龄判定:

    如果Survivoer空间中相同年龄的对象总和大于一半,那么大于等于它的对象直接放入老年代;

    5、空间分配担保:

    在Minor GC发生之前,虚拟机需要检查老年代最大连续内存总和是否大于新生代所有对象总和,如果大于则放心进行gc.否则就可能存在风险,检查配置是否允许担保失败,检查老年代最大连续空间是否大于年轻代历史晋升平均内存大小,大于则尝试进行Minor GC,否则就是Full GC.

    八、类加载机制

    1、类加载过程

    类加载的生命周期,分为七个阶段:加载->验证->准备->解析->初始化->使用->卸载,其中加载、验证、准备、初始化、卸载是固定的,解析阶段比较特殊,它可以在初始化之后执行,这是为了支持java语言的运行时绑定的特点.
    何时开始类加载过程
    何时进行第一阶段“加载”《java虚拟机规范》并没有强制,而是有且只有在这6种情况下必须进行“初始化”(也就意味着类加载过程开始)
    1、使用new关键字创建对象、读取或设置一个静态变量(被final修饰除外)、调用一个类型的静态方法;
    2、反射调用
    3、子类初始化时发现父类没有初始化,需要触发父类初始化
    4、虚拟机启动时,用户指定的一个需要执行的主类(main方法)
    5、jdk7的动态语言
    6、jdk8,default修饰的接口方法,其实现类被初始化,该接口也会初始化

    **加载:**主要完成三件事
    1、通过类的全限定名获取此类的二进制字节流;
    2、
    3、
    **验证:**class文件的字节流是否符合《java虚拟机规范》的全部约束
    **准备:**为类中定义的静态变量设置初始值(通常为0值),实例变量会在初始化的时候分配
    **解析:**将符号引用更换为直接引用
    符号引用:一组符号来描述所指向的目标
    直接引用:可以直接指向目标的指针、偏移量等
    初始化:主动权交给java应用程序,在“准备”阶段已经给了“零”值,那么此时才会给变量赋真实值

    2、双亲委派机制


    先交给父类加载,如果父类加载不了,再有自己加载

    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 
        // 首先,检查请求的类是否已经被加载过了 
        Class c = findLoadedClass(name); 
        if (c == null) { 
            try { 
                if (parent != null) { 
                    c = parent.loadClass(name, false); 
                } else { 
                    c = findBootstrapClassOrNull(name); 
                }
            } catch (ClassNotFoundException e) { 
                // 如果父类加载器抛出ClassNotFoundException 
                // 说明父类加载器无法完成加载请求 
            }if (c == null) { 
                // 在父类加载器无法加载时 
                // 再调用本身的findClass方法来进行类加载 
                c = findClass(name); 
            } 
        }if (resolve) { 
            resolveClass(c); 
        }
    return c;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
  • 相关阅读:
    OPPO广告联盟战略升级,全面提升开发者变现效率
    three.js
    ciscn_2019_n_8【BUUCTF】
    自建Elasticsearch 集群的规划和常见问题
    【C语言】每日一题(半月斩)——day3
    买卖股票的最佳时机 II
    AcWing 1211:蚂蚁感冒 ← 模拟题
    ScrollView如何裁剪粒子特效
    ABAP Json和对象的转换
    Myeclipse反编译插件(jad)的安装和使用
  • 原文地址:https://blog.csdn.net/qq_36952611/article/details/126527979