JVM整体认知
JVM组成大致为三个部分(Hospot & jdk1.8):1、类加载子系统:主要完成class文件加载到内存区域2、执行子系统:执行代码3、运行时数据区:主要内存区域,包括虚拟机栈、程序计数器、堆、方法区、本地方法栈
什么是JVM
JVM:Java Virtual Machine 缩写 ,Java虚拟机
不要把Java语言和JVM混淆,Java语言只是一种面向对象的语言,可以被编译称class字节码文件在JVM执行。
JVM为Java虚拟机,但JVM不仅仅可以运行Java字节码文件,只要是遵守JVM规范的语言或者字节码文件,JVM都可以执行。比如当下比较火热的消息队列Kafka使用的语言Scala等
当下可以被JVM执行的语言有:Java,kotlin,Scala,Clojure,Groovy,Jython,JRuby,Ceylon,Eta,Haxe。
JVM有很多版本,但我们接触的一般就是Oracle的Hospot虚拟机。
学习JVM前我们首先要可以弄明白Java为什么这么牛逼,这么炙手可热?
Java10多年前就出来了,当时还只是做桌面程序,到现在做成了后端一哥地位。是什么原因呢?
我认为有如下几点:
1、Write once, run anywhere,跨平台性。
2、GC,垃圾回收机制。
3、牛逼的生态。
这其中1、2两点都离不开JVM。
JVM组成部分
JVM组成大致为三个部分(Hospot & jdk1.8):
1、类加载子系统:主要完成class文件加载到内存区域
2、执行子系统:执行代码
3、运行时数据区:主要内存区域,包括虚拟机栈、程序计数器、堆、方法区、本地方法栈
其中虚拟机栈、程序计数器为线程独有。
如图是JVM的一个逻辑划分:
注:在1.8以及以后的版本中永久代和常量池被替换成了元空间,元空间是直接放在堆外内存的,字符串放到了堆中。但是从逻辑上都属于方法区。
类加载过程
类加载过程
1、加载:把磁盘中的class文件读取到元空间
2、验证:验证class文件是否正确,例如magic是否为cafebabe
3、准备:给静态变量分配内存,并附上默认值,例如int默认值为0
4、解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方 法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过 程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。
5、初始化:把静态变量的设置为初始值,执行static静态代码块
6、使用:使用对象
7、卸载:无引用那么也可能被卸载
类加载器有哪些:
我们可以通过class.getClassLoader()来获取类的加载器
BootstrapClassLoader:jvm最顶层的加载器,主要加载jdk中核心的class,如rt.jar。比如java.lang.String.class
ExtClassLoader:加载附加class,主要是Ext下面的jar包
AppClassLoader:自定义类加载器:开发者自定义ClassLoader,比如tomcat等容器自定义ClassLoader来管理每个项目的class
双亲委派
双亲委派模式就是当一个类加载器要加载类的时候,首先需要让上级类加载器加载,如果上级加载成功了,就以上层返回结果为结果
对象创建过程
对象分配主要是:检查类是否加载、内存分配、初始化、设置对象头、执行<init>方法
1、检查类是否加载,收到创建指令(new、反射、clone)后,对象要创建首先需要有class信息,不然怎么执行对象相关代码、怎么引用对象相关属性。所以第一步要确保已经类加载完成,没有加载就尝试加载。
2、分配内存:这个过程就是给对象在JVM划分一块内存区域。(详见:对象分配过程)
3、初始化:内存分配成功后的内存区域(除开对象头)都会被初始化为0。(TLAB是在分配时就会初始化)
4、设置对象头:在Java中每个对象都有一个对应的对象头,对象头的数据包括了一些对象的锁状态,分代年龄等信息。
5、执行<init>:这个就是对相关成员变量进行赋值和执行调用的构造方法。
对象分配过程
这个过程就是给对象在JVM划分一块内存区域。
内存划分的方法:
两种方式
1、指针碰撞:内存中的区域绝对规则,有指针来划分空闲内存和使用内存的边界。当需要为对象划分内存的时候我们,我们就直接在把指针向空闲的一边移动需要的内存即可。
2、空闲列表:这种情况是堆中的区域比较零散的时候有的,那么无法通过指针碰撞来移动,JVM就采用一个数据结构来记录空闲的空间(空闲列表)。当需要对象内存分配的时候,就直接去空闲列表找。
并发处理方法:
主要有两种方式
1、CAS:在执行的时候进行CAS乐观锁控制。
2、TLAB:在线程本地划分一块区域(开启 TLAB XX:+UseTLAB 执行TLAB -XX:TLABSize)
对JVM整体有了一个比较完整的认识之后就可以看一下对象内存分配的流程:
1、栈上分配
(开启逃逸分析:-XX:+DoEscapeAnalysis 关闭逃逸分析:-XX:-DoEscapeAnalysis)
2、大对象分配老年代
(-XX:PretenureSizeThreshold=1024000 (单位是字节)设置对象大小)
3、分配Eden区
垃圾回收
什么样的对象可以被当成垃圾回收掉呢?
在自动回收的算法中大致有两种算法:
1、引用计数法
引用计数顾名思义:就是没有一个对象引用就计数一次,那么当对象的计数为0的时候就认为该对象可以被回收。
该方法有一个大问题就是不好区分循环引用的问题,比如A对象中引用了B对象,同时B对象也引用了A对象,但是这两个对象都是垃圾对象,此时就无法判断。
2、可达性分析算法
为了解决引用计数法的问题,出现了可达性分析算法:
JVM定义了一个GCRoots根,只要对象通过引用方式可以达到GCRoots根那么就认为不是垃圾,反之就是垃圾。
常见的GCRoots根
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象
GC算法
- 标记-清除法
- 标记-整理算法
- 复制算法
- 分代算法(更像是一种思想)
常见的垃圾回收器
- Serial
- SerialOld
- ParNew
- CMS
- Parallel Scavenge
- Parallel Scavenge Old
- G1
- ZGC
从Eden区域开始分配
对象分配中大多数都是走的Eden区域分配,TLAB,大对象直接老年代都是一些优化手段。
1、对象分配到Eden区
2、Eden空间满了,进行GC回收Eden区域,把存活的对象放入S1
3、S1到达阀值那么利用赋值算法清空
调优工具
jps
#查看java进程 ,有事
jps -l
jamp
#查看对内存信息
jmap -heap [pid]
#dump内存日志
jmap -dump:format=b,file=[filename.prof] [pid]
#查看对象分布信息
jmap -histo [pid]
jstat
#查看GC信息 相对于-gcutil信息全一些
jstat -gc [pid]
#查看GC信息
jstatc -gcutil [pid]
#[ms]:多少毫秒打印一次 ,[times]打印多少次
jstat -gc [pid] [ms] [times]
jinfo
jstat -flags [pid]
jstack
#打印线程堆栈
jstack [pid] >> [file.text]
jvisualvm
直接在JAVA_HOME/bin 下面运行jvisualvm即可
可下载GC插件
可导入jmap堆栈日志
Arthas
这是一款阿里巴巴开源的JVM性能调优工具
常见名词
- 类加载
- 双亲委派
- 指针碰撞
- 空闲列表
- CAS
- TLAB
- 逃逸分析
- 标量替换
- 老年代空间担保机制
- 对象动态年龄判断机制
- GC算法
- GCRoots
- 垃圾回收器
- CMS
- G1
- ZGC
- 三色标记算法
- 记忆集与卡表
- 读写屏障
更多推荐
所有评论(0)