JVM 原理与实战

用户头像
东哥
关注
发布于: 2020 年 08 月 20 日



JVM



JVM内存模型



五部分



方法区
加载类的定义数据、常量(运行时常量池)、静态变量、JIT编译后的代码
两种实现
对象实例在heap中分配好以后,需要在stack中保存一个4字节的内存地址。
程序计数器
当前线程执行的字节码的位置指示器。分支、循环、跳转、异常处理、线程恢复都依靠它。
java栈
java指令由 操作码(方法本身) 和 操作数(方法内部变量) 组成
方法本身、内部变量保存在stack中,对象地址在heap
虚拟机栈也叫栈内存,创建线程时创建,不存在垃圾回收。每个方法执行的同时创建栈帧用于存储局部变量表()、操作栈(记录出入栈)、动态链接、方法出口等信息,方法的执行就是入栈和出站
本地方法栈



概述



JVM初始运行都会分配好方法区和堆
没遇到一个线程,就为其分派一个程序计数器、虚拟机栈、本地方法栈,终止时释放。生命周期同线程
分清什么是实例,什么事对象:Class a = new A(); a 实例,A对象
类成员变量,不同对象各不相同;方法确实共享的
栈内存管理,较为简单,LIFO原则;堆空间内存管理较为复杂。
抛出异常方式不同



内存分配



1:前置条件: 关键字new
2:类加载检查
检查该指令的参数是否能在常量池中定位到一个类的符号引用
检查这个符号引用代表的类是否已经被加载、解析、初始化过。
3:检查未通过则进行类加载
4:检查通过则为对象分配内存
内存分配,根据堆内存是否规整,分为:空闲列表和指针碰撞
需要解决线程安全问题
同步处理分配内存空间的行为:CAS+ 失败重试
把内存分配行为 按照线程 划分在不同的内存空间进行
5:将内存空间初始化为0值
保证对象实例在使用时可不赋初值就可直接使用
6:对对象进行必要的设置
设置这个对象是哪个类的实例;并存放在对象的对象头中
7:判断后置条件,手动对对象初始化

**逃逸分析**
就是分析java对象动态作用域。当一个对象被定义之后,可能被外部对象引用,成为方法逃逸,也有可能是线程逃逸;如果没有逃逸,则进行站上分配,无需垃圾回收
**对象内存布局**
对象头:4个字节
MarkWord:是对象自身的运行时数据:哈希码、GC分代年龄、锁状态标识、线程池有的锁、偏向锁ID、偏向时间戳等,32或64位;非固定的数据结构方便在极小的空间存储尽量多的信息。
类型指针:该指针的长度为JVM的一个字大小。32位(4字节)或者64位(8字节)。开启指针压缩后 4 字节:UseCompressedOops
锁标志位;锁的是对象;锁状态:无-偏向-轻量级-重量级。可升级不能降级
实例数据
对齐填充 4个字节



JVM整体结构



