jvm学习
jvm是什么
Java Virtual Machine(Java虚拟机)。从软件层面屏蔽底层硬件,指令细节,java跨平台的原因。整体结构如下
jvm生命周期
启动:通过根加载器创建一个初始类来完成,这个类由虚拟机的具体实现指定
执行:执行一个java程序的时候 就是虚拟机的一个进程
退出:执行完、执行出错异常终止、System.exit(0)。
字节码
前端编译器(javac):
程序想要运行,首先要将java源码编译为字节码文件(.class)。是二进制文件,内容就是jvm的指令,再由执行引擎解释/编译成机器码。机器就能识别。c、c++经编译器直接生成机器码,所以效率高。
符合jvm规范的字节码,jvm就能加载,不一定是java编写的
类加载
类加载器
负责将前端编译器编译生成的.class文件加载到内存中,生成对应的Class对象。
分类及其作用
- Bootstrap ClassLoader
根类加载器,也叫引导类加载器,负责Java核心类的加载。比如System,String等。在JDK中JRE的lib目录下rt.jar文件中。c c++编写。 - Extension ClassLoader
扩展类加载器。负责JRE的扩展目录中jar包的加载。在JDK中JRE的lib目录下ext目录 - Sysetm ClassLoader
系统类加载器。负责在JVM启动时加载来自java命令的class文件,以及classpath环境变量所指定的jar包和类路径(我们自己写的东西一般就是这个加载的) - jdk9变化:扩展类加载器->平台类加载器
自定义类加载器
作用:
隔离加载类:确保应用中依赖的jar不会影响到中间件运行的jar
扩展加载源:从数据库 网络加载二进制class文件
how:
继承ClassLoader类,重写loadClass 或者findClass(建议)
类加载过程
- 装载 (Loading)
- 分配一个基本的内存结构,将待加载类的二进制 class 文件, 装载到虚拟机。
- 链接 (Linking ):
- 字节码验证:验证字节码是否符合规范
- 准备:为类的静态变量分配内存,并将其赋默认值。final修饰的编译的时候就分配了
- 解析:将类、接口、字段、方法的符号引用转为直接引用
- 初始化(Initializing)
- 执行类中定义的静态代码块, 为类的静态变量赋初始值。发现其父类还没有进行过初始化、则需要先初始化其父类
类的加载时机
- 创建类的实例
- 访问类的静态变量,或者为静态变量赋值
- 初始化某个类的子类
隐式加载 vs 显示加载
- 显式加载
程序主动调用下列类型的方法去主动加载一个类
classloader.loadClass( className)
Class.forName( className) - 隐式加载
被显式加载的类对其他类可能存在如下引用:
继承、实现接口、域变量、方法定义、方法中定义的本地变量
被引用的类会被动地一并加载至虚拟机, 这种加载方式属于隐式加载
主动使用与被动使用
主动使用:new
被动使用:通过子类访问父类的静态变量,其子类不会执行Initializing
双亲委派机制
原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终将到达顶层的启动类加载器。
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
作用
- 避免类重复加载,确保类的唯一性
- 安全,防止代码植入(沙箱安全机制):
1、自定义java.lang.String(jdk的lang包下有的),加载这个String类的时候因为是我们自己写的,先用Sysetm ClassLoader加载,然后找父级Extension ClassLoader最后找到bootstrap classloader,它会把jdk的String类加载出来,就加载不到我们的类了。
2、自定义java.lang.xxx(jdk有lang这个目录,没有xxx这个类)。bootstrap classloader在java.lang包下加载xxx类,会直接报错。
在这种机制下这些系统的类已经被Bootstrap classLoader加载过了,所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
类的唯一性
同一个class文件,相同的类加载器加载。不同的加载器加载就是不同的类了
JVM的内存结构
程序运行起来,是一个进程,如果执行了五个线程。线程共用元空间、堆,各自有程序计数器、本地方法栈,栈
堆
存放对象实例 数组 常量池
Student stu = new Student(),栈里面放stu局部变量,指向栈里的对象
new 出来的对象包含三部分:对象头 实例数据 对齐填充(见对象在堆的存储布局)
新生代:老年代=1:2。 新生代中:eden:s0:s1=8:1:1。
s0 s1有一个一定为空,方便复制,实际操作是6:1:1,要显示指定才会是8。1.8用的是parallel收集器,会自动优化调整比例。谁空的就是s0。
如何进入老年代:
- 虚拟机初始时对象优先分配在Eden Space,许多对象也是在这里死去。新生代满了会执行“YGC(minor GC)”,仅针对它回收,还被其他对象引用的则放到from或者to并且长一岁,超过设置年龄(默认15岁)会到老年代中 。(为什么是15可以从对象头中找到答案)
- 相对于设置的参数(pretenureSizeThreshold) 大对象直接进入老年代
注意 :如果太小,即使大于设置的参数也不会到老年代 - 动态对象年龄判定:当新生代中某个年龄段的对象大于整体50%,则全部晋升到老年代;
- 空间分配担保:s0+Edgn 回收后 > s1空间
老年代区域被占满的时候,则会发送Major GC/FullGC
前者只回收老年代,后者是整堆和方法区的回收。只有部分垃圾回收器能单独回收老年代,所以很多时候都在混淆使用。
栈
程序的执行顺序 局部变量 对象的引用(每个线程独立栈)
栈帧是基本单位,执行的方法与栈帧一一对应
本地方法栈
调用本地方法,即调用c/c++的一些函数,开启线程native0方法等
元空间
- 存储类型信息、字段、方法。保存在本地内存,而不是虚拟机内存。新生代+老年代=堆空间大小
- 1.8以前叫方法区,也叫“永久代”,jdk1.8中移除整个永久代。取而代之的是一个叫元空间(Metaspace)的区域。具体原因是orcal的虚拟机和JRockit的虚拟机融合导致,JRockit是没有永久代的,oracle才有,并且用本地内存不容易发生full gc,所以沿用了元空间。
- 常量池、静态变量以前在方法区,现在在堆。方法区full gc才回收,放堆里方便回收。
程序计数器
存储下一条指令的地址,由执行引擎读取下一条指令。
在执行过程中,cpu会被不同的线程抢占,交替执行各个线程,再次执行时需要它明确下一条该执行什么
问 所有的对象都是在堆创建吗?
不是 逃逸分析发现,如果一个对象没有逃逸出方法,那么就可能被优化在栈上分配,也就无需垃圾回收即堆外存储技术。(new的对象就只在本方法使用就叫没有逃逸出方法,当成局部变量在使用。这都是编译器逃逸分析出来的)
对象在堆的存储布局
new Object()一般分为三部分
对象头 header
对象头分为两部分,各占8字节。如果属性什么都没有,new一个对象就占8字节。
对象标记 mark work
- 记录哈希码、gc标记、gc次数、同步锁标记、偏向锁持有者。分代年龄占4位,所以最多到15。
- 对象计算过hashcode之后无法使用偏向锁,因为偏向线程的id会覆盖掉hashcode值,会造成多次调用hashCode不一致。所以偏向锁和hashcode不共存。轻量级锁和重量级锁复制保存在其他地方了,可以共存。当已经持有偏向锁再去访问hashcode的话,会直接锁升级。
类型指针
指向元空间的class信息,压缩后可能小于8字节
实例数据
存放类的属性,包括父类的。比如有个int类型的属性,就会32位占4字节。
对齐填充
保证8个字节的倍数,虚拟机要求的
执行引擎
- 上面的前端编译器已经将源码编译为字节码文件,并且由类加载器加载到内存。
- 执行引擎的作用就是将字节码指令解释/编译为对应平台的本地机器指令。
- 为什么既有解释又有编译?因为java是半编译半解释语言。程序启动后解释器可以马上发挥作用,省去编译时间立即执行。jit编译器需要时间编译,但是编译完后缓存起来执行效率更高。
垃圾回收
是什么
jvm不定时回收不可达的对象(对象没有被引用),主要围绕堆。回收前会执行finalize()方法
堆 有gc 有溢出问题
元空间 有 有
栈/本地方法栈 无 有
程序计数器 无 无
怎么判断是否是垃圾
- 引用计数法(已淘汰)
有对象引用时计数器+1,引用失效-1,为0时回收。但无法解决循环引用的问题。(几个对象都没有地方用了,但是他们相互引用,计数器就是1,造成内存泄露) - 根引用算法(分析时要保证一致性,所以所有的垃圾回收器都会有stop the world)
依据:是否和根节点(gc roots有依赖)。根节点可以是- 虚拟机栈(栈桢中的本地变量表)中的引用的对象
- 局部变量 类静态属性和常量
- 本地方法栈中JNI的引用的对象;
垃圾回收算法
- 标记清除法
从引用根节点遍历,在对象头中标记被引用的对象,再清除没有标记的会产生空间碎片的问题(清除后产生的空间不是连续的) - 标记整理算法
上面的改进,标记后将不回收的压缩到内存的一端,按顺序排放。再清理边界以外的空间。 运用在老年代垃圾回收 - 复制算法
运用在新生代垃圾回收。对象初始化在eden,gc时有引用的对象复制进入to,from有引用的也复制进入to,清空eden和from。from和to名字会交换。
解决碎片化问题,快速,干净。会有一定的浪费空间。 - 分代算法
根据是什么代分别执行标记整理算法\复制算法。 - 分区算法
将一块大的内存分成若干小块,一次只回收一小块,减少stw的时间 - 增量收集算法
gc时要stw,让gc和用户线程交替执行,减少stw的时间。不过要考虑线程切换上下文的消耗
问:老年代和新生代回收算法是否可以交换
老年代GC的时候存活对象比较多,如果采用复制算法,复制太多性能低。浪费的空间也大。
新生代回收的对象多,所以把不回收的复制出来,再全部回收效率更高。
system.gc():手动触发full gc但不保证一定执行
垃圾回收器
衡量标准
- 吞吐量:执行用户线程的时间/(执行用户线程的时间+gc时间)
- stw时间
分类
按线程数
串行:只有一个线程回收垃圾,程序停止时间比较长
并行:多个线程同时进行回收垃圾,回收时也会stw 但较短
按工作模式
并发:垃圾回收与用户线程在同时进行(其实是交替工作),以尽量减少stw
独占:垃圾回收一开始就stw,直到结束
记忆集与写屏障
回收一个区时,遍历整个区的gc roots标记垃圾,但是这个区可能被其他区引用,那我们要遍历堆的所有的对象?
每个区维护一个记忆集,记录了引用这个区的其他区,自己引用自己的就通不过写屏障而不记录。gc就只用扫描记忆集里的区。所有的垃圾回收器都是这样的。G1特别明显,因为它是分区回收。
jvm调优
主要围绕堆
Student stu = new Student()。程序执行完栈里面的局部变量stu会自己出栈,而堆里面的对象不会,需要垃圾回收
常见参数
-Xms 堆初始值,默认为物理内存的1/64
-Xmx 堆最大可用值,默认为物理内存的1/4
-Xss: 每个线程栈的大小
-XX:+PrintGC 每次触发GC的时候打印相关日志。能看到回收前多大,回收后多大。各个地方分配了多大内存
-XX:+PrintGCDetails 更详细的GC日志
-XX:+HeapDumpOnOutOfMemoryErro :oom时生成dump文件
-XX:PermSize=64m -XX:MaxPermSize=256m:更改方法区的大小
-XX:SurvivorRatio 用来设置新生代中eden空间和Survivor(from+to)空间的比例.
-XX:NewRatio=4 设置新生代和老年代的内存比例为 1:4;
-Xmn: 新生代最大可用值
两种内存问题
内存溢出:申请的内存超出了JVM能提供的内存大小。
内存泄露:对象不会再被使用,但是gc回收不到
threadlocal里面的数据没有用remove()方法来释放数据
类的静态成员变量、单例对象、静态修饰的集合,生命周期同jvm
数据库、网络、io连接必须手动close,否则无法回收
常见三种oom
java.lang.OutOfMemoryError: Java heap space堆溢出
- 内存泄露:通过内存监控软件查找程序中的泄露代码
- 堆的大小设置不当,或者代码创建了太多对象、数组
java.lang.OutOfMemoryError: PermGen space 方法区溢出/Metaspace 元空间溢出
- 出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。
java.lang.StackOverflowError:栈溢出
- 不会抛OOM error,但也是比较常见的Java内存溢出。代码死循环或者深度递归调用,局部变量太多
调优之命令行
- jps (Java process status):查看正在运行的java进程。ps/top都可以看进程,前者静态,后者会根据cpu占用动态显示。
- -v 看虚拟机启动时设置的参数,没设置的看不到(jinfo)
- jstat
- -gc 进程id -t 1000 查看java进程对应的内存、垃圾收集的运行数据
1000是每秒打印一次,可以找一个时间段,看gc时间占比,如果超过20% 则堆的压力较大。查看gc后老年代占用,如果越来越大可能有内存泄露
- -gc 进程id -t 1000 查看java进程对应的内存、垃圾收集的运行数据
- jmap
- -dump:导出内存映像文件,是二进制文件,用visualVm可以打开。也可以设置jvm参数,每次oom的时候导出。
- jstack
- 进程id:可以看到对应进程里面所有线程的状态,排查死锁。
- jinfo
- -flags pid:查看虚拟机配置参数,还可以调整部分配置参数
调优之gui
- jdk自带的
- jconsole
- visualVm:可安装visualGC插件。有哪些线程、线程状态、有没有死锁、堆空间使用情况、生成dump文件(有哪些类 多少实例)。
- mat
擅长分析dump文件,分析内存 有没有泄露
导入dump文件后。查看饼图,会显示各个对象占用比例,从大到小看哪些有内存泄露的疑点。 - jprofiler
- jmeter:压测工具,测试API
- GC Easy在线分析
- 上传gc日志,在线分析 https://blog.csdn.net/qq_40093255/article/details/115376746
总结
- 调优是在保证没有oom的前提下,从吞吐量和stw两方面衡量系统。阐述oom的分类…可以设置相应参数。
- 调优要先监控:通过命令或者是工具查看内存使用情况,做相应调整,找到瓶颈、可能内存泄露的点。可以做如下操作
- 将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高吞吐量。
- 新生代的占比要比老年代少,便于在新生代回收(ygc比Major GC/FullGC消耗的资源要少得多)。如果gc后老年代占用一直增大则可能有内存泄露。
- 垃圾回收算法–>选择合适的垃圾回收器
- 最后可以通过gc easy分析gc日志。看吞吐量和stw有没有进步。
cpu突然飙高怎么处理
top查看哪个进程cpu使用率最高
top -Hp pid 查看是哪个线程。找到cpu使用较高的,线程id转为16进制
jstack pid | grep -A 20 tid 显示该线程的运行栈信息找到问题代码
jvm强弱引用
- 强引用:只要强引用还存在,垃圾回收器就永远不会回收掉被引用的对象
- 软引用:在系统内存不够用时,这类引用关联的对象将被垃圾回收器回收
- 弱引用:无论当前内存是否足够,都会回收掉只被弱引用关联的对象
- 虚引用:最弱的一种引用,不会对对象的生命周期造成任何印象,也无法通过虚引用来取得一个对象的实例,唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。