1. jvm主要包括四个部分:
- 类加载器(ClassLoader):将class文件加载到JVM中
- 执行引擎:负者将字节码指令解释编译成对应平台上的机器码
- 内存区(也叫运行时数据区)
- 本地方法接口
jvm执行流程:首先将java代码(.java)代码转化为字节码(.class),然后通过类加载器将字节码加载到内存中(运行时数据区),通过执行引擎将字节码翻译成可以被底层操作系统执行的指令再去执行。
2. jvm内存区布局:
简单可分为两大块:
-
线程共占内存:堆内存;方法区
-
线程独享内存区域:虚拟机栈;本地方法栈;程序计数器
堆:堆是jvm中占用空间最大的一块区域;这块是GC的主要区域。《java虚拟机规范》对堆的描述是:“java所有的对象实例以及数组都应当在堆上分配”。但在技术快速发展的今天看来已经不是那么准确了,比如JIT(即时编译)优化中的逃逸分析使得变量可以直接在栈上被分配。
注意字符串常量是在堆上当对象或者变量在方法中被创建之后,其指针可能被线程所引用,而这个对象被称作指针逃逸或者引用逃逸。
如一下代码中sb对象的逃逸:
public static StringBuffer createString() {
StringBuffer sb = new StringBuffer();
sb.append(“Java”);
return sb;
}
sb是一个局部变量,它被return出去了,因此可能赋值给了其他变量,并且被完全修改,于是sb就逃逸到了方法外部。
想要sb不逃逸可将代码改为如下:
public static String createString() {
StringBuffer sb = new StringBuffer();
sb.append("Java");
return sb.toString();
}
小贴士:通过逃逸分析可以让变量或者对象直接在栈上分配,从而极大的降低了垃圾回收的次数,以及堆分配对象的压力,进而提高程序整体运行效率。
堆内存的大小值可以通过配置-xms(设置最大值)和-xmx(设置最小值)来设置,当堆超过最大值就会抛出OOM(OutOfMemoryError)异常
堆内存博客地址
堆内存通常被分为三块区域:
在JDK1.8之前分为:新生代;老年代;永久代
在JDK1.8之后将最初的永久代取消分为:新生代;老年代;元空间
方法区:用于存储已经被JVM加载的类型信息,常量,静态变量,代码缓存等信息。
程序计数器:程序寄存器是线程独享的一块很小的内存区域,保存当前线程所执行字节码的位置,包括执行的指令,跳转,异常处理等。
虚拟机栈:java虚拟机栈是用来描述java方法的执行,每个方法被执行时就会同步创建一个栈帧(且多和线程联系在一起;每当创建一个线程,JVM就会为这个线程创建一个对应的java栈)用于存放局部变量表,操作栈,方法返回值等信息;每一个方法的从调用到执行完毕就对应着一个栈帧在java虚拟机栈中入栈到出栈的过程。
本地方法栈:本地方法栈与java虚拟机栈类似,只不过java虚拟机栈是为执行java方法服务的,本地方法栈是为本地方法(native)服务的。
3. 类加载:
类从被加载到内存中开始到卸载出内存为止整个生命周期包括以下7个阶段:(1.加载;2.验证;3.准备;4.解析;5.初始化;6.使用;7.卸载)
其中验证,准备,解析阶段统称连接;通常所说的JVM类加载就是指前五阶段。
- 加载:加载阶段主要做三件事。1.通过类名查找到相应的类;2.并将此类的字节流转换为方法区运行时的数据结构3.在内存中生成一个能代表此类的java.lang.class对象,作为其他数据访问的入口。
- 验证:主要是为了验证字节码的安全性
- 准备:为类中定义的静态变量分配内存,这些静态变量会被分配到方法区上。HotSpot虚拟机在JDK1.7之前都在方法去上,而在JDK1.8后这些变量 会随着类对象一起存放在java堆中。
- 解析:此阶段主要是用来解析类,接口,字段,方法的,解析时会把符号引用替换成直接引用。
符号引用是指以一组符号来描述所引用的目标;直接引用是可以直接指向目标的指针。两者重要区别:使用直接引用,引用目标必定已经存在于虚拟机的内存中了,而符号引用不一定。
- 初始化:JVM正式开始执行java业务代码了,到这一步,类加载的过程就算正式完成了。
类加载器:
- Bootstrap ClassLoader(启动类加载器)
- Extention ClassLoader(扩展类加载器)
- App ClassLoader(应用类加载器)
双亲委派机制:
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,一次递归,如果父类加载器可以完成加载任务,就成功返回;只有在父类加载器无法完成此加载任务时,才自己去加载。
4.GC:
- 基本所有数据都会保存在JVM堆内存中
- 对于整个GC流程里面。最需要处理的是年轻代与老年代的内存清理操作
- 元空间不在GC范围类
GC具体流程:
1.当一个新的对象产生,JVM需要为该对象进行内存空间申请
2.先判断Eden区是否有内存空间,如果有,直接将新对象保存在Eden区
3.如果Eden区内存空间不足,会执行一个Minor GC操作,将Eden区无用的内存空间进行清理
4.清理完Eden区之后继续判断Eden区内存情况,如果充足,将新对象直接保存在Eden区
5.如果执行完Minor GC后,发现Eden区的内存依然不足,那就判断存活区的内存空间,并将Eden区的部分活跃对象保存在存活区。
6.活跃对象迁移到存活区后,继续判断Eden区内存情况,如果充足则将新对象保存再Eden区
7.如果存活区也没空间了,则继续判断老年区,如果老年区充足,则将存活区的部分活跃对象保存在老年区
8.存活区的活跃对象迁移到老年区后,则将Eden区的部分活跃对象保存在存活区
9.如果老年区也满了,这时候产生Major GC(Full GC)进行老年区的内存清理
10.如果老年区进行了Major GC后发现还是无法进行对象保存,这时候就会产生OOM异常。
5.垃圾回收:
-
分代垃圾回收机制
-
-
5.1GC回收判断对象已死的方法
1.引用计数算法: 给对象中添加个引用计数器,当对象被引用时计数器+1;失去引用时计数器-1;
优点:判断效率高
缺点:很难解决对象之间相互引用的问题
2.可达性分析算法:(Hotspot虚拟机使用的可达性分析算法);可达性分析算法时通过一系列的称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明这个对象是不可用的,可以被回收的。
死亡对象的判断:当使用可达性分析判断一个对象不可达时,并不会直接标识这个对象为死亡状态,而先是将他标记为”待死亡“状态在进行一次校验。校验的类容就是此对象是否重写了finalize()方法,如果该对象重写了finalize()方法,那么这个对象会被存入F-Queue队列中,等待JVM的Finalizer线程去执行重写的finalize()方法,在这个方法中如果此对象将自己赋值给了某个类变量时,则表示此对象已经被引用了。因此不能被标识为死亡状态,其他情况则会被标识为死亡状态。
在java中可作为GC Roots的对象包括1.虚拟机栈中的引用对象;2,方法区中的静态属性,常量引用的对象;3,本地方法中(Native方法)引用的对象
- 5.2垃圾回收常用算法:
- 标记-清除算法
标记清除算法是由标记阶段和清除阶段构成的。标记阶段会给所有的存活对象做上标记,而清除阶段会把没有标记的死亡对象进行清除
标记清除算法最大的弊端是会产生内存空间的碎片问题;也就是说标记清除算法执行后会产生大量不连续的内存空间,这样当程序需要分配一个大的对象时,应为没有足够的连续空间而导致需要提前触发一次垃圾回收动作。
- 标记-复制算法
标记-复制算法时标记-清除算法的一个升级 ,使用它可以有效的解决内存碎片问题。它是将内存分为大小相同的两块区域,每次只使用其中一块区域,这样在使用垃圾回收时就可以直接将存活的对象复制到新的内存中,然后把另一块区域的内存全部清空。弊端:标记-复制算法会降低内存利用率。
- 标记-整理算法
标记-整理算法它是由两个阶段(标记,整理)组成的其中标记阶段和标记-清除算法的标记阶段一样,整理阶段不是对直接对内存的清理,而是把所有存活的对象移动到内存的一端,把另一端所有死亡额对象全部清除。
- 分代收集算法
老年代一般使用”标记-清除“、‘标记-整理算法’;年轻代一般用标记-复制算法。
垃圾回收器:
- serial收集器
serial是单线程运行的垃圾回收器,其单线程是指进行垃圾回收时所有的工作线程必须暂停,直到垃圾回收结束为止。
serial收集器的特点:简单高效,本身的运行对内存要求不高,在客户端模式下使用较多。
- ParNew收集器
ParNew收集器是serial收集器的多线程并行版本。
- Paraller Scavenge回收器
该回收器关注的侧重点是一个可以控制的吞吐量【它的计算公式:用户运行代码时间 / (用户运行代码时间 + 垃圾回收执行时间)】;Paraller Scavenge追求的目标是将这个吞吐值控制在一定范围类。
- CMS收集器(Concurrent Mark Sweep)
CMS垃圾收集过程分为4个阶段:
- 初始标记:只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
- 并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
- 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
- 并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。所以总体来看CMS收集器的内存回收和用户线程是一起并发的执行的
CMS收集器是基于标记-清除算法实现的。
- G1垃圾回收器
Garbage first垃圾回收器相比CMS收集器,G1收集器两个重要改进是:
1.基于标记-整理算法,不产生内存碎片。
2.可以非常精确控制停顿时间,在不牺牲吞吐量的前提下,实现低停顿垃圾回收。
G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保G1收集器可以在有限的时间获得最高的垃圾收集效率。
6.排查与优化JVM
生产环境如何排查JVM问题:
1.最简单的做法是使用JDK自带的命令工具
- jps (虚拟机进程状况工具) 类似linux里的ps命令,用于列出正在运行的JVM的LVMID(本虚拟机的唯一ID),以及JVM的执行主类,JVM的启动参数等信息。语法如下:
jps [options] [hostid]常用的options选项:
- -l :用于输出运行主类的全名
- -q:用于输出虚拟机唯一ID
- -m:用于输出虚拟机启动时传递给主类main()方法的参数
- -v:用于输出启动时的JVM参数
7.JVM锁优化和膨胀过程
锁优化策略:
1.自旋锁:自旋锁就是在拿锁的时候发现已经有线程拿了锁,如果去拿会阻塞自己,这时候将会进行一次忙循环尝试。也就是不停的循环看是否等等到上一个线程自己释放锁。自适应自旋锁指的是例如第一次设置最多自旋10次,结果自旋过程中成功获取到了锁,那么下一次就可以设置成最多自旋20次。
2.锁粗化:虚拟机通过适当的扩大锁的范围以避免频繁的拿锁释放锁的过程。
3.锁消除:通过逃逸分析发现其实根本就没有别的线程产生竞争的可能,或者同步块内进行的是原子操作,”而自作多情的加锁“;有可能虚拟机会去掉这个锁。
4,偏向锁:在大多是情况下,锁不仅不存在多线程竞争,而总是由同一个线程获得,因此为了让此线程获得锁的代价更低引入偏向锁的概念。偏向锁的意思是如果一个线程获得一个偏向锁,如果在接下来的一段时间内没有别的线程竞争锁,那么持有偏向锁的线程进入或退出同一个同步代码块,不需要进行抢占锁和释放锁的操作。
5.轻量级锁:当超过一个线程竞争一个同步代码块时,会发生偏向锁的撤销,当前线程会尝试使用CAS来获取锁,当自旋超过指定数时,仍无法获得锁,此时锁会膨胀为重量级锁
6.重量级锁:重量级锁依赖对象内部的monitor锁实现而monitor又依赖操作系统的互斥锁。当系统检查到是重量级锁之后,会把想要等待获取锁的线程阻塞掉,被阻塞的线程不会消耗CPU,但阻塞或唤醒一个线程,都需要操作系统来实现。
8.java对象的组成
在jvm中对象在内存中分为三个区域
- 对象头
对象头分为
1.mark word(标记字段)默认存储对象的HashCode,分代年龄和锁标志位信息。他会根据对象的状态复用自己的存储空间,也就是说在运行期间mark word里面存储的数据会随着锁标志位的变化而变化。
2.kass point(类型指针)对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 - 实例数据
这部分主要存放类的数据信息,父类的信息。 - 对其填充
由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅是为了字节对齐。