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后老年代占用,如果越来越大可能有内存泄露
  • 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强弱引用

  • 强引用:只要强引用还存在,垃圾回收器就永远不会回收掉被引用的对象
  • 软引用:在系统内存不够用时,这类引用关联的对象将被垃圾回收器回收
  • 弱引用:无论当前内存是否足够,都会回收掉只被弱引用关联的对象
  • 虚引用:最弱的一种引用,不会对对象的生命周期造成任何印象,也无法通过虚引用来取得一个对象的实例,唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值