![image](https://static001.geekbang.org/infoq/ad/adfe57adae5bd3ccd9e5ca046086f97c.png)



类加载



类加载概念



类加载流程



![image](https://static001.geekbang.org/infoq/af/af7c89d0f8e723200fd8d1d50e01160b.png)

加载
1:通过类的全限定名来获取定义此类的二进制字节流
2:这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只转化了数据结构,并未合并数据
3:在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。

链接:负责将类的二进制合并入JRE

验证
格式检查->语义检查->字节码验证->符号引用验证

准备
为类分配相应的内存空间,并赋零值。

解析阶段
将符号引用转化为直接引用

初始化阶段
开始执行java字节码;
主要工作是为静态变量赋值:初值
触发条件
new创建或则 getstatic、putstatic或者设置一个静态字段的值、的调用一个静态方法的时候,必须经过初始化
反射调用的时候
初始化的时候发现父类没有初始化,首先父类初始化
虚拟机启动时,需要制定一个main类,首先初始化这个类



双亲委派模型



**概述**
非一次性加载,而是按需加载
当用户加载器需要加载某个类时,会将请求转给父类加载器,委托帮忙加载
首先最顶层的加载,加载不到转交给子类夹杂器。如果还是不行排除异常
**加载器**
BootstrapClassLoader:JVM自身需要的类,c++实现
ExtClassLoader:ext/lib/jre/jdk下的
AppClassLoader:CLASSPATH下的
CustomerClassLoader
**实现**
ClassLoader.loadClass
先从缓存找改class,找到直接返回
未找到委托给父类夹杂器,没有父类自己加载
都没有找到通过自定义findClass方法去查询并加载
如果需要加载时进行解析,则进行解析
**双亲委派的好处**
为了安全性,避免核心类被替换
避免类的重复加载
**两个class是否为同一个对象**
类的完整类名必须一致
加载这个类的ClassLoader必须相同



自定义类加载器



应用场景
加密
java代码可以轻易反编译,需要加密防止反编译。需要先解密在加载
从非标准的来源加载代码
如果你的字节码是放在数据库、甚至是云端
**如何自定义**

如果不想打破:findClass
如果想打破: 重写整个loadClass



双亲委派的破坏-线程上下文类加载器



**获取**
我们可以通过Thread.getContextClassLoader()和setCOntextClassLoader();



Tomcat类加载器器结构



**作为一个web服务器要解决的问题**
部署在同一个web容器里的应用要类库要相互隔离
部署在同一个web容器上的两个web应用所使用的类库可以共享
web容器需要尽可能地保证自身的安全不受部署的应用的影响
**结构**
Bootstrap
ExtensionClassLoader
ApplicationClassLoader
CommonClassLoader
CatalineClassLoader SharedClassLoader
WebAppClassLoader
JasperClassLoader
**加载顺序**
先从缓存中加载
如果没有,从Bootstrap加载
如果没有,从当前类加载器加载按照 web-inf/classes、WEB-INF/lib的顺序
如果没有,从父类夹杂器,以AppClassLoader、COmmon、Shared的顺序加载



垃圾回收



finalize



finalize() 一次复活的机会 只有一次

finalize在java9中已经被废弃

原因:

- **性能问题**

会死锁和线程挂起

- **内存泄漏**

如果不需要也没办法取消垃圾回收,遇到这种情况,对象调用finalize方法就会无限期延后

- **java9对应**

AotoCloseable



引用级别



  • 强引用

  • 软引用

  • 弱引用

  • 虚引用



垃圾回收算法



什么是垃圾回收



GC:垃圾回收,即:Garbage Collection。
垃圾:特指存在于内存中的、不会再被使用的对象。
回收:清除内存中的“垃圾”对象。



垃圾回收算法



  • 引用计数法:java不用

  • 标记清除算法

  • 复制算法

  • 标记压缩算法(标记整理)

  • 分代算法

  • 分区算法



串行回收器



- GC单线程

- 运行时停止停止业务线程运行

- 单核时更加高效(减少线程切换)

- client模式下默认的



  • Serial

  • Serial Old



并行回收器



将串行回收器多线程化



  • Parallel New

  • Parallel Scavenge ParallelGC

  • Parallel Old ParallelOldGC

  • CMS :并发标记清除

  • G1回收器

  • 混合收集



JVM参数



  • 常用参数

  • Java [-options] [package+className] [arg1,arg2,…,argN] options: -Xms128m 设置初始化堆内存为128M -Xmx512m 设置最大堆内存为512M -Xmn160m 设置新生代大小为-Xmn160M(堆空间1/4~1/3) -Xss128m 设置最大栈内存为128M -XX:SurvivorRatio 设置新生代eden区与from/to空间的比例关系 -XX:PermSize=64M 设置初始永久区64M -XX:MaxPermSize=128M 设置最大永久区128M -XX:MaxMetaspaceSize 设置元数据区大小(JDK1.8 取代永久区) -XX:+DoEscapeAnalysis 启用逃逸分析(Server模式) -XX:+EliminateAllocations 开启标量替换(默认开启) -XX:+TraceClassLoading 跟踪类的加载 -XX:+TraceClassUnloading 跟踪类的卸载 -Xloggc:gc.log 将gc日志信息打印到gc.log文件中 -XX:+PrintGC 打印GC日志 -XX:+PrintGCDetails ** 打印GC详细日志 -XX:+PrintGCTimeStamps ** 输出GC发生的时间 -XX:+PrintGCApplicationStoppedTime GC产生停顿的时间 -XX:+PrintGCApplicationConcurrentTime 应用执行的时间 -XX:+PrintHeapAtGC 在GC发生前后,打印堆栈日志 -XX:+PrintReferenceGC 打印对象引用信息 -XX:+PrintVMOptions 打印虚拟机参数 -XX:+PrintCommandLineFlags 打印虚拟机显式和隐式参数 -XX:+PrintFlagsFinal 打印所有系统参数 -XX:+PrintTLAB ** 打印TLAB相关分配信息 -XX:+UseTLAB 打开TLAB -XX:TLABSize 设置TLAB大小 -XX:+ResizeTLAB 自动调整TLAB大小 -XX:+DisableExplicitGC 禁用显示GC (System.gc()) -XX:+ExplicitGCInvokesConcurrent 使用并发方式处理显式GC



JVM监控优化



  • Visual VM

  • 命令

  • linux

  • top能够实时显示系统中各个进程的资源占用情况。

  • 分为两部分:系统统计信息&进程信息。

  • 系统统计信息:

  • Line1:任务队列信息,从左到右依次表示:系统当前时间、系统运行时间、当前登录用户数。

  • Load average表示系统的平均负载,即任务队列的平均长度——1分钟、5分钟、15分钟到现在的平均值。

  • Line2:进程统计信息,分别是:正在运行进程数、睡眠进程数、停止的进程数、僵尸进程数。

  • Line3:CPU统计信息。us表示用户空间CPU占用率、sy表示内核空间CPU占用率、ni表示用户进程空间改变过优先级的进程CPU占用率。id表示空闲CPU占用率、wa表示待输入输出的CPU时间百分比、hi表示硬件中断请求、si表示软件中断请求。

  • Line4:内存统计信息。从左到右依次表示:物理内存总量、已使用的物理内存、空闲物理内存、内核缓冲使用量。

  • Line5:从左到右表示:交换区总量、已使用交换区大小、空闲交换区大小、缓冲交换区大小。 进程信息: PID:进程id USER:进程所有者 PR:优先级 NI:nice值,负值高优先级,正值低优先级 VIRT:进程使用虚拟内存总量 VIRT=SWAP+RES RES:进程使用并未被换出的内存。CODE+DATA SHR:共享内存大小 S:进程状态。 D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 %CPU:上次更新到现在的CPU时间占用百分比 %MEM:进程使用的物理内存百分比 TIME+:进程使用的CPU时间总计,单位 1/100秒 COMMAND:命令行vmstat性能监测工具,显示单位均为kb。它可以统计CPU、内存使用情况、swap使用情况等信息,也可以指定采样周期和采用次数。例如:每秒采样一次,共计3次。 procs列:r表示等待运行的进程数。b表示处于非中断睡眠状态的进程数。 memory列:swpd表示虚拟内存使用情况。free表示空闲内存量。buff表示被用来作为缓存的内存。 swap列:si表示从磁盘交换到内存的交换页数量。so表示从内存交换到磁盘的交换页数量。 io列:bi表示发送到块设备的块数,单位:块/秒。bo表示从块设备接收到的块数。 system列:in表示每秒的中断数,包括时钟中断。cs表示每秒的上下文切换次数。 cpu列:us表示用户cpu使用时间。sy表示内核cpu系统使用时间。id表示空闲时间。 wa表示等待io时间。iostat可以提供详尽的I/O信息。 如果只看磁盘信息,可以使用-d参数。即:Iostat –d 1 3 (每1秒采集一次持续3次) tps列表示该设备每秒的传输次数。 Blk_read/s列表示每秒读取块数。 Blk_wrtn/s列表示每秒写入块数。 Blk_read列表示读取块数总量。 Blk_wrtn列表示写入块数总量。

  • JDK

  • jps 用于列出Java的进程。 执行语法: jps [-options]jps 列出java进程id和类名 91275 FireIOTest jps –q 仅列出java进程id 91275 jps –m 输出java进程的入参 91730 FireIOTest a b jps –l 输出主函数的完整路径 91730 day1.FireIOTest jps –v 显示传递给JVM的参数 91730 FireIOTest -Xmx512m -XX:+PrintGC -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=51673:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8jstat用于查看堆中的运行信息。 执行语法:jstat –help jstat -options jstat <-option> [-t] [-h<lines>] <vmid> [<interval> [<count>]] jstat -class -t 73608 1000 5 查看进程73608的ClassLoader相关信息,每1000毫秒打印1次,一共打印5次,并输出程序启动到此刻的Timestamp数。 jstat -compiler -t 73608 查看指定进程的编译信息。 jstat -gc 73608 查看指定进程的堆信息。 Jstat -gccapacity 73608 查看指定进程中每个代的容量与使用情况 jstat -gccause 73608 显示最近一次gc信息 jstat -gcmetacapacity 73608 查看指定进程的元空间使用信息 jstat -gcnew 73608 查看指定进程的新生代使用信息 jstat -gcnewcapacity 73608 查看指定进程的新生代各区大小信息 jstat -gcold 73608 查看指定进程的老年代使用信息 jstat -gcoldcapacity 73608 查看指定进程的老年代各区大小信息 jstat -gcutil 73608 查看指定进程的GC回收信息 jstat -printcompilation 73608 查看指定进程的JIT编译方法统计信息jinfo用于查看运行中java进程的虚拟机参数。 执行语法: jinfo [option] <pid> jinfo -flag MaxTenuringThreshold 73608 查看进程73608的虚拟机参数MaxTenuringThreshold的值 jinfo -flag +PrintGCDetails 73608 动态添加进程73608的虚拟机参数+PrintGCDetails,开启GC日志打印。 jinfo -flag -PrintGCDetails 73608 动态添加进程73608的虚拟机参数+PrintGCDetails,关闭GC日志打印。jmap命令用于生成指定java进程的dump文件;可以查看堆内对象实例的统计信息,查看ClassLoader信息和finalizer队列信息。 执行语法: jmap [option] <pid> jmap -histo 73608 > /Users/muse/a.txt 输出进程73608的实例个数与合计到文件a.txt中 jmap -dump:format=b,file=/Users/muse/b.hprof 73608 输出进程73608的堆快照,可使用jhat、visual VM等进行分析jhat命令用于分析jmap生成的堆快照。 执行语法: jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>jstack命令用于导出指定java进程的堆栈信息



CPU 占用过高问题排查

  1. 查看系统状况

  2. 通过top 找到过载的pid

  3. 定位现成问题

  4. 使用ps -mp pid -o THREAD,tid,time 查看该进程的线程情况。

  5. 查看问题线程堆

  6. 将线程id换位16进制

  7. 就stack查看堆栈信息

  8. jstack pid |grep tid //十六进制

  9. jstat查看进程内存状况

  10. jstat -gcutil 6764 2000 10 查看垃圾回收统计

  11. jstack和jmap分析进程堆栈和内存状况

  12. 使用jmap命令导出heapdump文件,然后拿到本地使用mat、jprofile等工具

  13. jmap -dump:format=b,file=dump.bin 1

  14. jstack vmid

  15. jstack -l 2 >> jstack.out

  16. 从dump文件定位程序中的共从现场



用户头像

东哥

关注

还未添加个人签名 2018.03.25 加入

还未添加个人简介

评论

发布
暂无评论
JVM原理与实战