JVM(Java Virtual Machine的缩写)虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。(引自百度百科)
1、JVM结构
2、JVM的结构
3、JVM程序计数器
根据计算机组成原理的知识,计算机所执行的每一条“指令”都由指令操作码+地址码组成。操作码一般储存操作的类型的地址,地址码储存操作数的地址。当执行完一条指令,CPU就会去程序计数器中查询下一条指令的地址。所以程序计数器就是保存下一条指令所在单元的地址的地方。保证程序可以连续执行使得CPU可以不间断地运行下去。
4、虚拟机栈
4.1虚拟机栈组成
虚拟机就是java线程运行需要的一个“内存空间”。虚拟机栈由一个“栈帧”组成,栈帧就是每个方法需要的内存空间,一个个方法所需的内存空间组成了一个虚拟机栈。
代码运行的方式查看虚拟机栈原理:
方法执行结束释放内存,指的就是方法对应的栈帧“出栈。”
4.2虚拟机栈大小以及垃圾回收机制
注意jvm的垃圾回收机制指的是“堆内存(存储内存)”,而非“栈内存(运行内存)”。因为虚拟机栈会随着方法调用的结束自动将对应的“栈帧”弹出栈。虚拟机栈内存大小可根据需要自定义。需要注意的是“栈内存”指的是一个java线程运行需要的内存,当有多个线程要执行时内存就会叠加,物理机的内存是固定的。比如物理机内存大小200M,你划分一个栈大小为2M,则物理机可容纳100个线程,即100个虚拟机栈。如果一个虚拟机栈大小1M,则物理机可容纳200个虚拟机栈。所以并非每个虚拟机栈内存越大越好,反而是虚拟机栈内存越大物理机所容纳的数量越少。
3.线程安全问题
所谓线程的安全大多时候指的是java多个线程运行时变量的作用域问题。int a = 1。方法method1调用变量a循环累加至1000,在method1执行完之前method2再调用a循环累加2000,method1得到的结果不会收到method2调用的影响。但如果a 变量由于static修饰的结果就会不同,method1的调用就会影响method2的调用,即虚拟机栈1调用a会底层是址传递,根据计算机组成原理的知识,指令f1操作的地址码指向a的地址,指令f2的地址码也指向a的地址,但根据单例模式的知识,a变量由static修饰了,就表示内存中只能存在一个“a变量”。
结论:一个变量是否是线程安全的要看两个方面,一是变量是否属于方法内的局部变量,二是变量是否“逃离”了方法的作用域(变量的地址返回,或者将变量作为调用其他的方法的参数)。当变量作用域逃离了当前方法,就可能会造成多线程并发执行的时候共同修改同一个变量的问题。线程就会变得不安全。所以很多方法为了安全都使用匿名内部类的方式,避免变量作用域的逃离。
4.栈内存溢出
(1)可能导致栈溢出的因素
栈帧过多:造成栈帧过多的典型情况就是“递归”,如果一直进栈,而不出栈就会造成内存溢出。
以下代码表示递归调用未设置递归终止条件,模拟栈帧过多造成栈溢出的情况:
栈帧过大:典型情况是局部变量太多,或者太大。
5.JVM调优
(1)CPU占用过高。以linux系统为例子(一般的例子是递归调用,或者实体类的双向关联):
用top命令定位,CPU占用过过高的进程
再用如下命令,进一步定位进程中占用高的线程
ps H -eo pid,tid,%cpu | grep进程idl
或配合使用jdk给出的排查一个进程中线程的命令:
jstack 进程id
(2)运行时间过长
如线程死锁:
线程a已经被锁,有对象却在等待锁住a。
6.本地方法栈
本地方法栈指的是jvm调用一些本地方法时,需要给这些“本地方法”提供的内存空间,所谓“本地方法”指并非由Java代码编写的代码。
7.堆
堆属于线程共享的,所有用new关键字创建的对象都使用堆的内存。所有堆的对象都要考虑线程安全的问题。堆有垃圾回收机制。
(1)内存的溢出。
当堆内存中的对象不再被使用,就会被回收。但如果一直产生新的对象,且对象一直不被回收,就会造成“堆内存溢出”的问题。
内存溢出的报错:OutOfMemoryError
设置Java虚拟机的最大内存:
Xmx内存大小 //Xmx8m,表示设置java堆内存最大为8m
第三方的java堆内存检测工具
(2)jconsole、jmp、jvisualvm等。
(3)jvirsualvm使用。jvisualvm已经被集成在jdk1.6以上的版本中。自身运行需要最低jdk1.6版本,但是可以监控运行在jdk1.4以上版本的java程序。
解压后直接启动。
启动jvisualvm:如实jdk没有集成就去。官方下载:https://github.com/oracle/visualvm
已经集成就在此路径启动
8.方法区
方法区是所有java虚拟机线程共享的区域。存储类的结构相关的信息,包括类的成员变量、构造器方法等等。jdk1.6以上逻辑上可理解为“堆“的一部分。方法区也可能会出现内存溢出的情况。jdk1.8之后一般默认使用物理机的内存,使用“元空间的形式实现”。1.8之前使用永久代的形式实现。
动态代理使用字节码文件加载一个类的情况,就可能造成方法区的内存溢出。
(1)运行时常量池
常量池的作用就是,给java虚拟机指令提供“常量符号”,然后通过查表的方式,找到每个常量符号对应的含义。
.java文件经过编译得.class文件,二进制字节码文件。通过对字节码进行反编译就能动态生成一个类。
常量池中的类的信息在运行时会加载到“运行时常量池”中,符号所指向的数据会被加载进去,而不是存入指针,而是数据本身。
9.直接内存
直接内存不属于Java虚拟机内存,是操作系统内存的一部分。
(1)常见于NIO操作时,用于数据缓冲区
(2)分配回收成本较高但读写性能高
(3)不受JM内存回收管理
5、JVM垃圾回收
1.如何判断对象可以回收
(1)可达性分析算法
可达性分析算法的含义指如果一个对象不可以肯定不可以被当成垃圾回收的对象,称为“根对象”。如果堆内存中一个对象被“根对象”直接或间接调用,则此对象不可以被回收。如下图未被根对象所引用的对象3将被执行垃圾回收机制,释放空间内存。
可达性算法可以理解为数据结构中的二叉树的节点。
(2)可达性分析算法原理
- Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着Gc Root对象为起点的引用链找到该对象,找不到,表示可以回收
- 哪些对象可以作为GC Root ?
指的是java程序运行过程中那些必不可少的对象。
(3)java虚拟机的四中引用(常见的四种引用)
(1)强引用
比如用new关键字创建了一个对象,用等号赋值给一个变量,则该变量就对对象是强引用。指的是沿着根节点GC ROOT的引用线可以找到的对象,java虚拟机垃圾回收时,强引用对象不会被回收。只有当那些根节点(GC ROOT)结束对该对象的强引用都断开时,才会被垃圾回收。
(2)软引用
软引用,指的是在Java程序运行时那些未被GC ROOT对象直接强引用的对象。这些对象并非java并非java程序运行所必须的对象。如果没有GC ROOT对象强引用它,在发生垃圾回收时如果发现释放的内存不够,就会再执行一次垃圾回收机制,释放掉软引用对象所占用的堆内存空间。
具体例子:
List<SoftReference<byte[]>> list = new ArrayList<>();
此时List对象对byte[]不在是直接引用,由List–>byte[]变成List–>SoftReference–>byte[]
(3)弱引用
相对于“软引用”对象来说,只要发生垃圾回收机制,不管内存空间是否充足都会被释放掉所占用的堆内存空间。软引用和弱引用在被执行垃圾回收机制时可配合一种引用队列的机制使用。
(4)虚引用
垃圾回收时会进入“引用队列”,遍历该对象所引用的对象,比如释放“直接内存”避免java虚拟机无法管理的直接内存发生内存溢出。
(5)终结器引用
所有java对象都继承与Object对象。Object对象中有一个finallize()方法(终结方法),如果一个对象重写了finallize()方法并且没有对象强引用它,该对象就会被执行垃圾回收。当一个对象没有被任何一个对象强引用时,虚拟机就会自动为其创建一个“终结器引用”。
(6)需要注意的是一个对象的引用类型并非一成不变的,即并非从程序开始到程序结束都不会改变。一个对象的引用类型是随着GC ROOT对其的引用的变化而变化,当根节点对象不再强引用它时,它就有可能被垃圾回收掉。四种引用类型是相对于“GC ROOT”节点是否直接强引用来说的,虚引用和终结器引用都需要配合一个引用队列来使用。
2.垃圾回收算法
2.1 标记清除算法
标记清除算法一般分为两步,首先标记未被GC ROOT节点所引用的对象,清除标记对象。
- 优点:速度快,因为操作步骤少。
- 缺点:容易产生“内存碎片”即零散的内存空间,可用内存不连续。
2.2标记整理算法
在标记清除算法的基础上理解,就是标记清除之后,会再将内存空间整理。使得可用空间连续。
- 缺点:因为牵扯到对象的移动,所以速度较慢。
- 优点:避免了内存的碎片化。
2.4复制
所谓复制,指的是将内存区划分为大小相等的两个区域。FROM和TO区。TO区是空闲区域。步骤和·优却点如下:
- 将不被引用的对象标记为垃圾。
- 将存活的对象复制到TO区,同时完成内存的整理。
- 清空FROM区域。
- 将TO区和FROM区的位置对调,TO变FROM、FROM变TO。
- 优点:避免内存碎片化。
- 缺点:需要双倍的内存空间。
2.5三种算法引用
在实际的JVM垃圾回收中会根据场景决定使用那种,并且是结合使用完成JVM的垃圾回收机制。
3.分代垃圾回收
将堆内存划分为两个区域新生代和老年代,这就可以根据对象的生命周期的不同,将对象放在不同的堆内存区域当中,执行不同的回收机制。这提高了垃圾回收的机制。需要注意,对象创建时都会放在新生代的“伊甸园区”中,只有当新生代区域不足时才会将寿命较长的对象移动到老年代区域。
- 新生代 -垃圾回收频率高( 新生代分为、伊甸园、FROM、TO三个区域)
Minor GC指的是当伊甸园中的内存不够,垃圾回收机制就会(标记清除算法,复制清除算法)回收伊甸园中的内存。一次垃圾回收未被清除的对象会被复制到TO区域,同时该对象的“寿命加1”。伊甸园区域将清空,同时将幸存区TO和幸存区FROM交换位置。如果幸存区TO不为空,则垃圾回收时也需要扫描,如果幸存区的对象还存活就放到幸存区FROM,对象寿命再加1。
当新生代中FROM区的对象经过一定次数的垃圾回收仍然没有被回收,则证明该对象需要长时间“存活”,那就将其从“新生代”晋升到“老年代”,降低垃圾回收的评率。
发生Minor GC时会产生stop the world。即产生Minor GC垃圾回收时会停止(停止时间较短)其他的用户线程,由垃圾回收线程先执行完垃圾回收后再继续用户的线程。因为垃圾回收过程中会发生对象的复制,这牵扯到对象地址的改变,会造成程序的混乱。
- 老年代-垃圾回收频率低
长时间需要引用的对象一般放在堆内存中的“老年代”区域。
Full GC(也会造成stop the world,且时间更长)指的是当老年代的空间也不足的时候就会触发一次“Full GC”,Full GC 会将新生代和老年代都做一次“垃圾回收”。
3.1相关VM设置参数
堆初始大小
-Xms
堆最大大小
-Xmx或-XX:MaxHeapSize=size
新生代大小
-Xmn或(-XX:NewSize=size +-XX:MaxNewSize=size )
幸存区比例(动态)
-XXInitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy
幸存区比例
-XX:SurvivorRatio=ratio
晋升阈值
-XX:MaxTenuringThreshold=threshold
晋升详情
-XX:+PrintTenuringDistribution
Gc详情
-XX:+PrintGCDetails -verbose:gc
FullGC前MinorGC
-XX:+ScavengeBeforeFullGC
4.垃圾回收器
- 串行垃圾回收器
底层是一个单线程的垃圾回收器。即当发生垃圾时,其他的线程都暂停。
(1)单线程
(2)适用场景:堆内存小,个人电脑、cpu内核单核
开启串行垃圾回收器:-XX:+UseSerialGC = Serial + Serial0ld
新生代复制算法+老年代标记整理
串行垃圾回收器工作流程:
- 让各个线程在“安全点”停止(线程状态更改为阻塞)
- 执行垃圾回收机制
- 用户线程恢复运行
- 吞吐量优先垃圾回收器(并行)
jdk1.8后默认使用的就是吞吐量量优先的垃圾回收器,又称并行垃圾回收器。
使用标记加整理的垃圾回收算法
jdk1.8之前的开启并行垃圾回收器:-XX:+UseParallelGC~ -XX:+UseParallel0ldGc
(1)多线程,一般是根据cpu核数决定要开启的线程数,多线程共同完成垃圾回收的工作,开启垃圾回收的瞬间CPU占用率高。
(2)适用于堆内存较大,cup多核的情况。
(3)让单位时间内,stop the world尽可能短,总的stop the world时间更短。
3. 响应时间优先垃圾回收器(也称“并发垃圾回收器”)
开启并发垃圾回收器:-XX:+UseConcMarkSweepGc-XX :+UseParNewGC~ Serial0ld
(1)多线程
(2)堆内存较大,cup多核
(3)尽可能让单次stop the world的时间短
(4)“并发”指的是垃圾回收线程和用户线程可以同时执行。
(5)UseConcMarkSweepGC是工作在老年代的垃圾回收器,与之对应的是UseParNewGC,工作在新生代的垃圾回收器,基于复制算法
(6)基于“标记整理算法”。
5.垃圾回收器G1(Garbage First)
jdk1.8开启G1垃圾回收器:-XX:+UseG1GC
从2017 JDK9开始默认的一款垃圾回收器,属于并发的垃圾回收器,同时注重吞吐量、低延迟两个指标。
即适用于:
- 同时注重吞吐量、低延迟两个指标。
- 超大堆内村,超大堆内存会被划分为大小相等的“区域”。
- 整体上是标记+整理算法,两个区域间是复制算法。
5.1G1回收器的三个阶段。三个阶段会循环执行完成垃圾回收。
(1)新生代的垃圾收集
初始标记(找到根对象),并发标记
(2)新生代的垃圾收集+并发标记
(3)混合收集
对新生代的三个区进行垃圾回收
6、GC调优
6.1确定调优的目标
首先是明确调优的目标,然后根据目标选择不同的调优器。
6.2最快的GC是不发生的GC
如果经常发生GC应当检查代码是否存在漏洞。以SQL查询为例子:
- 一次性查询数据太多,过多的数据进入内存
- 表数据太臃肿,对象图太大,映射的对象太大
- 内存泄露,如静态对象多大
6.3内存调优-新生代调优
新生代的特点
- 所有的new操作的内存分配非常廉价
TLAB thread-local allocation buffer死亡对象的回收代价是零 - 大部分对象用过即死
- Minor GC的时间远远低于Full Gc
6.3.1调优策略
并非新生代的内存设置越大越好。因为新生代空间越大,老年代的空间也会相应减少,这样虽然minor gc次数减少了,但却可能导致时间更长的Full GcI。
- oracle工程师建议新生代内存大小应当设置在堆内存大小的25%~50%之间。
- 新生代的幸存区大小应该能够保留【当前活跃对象+需要晋升的对象】
- 新生代的调优主要就是需要长时间存活的对象尽快晋升到“老年代”,避免每次minor gc都重复操作一遍这些对象,以减少每次minor gc的时间,同时新生代的幸存区大小应该够大,避免太多对象进入老年代造成空间不足。
调整幸存区大小:-XX:NaxTenuringThreshold=threshold
每次垃圾回收时打印幸存区的信息:-XX:+PrintTenuringDistribution
6.4内存调优-老年代调优
以GMS垃圾回收为例子,他的老年代空间大小越大越好。
老年代的垃圾调优核心思想就是,尽量不要触发FULL GC,因为FULL GC时间更长,所以当内存不足时应该首先尝试minor gc。
当新生代调优已然无法解决问题时再尝试老年代调优。使用如下命令调整老年代空间大小:
提升老年代空间大小:-xX:CMSInitiatingoccupancyFraction=percent
7.类加载
7.1类文件结构(JVM规范)
u4 魔数
magic;
u2
minor_version;
u2
major_version;
u2 常量池
constant_pool_count;
cp_info
constant_pool[ constant_pool_count-1];
u2
access_flags;
u2 当前类
this_class;
u2 父类
super_class;
u2 接口信息
interfaces_count;
u2
interfaces[interfaces_count];
u2 成员变量信息
fields_count;
field_info
fields[fields_count];
u2 方法信息
methods_count;
method_info
methods[methods_count];
u2 类的附加信息
attributes_count;
u2
attribute_info attributes[attributes_count];
7.1.1魔数
所谓魔数就是用来标识该文件是什么类型的文件的,Java文件的标识是:ca fe ba be
0~-3字节,表示它是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
[][ca fe ba be:标识文件类型][00 00 00 34:标识版本号十六进制34代表十进制52,即jdk8,51是jdk7,52是jdk9]
7.1.2常量池
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
[][ca fe ba be:标识文件类型][00 00 00 34:标识jdk版本号][00 23:表示常量池有多少项,十六进制23为十进制35,即常量池中有35项]