JVM总结
JVM 的职责
负责去加载,并且把加载的数据拿到内存里面,并且去分配内存
为什么要优化 JVM
因为在某些时候,我们发现代码完全没问题,只是 JVM 的瓶颈影响代码的性能,就必须优化它
java 跨平台原理
一次编译,处处运行
只要有虚拟机和字节码,就可以
类加载机制
加载、连接、初始化,其中连接包括验证、准备、解析
类加载机制就是把字节码加载到内存中来,最终形成可以被虚拟机直接使用的java类型
初始化时机
有且只有5种情况必须立即对类进行初始化
- new对象 调用静态方法 (除了final修饰的字段)
- 反射
- 父类还没进行初始化
- 虚拟机启动,执行主类
- 动态语言支持
这五种场景中的行为称之为对一个类进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,叫做被动引用。
被动引用
- 通过子类引用父类的静态字段,不会导致子类初始化,因为此时的静态资源不是属于子类父类的,是直接属于类的
- 通过数组定义来引用类,不会触发此类的初始化
- 如果字段是常量,在编译时期就已经确定是在类的常量池中,所以不会对类进行初始化
加载
在加载阶段,虚拟机需要完成以下3件事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流.
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构.
3.在内存中生成一个代表这个类的java.lang.Class
对象,作为方法区这个类的各种数据的访问入口.
》》方法区放的存储结构,也就是构造器,字段,方法这些
讲一下类加载的过程
加载、连接、初始化,其中连接包括验证、准备、解析
加载目的是能够把字节码文件拿到虚拟机来,得到一个可以使用的字节码对象
- 通过全限定名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
字节码从哪里来是不固定的,所以有了下面的技术
验证主要是为了安全
1. 文件格式验证
2. 元数据验证
3. 字节码验证
4. 符号引用认证
准备是为了静态变量赋初始值
为类变量分配内存并设置类变量初始值
解析是把符号引用解析为直接引用
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
初始化是执行类构造器 <clinit> ()
方法
初始化阶段是执行类构造器 <clinit> ()
方法的过程
类加载器
类加载器是用来完成类加载的工具,在java里面就是一堆的类
字节码对象只能有一份,比如说 Person 类可能被很多类加载器同时加载---->>
A类加载器加载 Person 类形成一个 Class 对象
B类加载器加载 Person 类形成一个 Class 对象
如果字节码对象不相等了,那么可能会推翻 java 的一些基本概念。因此,必须要保证一个类只会被加载进内存一次。
java.lang.Obejct
,自定义了一个这样的类,语法没问题,编译通过,但是绝对不可能使用JVM把它加载到内存中
谈谈双亲委派模型、优点
双亲委派模型是加载java类的模型
对于类的加载,只需要加载进内存一次就足够了。为了避免重复加载,当父 ClassLoader 已经加载了该类的时候,就没有必要让子 ClassLoader 再加载一次。这种加载器之间的层次关系,就叫做双亲委派模型(Parents Delegation Model).
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父(没有继承关系,而是使用组合关系来组织他们的层级关系)类加载器。如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,当父类加载器反馈自己不能完成这个加载请求(自己负责的范围内没有这个类),子类加载器才会尝试自己去加载。
双亲委派模型有一个显而易见的好处就是 java 类随着它的类加载器一起具备了一种带有优先级的层次关系。比如java.lang.Object
类,无论是哪个类加载器接收到加载的请求,都会委派给启动类加载器去加载。因此 Object 类在程序中都是同一个类。相反,如果没有双亲委派,任何一个类加载器收到请求都自己去加载,那么系统中将会出现多个不同的 Object 类,java 类型体系的最基本的行为就无法保证了。
打破双亲委派模型
双亲委派模型被破坏,并不包含贬义,只要有足够意义和理由就可以认为这是一种创新,什么方式会打破双亲委派模型呢?
1.自定义类加载器,复写loadClass方法;
2.使用线程的上下文类加载器对象。
双亲委派模型,并不是一个强制性的约束模型,而是java设计者推荐给开发者的类加载实现方式。在java的世界中大部分的类加载器都遵循这个模型。但是,在一些应用场景下,由于直接或间接的原因,双亲委派模型被破坏:
1.在我们自定义类加载器的时候,可以复写父类ClassLoader的loadClass
方法,这样就直接破坏了双亲委派模型。到后面 JDK1.2 之后,为了解决这个问题以及兼容问题,提供了一个findClass()
方法;
2.如果 API 中的基础类想要调用用户的代码(JNDI/JDBC等),此时双亲委派模型就不能完成。为了解决这个问题,java设计团队只好使用一个不优雅的设计方案:Thread的上下文类加载器,默认就是应用程序的类加载器;
3.由于程序动态性的发展,希望应用程序不用重启就可以加载最新的字节码文件,此时就需要破坏双亲委派模型。
JVM内存模型
运行时数据区
:JVM 管理当前应用的内存(内存分配和内存销毁)。JVM 是模拟的计算机环境,字节码文件是运行在虚拟机中,虚拟机会去管理针对这一个应用的内存。这也说明了,虚拟机为什么要去分区,目的就是为了把不同的数据放在不同的地方,方便管理。
程序计数器、虚拟机栈、本地方法栈、堆、方法区、直接内存
程序计数器
就是一个行号,确定代码执行到哪一行的一个标记。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器(内存空间很小,不会内存溢出。线程私有)。
虚拟机栈
描述 java 方法执行的内存模型(线程私有)
存的是局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
》》所以垃圾回收不会来这里,毕竟每次出栈,都进行了内存销毁
本地方法栈
虚拟机使用到的Native
堆
heap
对象的创建
必须验证有没有连接
有没有加载
再去创建,创建的时候要不要执行初始化方法
创建完之后,空间主要放的是对象头、一些字段信息以及补全的信息
HotSpot 使用的是直接指针的访问方式
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部可以设置划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
从内存回收的角度来看,由于现在收集器基本上都采用分代收集算法,所以对空间还可以细分为:新生代(年轻代),老年代(年老代)。再细致一点,可以分为Eden空间,From Survivor空间, To Survivor空间。
不论如何划分,都与存放内容无关,都是存放的是对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存。
方法区
线程共享
运行时常量池
直接内存
存在于 非虚拟机运行时数据区 的部分。
java 反汇编指令 javap -c
GC
什么是垃圾
没有被引用的就是垃圾。当然,循环引用然而没有被外界引用,这俩也是垃圾
对象存活判断
-
引用计数算法
给对象添加一个引用计数器。有一个地方引用时,计数器+1,失去引用,计数器-1,为0表示就是垃圾,但是难以解决循环引用问题,也就是A中只有B,B中只有A,显然AB都是垃圾,却没被引用计数算法判断到,因此 java 虚拟机并没有选用该方法
-
可达性算法
通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用,如下图,GC Roots对象不可达的对象,就是可以回收的垃圾对象。
垃圾收集算法
-
标记-清除算法,效率低,空间上存在内存碎片
-
标记-整理 (也叫标记-压缩),把空间整理一下
标记整理,主要是用在存活对象比较多的情况,不适用于复制,因为复制会消耗性能,常用于老年代
-
复制,标记完把剩余的存活对象复制到空闲空间,比如右,然后把左边全部干掉,代价就是内存缩小到原来的一半,持续复制的话效率不高
复制算法主要用在存活对象比较少的情况,常用于新生代
-
分代收集(可以看成是集成者)
根据对象的生命周期,新生代用复制算法,老年代用 标记-清除 或者 标记-整理
新生代
新生代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Survivor Space。
当对象在堆创建时,将进入年轻代的Eden(伊甸园) Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Survivor(幸存区) Space,如果对象仍然存活,则复制到B Survivor Space,如果B Survivor Space已经满,则复制到Old Gen。同时,在扫描A Survivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Survivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Survivor Space。这么做主要是为了减少内存碎片的产生。
Young Gen垃圾回收时,采用将存活对象复制到到空的Survivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收。
老年代
老年代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片 (将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
按系统线程划分
- 串行收集,单线程,在目前多核电脑用得比较少
- 并行收集,GC时,需要暂停整个运行环境,多线程处理,速度快、效率高
- 并发收集,让GC和工作线程都工作
垃圾回收器
CMS 并发标记清除
最短回收停顿时间为目标的收集器
优点:并发收集、低停顿
缺点:标记-清除算法,会产生大量空间碎片、并发阶段会降低吞吐量,如果预留空间不足,还可能出现 Concurrent Mode Failure 失败,导致性能不好
G1
目前最前沿的收集器,解决CMS存在的问题。
采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了
优点:并行与并发、分代收集、空间整合、可预测停顿。
GC日志(基本技能)
在现实应用中,比较常见的组合使用大概就四种:
年轻代和老年代均使用 Serial GC。
年轻代和老年代均使用 Parallel GC。
年轻代使用 ParNew GC,老年代使用 CMS收集器。
不进行年轻代和老年代区分,使用 G1收集器。
新生代
Eden空间:存活区1:存活区2
8:1:1
例如10M内存空间,那么 Eden 有8M,存活区1和2分别有1M。需要注意的是,分配8M到Eden是放不下的,Eden其他地方就占用了一些空间,因此此时会触发GC。存活的放到存活区1,可是如果存活区1空间不够,那么就会直接放到老年代。
内存分配与回收策略
-
对象优先在 Eden 区分配
-
大对象进入老年代
-
使用并行收集器,默认,如果 Eden 区空间不足,那么判断新申请的空间如果超过了 Eden 的一半,直接进入老年代。
-
串行的话需要设置参数,表明多大是大对象
-
-
长期存活的对象将进入老年代
-
动态对象年龄判断
虚拟机并不是永远要求到达年龄才能晋升老年代
年龄总和的对象空间之和大于一半
-
空间分配担保
担保新生代存活的数据能完完全全进入到老年代里面
虚拟机性能监控和检测
虚拟机参数
堆设置
-Xms:初始堆大小(与Xmx设置值一样,避免JVM重新分配内存,也就是扩容,比如一个100,一个1000,100不够用不会去 GC,而是变成200,重新管理原来的那堆数据,浪费性能)
-Xmx:最大堆大小
方法区
-XX:PermSize:设置持久代(perm gen)初始值((与MaxPermSize设置值一样,避免JVM重新分配内存))
-XX:MaxPermSize=n:设置持久代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。默认为8 : 1 : 1
收集器设置
不用记,百度查表就行了
垃圾回收统计信息(打印到控制台)
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
调优命令
jstat
jps
》》有点像 Linux 的 ps 命令
jmap
jstack