Java Virtual Machine
Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。
1. 类装载器子系统
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
1.1 类的加载
指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
1.2 生命周期
整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载共7个阶段
2. 运行时数据区
2.1 程序计数器
可以看作是当前线程所执行的字节码的行号指示器。在多线程情况下,轮流切换并分配处理器执行时间,为了线程切换后能恢复到正确的执行位置。
2.2 方法区
线程共享;存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
运行时常量池
2.3 Java虚拟机栈
线程私有;描述的是Java方法执行的内存模型,用于存储栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
异常:StackOverflowError,线程请求的栈深度大于虚拟机所用的允许的深度。OutMemoryError,在动态扩展中无法申请到足够的内存。
2.4 本地方法栈
为虚拟机使用到的Native方法服务。
2.5 堆
线程共享;内存中最大的一块,唯一目的是存放对象实例和数组,几乎所有的对象实例都在这分配内存。
3. 对象
3.1 对象的创建
虚拟机遇到一条new指令,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如过没有,就先执行类加载过程。
类加载检查通过后,虚拟机为新生对象分配内存,所需大小在类加载完成后便可确定,在Java堆中划分一块内存空间。划分方法:
- “指针碰撞”:堆里的内存绝对规整,用过的都在一边,没用过在另一边,中间放着一个指针作为分界点的指示器,分配内存时就移动指示器。
- “空闲列表”:堆中内存不规整,虚拟机通过维护一个列表,记录上哪些内存块时可用的,在分配的时候从列表上找一个足够大的分配,并更新表。
划分方法又由垃圾收集器决定。Serial、ParNew带Compact过程的收集器,用指针碰撞;使用CMS这种基于Mark-Sweep算法的收集器,用空闲列表。
解决对象创建过程的线程安全问题:
- CAS
- 每个线程在Java堆中预先分配一小块内存,本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的TLAV上分配,只有TLAB用完并分配新的TLAB时,才需同步锁定。通过-XX:+/-UserTLAB参数来设定是否使用。
3.2 对象的内存布局
在HotSpot虚拟机中,对象在内存中存储布局可分为3块区域:对象头、实例数据、对齐填充(Padding)。
对象头包括两部分:第一部分是存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。第二部分是类型指针,虚拟机通过这个指针来确定对象是哪个类的实例。
4. 执行引擎
5. 本地方法接口
6. 垃圾收集模块
6.1 可达性分析
Java 虚拟机判断垃圾对象使用的是:GC Root Tracing 算法。其大概的过程是这样:从 GC Root 出发,所有可达的对象都是存活的对象,而所有不可达的对象都是垃圾–主要是堆和方法区里的。
- 对象没有引用
- 程序正常执行完毕
- 程序执行了Systenm,exit()
- 程序意外终止
- 作用域发生未捕获的异常
简单地说,GC Root 就是经过精心挑选的一组活跃引用,这些引用是肯定存活的。那么通过这些引用延伸到的对象,自然也是存活的。
新生代GC-Minor GC
当Eden区满时,触发Minor GC。
老年代GC-Major GC
(1)当对象首次创建时, 会放在新生代的Eden区, 若没有GC的介入,会一直在Eden区, GC后,是可能进入survivor区或者年老代
(2)当对象年龄达到一定的大小 ,就会离开年轻代,进入老年代。而对象的年龄是由GC的次数决定的
-XX:MaxTenuringThreshold=n 新生代的对象最多经历n次GC, 就能晋升到老年代, 但不是必要条件
-XX:TargetSurvivorRatio=n 用于设置Survivor区的目标使用率,即当survivor区GC后使用率超过这个值(假如是50%), 就可能会使用较小的年龄作为晋升年龄
(3)除年龄外, 对象体积也会影响对象的晋升的, 若对象体积太大, 新生代无法容纳这个对象
-XX:PretenureSizeThreshold 即对象的大小大于此值, 就会绕过新生代, 直接在老年代分配, 此参数只对串行回收器以及ParNew回收有效, 而对ParallelGC回收器无效
Full GC:是清理整个堆空间—包括年轻代和永久代。
触发条件:
- System.gc()
- 老年代空间不足
- 方法区空间不足
- 永生区空间不足(jdk8去除)
- 统计得到的Minor GC晋升
6.2 垃圾收集算法
6.2.1 标记-清除算法
效率低、会产生内存碎片;适合存活对象多的情况。
6.2.2 复制算法
内存一分为二,每次只使用一块,GC时把使用那块的存活对象移动未使用的一块上;适合存活对象少的情况。
6.2.3 标记-整理算法
标记清除的升级版,把存活的都移向一端。
6.3 分代思想
根据 JVM 内存的不同内存区域,采用不同的垃圾回收算法。例如对于存活对象少的新生代区域,比较适合采用复制算法。
对于老年代这种存活对象多的区域,比较适合采用标记整理算法或标记清除算法,这样不需要移动太多的内存对象。
在实际的 JVM 新生代划分中,分为:Eden 区域、from 区域、to 区域 这三个区域。在HotSpot虚拟机中,JVM 将内存划分为一块较大的Eden空间和两块较小的Survivor空间,其大小占比是8:1:1。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Eden空间。
6.4 垃圾回收器
6.4.1 新生代收集器[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bQZCCB1N-1579413738485)(http://www.pianshen.com/images/745/01a2e09c14ae23b32eb8fcece33fc081.png)]
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
Parallel Scavenge高吞吐量为目标,即减少垃圾收集时间(就是每次垃圾收集时间短,但是收集次数多),让用户代码获得更长的运行时间
https://blog.youkuaiyun.com/wxy941011/article/details/80653893
6.4.2 老年代收集器[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xx6J9dt9-1579413738487)(http://www.pianshen.com/images/92/7859da6c876ff8859fc3e7c04a1899ac.png)]
CMS目的是为了缩短 垃圾收集时用户线程的停顿时间。
https://www.cnblogs.com/chanshuyi/p/jvm_serial_08_jvm_garbage_collection.html
http://www.pianshen.com/article/8114148617/
6.5 GC日志
例子:33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
33.125:GC发生的时间,具体是JVM启动以来经过的秒数。
GC:这次垃圾收集停顿的类型;如果又Full,说明这次GC发生了“Stop the world”。
DefNew:GC发生的区域,区域名与使用的GC收集器是密切相关的,如Serial收集器中的新生代名为“Default New Generation”,所以显示“[DefNew”。
[]内部的3324K->152K(3712K):GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)
[]外部的3324K->152K(11904K):GC前Java堆已使用容量->GC后Java堆已使用的容量(Java堆总容量)