JVM

欲渡黄河冰塞川,将登太行雪满山。 Docker中跑的JVM,总是有奇奇怪怪的问题,我们先说概念,后谈GC优化以及工具的使用

为什么会有JVM

write once run anywhere - 一次编译到处运行说的是Java语言的跨平台特性。

C语言就不是跨平台的,你写Linux的C和Windows的C,调用同样功能的操作系统API(比如windows上的读文件和Linux上的读文件)可能存在较大差异。

Java语言是跨平台的,你写Linux的Java和Windows的Java,调用的是同一套JVM的API。至于JVM最终会在不同操作系统中如何调用底层API执行,那就不是我们关心的了。

Java虚拟机(JVM)类似于一个操作系统,所有Java程序员无论在MAC还是Windows还是Linux上编码时都面对JVM这个操作系统即可,调用的是JVM提供的API,JVM再根据不同的操作系统将自己本身的API调用转换成为操作系统的API调用。

所以说,Java的跨平台特性与Java虚拟机密不可分。

我们从一个标准的HelloWorld.java文件的编码到运行说起,有三个步骤:

1.编码

在HelloWorld.java中编写代码

2.编译

使用javac HelloWorld.java 得到HelloWolrd.class文件

3.运行

java HelloWorld

4.调试

你是否探究过每一步操作的背后有着怎样具体的操作?怎么就做到跨平台了呢?我们从每一步展开来说

1.编码:无论你是MAC还是Windows,编码这一步的操作是一样的,本质上是新建一个后缀为.java的文本文件

2.编译:使用javac命令的前提是你的电脑中已经安装了JDK。好的,我们在安装JDK时需要根据当前的环境来下载相应的版本, 比如MAC OS版本、Win 64bit版本、Win 32bit版本。编译之后得到HelloWolrd.class文件。.class文件是可以运行在任意版本的Java虚拟机(JVM)上的文件。

3.运行:输入java HelloWorld是在告诉系统使用Java虚拟机(JVM)来运行我的HelloWolrd。JVM是通过安装JRE得到的, 和JDK一样,我们在安装JRE时需要根据当前的环境来下载相应的版本。JVM将.class文件解释翻译成当前机器(目标机器)可识别的目标机器码,然后执行目标机器代码, 在这一步操作中,MAC OS版本的JVM会将.class翻译成MAC OS可以识别的机器码、Win 64bit版本的JVM会将.class翻译成Win 64bit可以识别的机器码...以此类推

在第3步,由于解释执行的速度过慢,于是就有了JIT(即时编译技术),可以将字节码直接转换成高性能的本地机器码来执行,而不是遇到每一行代码都先解释再执行。

在Java8时代,解释执行和即时编译技术混合使用并驾齐驱,热点代码会被直接JIT成目标机器码,非热点代码还是保持解释执行的方式

在Java9时代,"AOT"提供了将所有代码直接编译成机器码的方式

字节码的命名由来

随意打开一个*.class文件,可以看到开头一定是这样的

cafe babe 0000 0034 0052 0a00 1200 2b09

第一个字节是Java之父定义的一个魔法数,标志这个文件是class文件 第二个字节代表了java的版本号,00034是52,代表了JDK-52-version

一个字节8位,可以描述256种指令,每个指令意味着对JVM的一个操作码。 在x86计算机中00001111的字节码很难被人类阅读,于是有了汇编助记符,比如SADD\LOAD 当然,类似这种00001111的字节码,只适合机器阅读,所以JVM也发明出了一套汇编助记符,比如ICONST\IPUSH\ILOAD|GETFIELD

对象实例化的过程

Object o = new Object();

新建一个对象,分为多个步骤,分别是:

  • 使用当前类加载器ClassLoader+包名+类名作为key找到.class文件,如果没找到,抛出ClassNotFoundException
  • 如果有父类,初始化父类->初始化父类的static变量和方法块->计算占用内存->对父类成员变量设置默认值->设置对象值->调用构造函数
  • 初始化子类static变量和方法块
  • 计算对象的占用内存
  • 对成员变量设置默认值,不同的数据类型有不同的零值
  • 设置对象头
  • 调用类的构造方法

JVM内存模型

JVM内存区域

堆和非堆。其中堆是程序员可用可控的内存,非堆内存则相反。

堆内存=Heap Space= Old(年老代) + New{Eden,From Survivor,To Survivor}(年轻代)

非堆内存=Persitence Space(持久代)

新建一个对象的内存分配顺序

1.对象被new出来后,首先被放到Eden区。大对象直接进入老年代

 2.Eden足够时,内存分配结束。Eden区不够时,执行下一步3.JVM做YoungGC,将Eden空间中存活的对象放到Survivor区4.Survivor区用作Eden区和Old区的中间交换区域。当Old区空间足够时,Survivor存活了一定次数的对象会被移到Old区。如果在YounGC后Survivor放不下,将超出的部分挪到老年代5.当Old区空间不够时,JVM做FullGC6.若FullGC后,Survivor区及老年代仍然无法存放Eden区复制过来的对象,导致Out Of Memory
复制代码

JVM参数

—Xmx    最大堆内存(与—Xmx一致)-Xms    初始化堆内存(与—Xms一致)堆内存大于60%时,会增加到-Xmx;堆内存小于30%时,会减少到-Xms,为了减少频繁调整的次数,两者设置成一样-Xss    每个线程栈大小-Xmn    年轻代大小,推荐为堆大小的3/8-XX:MaxPermSize    持久代最大-XX:PermSize    持久代初始-XX:+PrintGCApplicationStoppedTime    打印详细GC日志-XX:PretenureSizeThreadhold    大于该值的对象直接分配到Old区。避免在Young区之间的大量拷贝
复制代码

两种GC

新创建的对象都在分配在Eden中。YoungGC的过程就是将Eden和使用中的Survivor移动到空闲的Survivor中,有一部分对象在经历了几次GC之后就会被移动到old区(可以通过参数设置)

JVM使用经验

热机批量启动容易踩坑

由于JVM刚启动时,所有的方法被执行次数都是0,热点代码还没有被JIT进行动态编译,所有的方法都是解释执行,这个时候性能是很低的

假如我们有100台机器需要做更新,不应该一次性全部更新100台机器,否则很容易产生因为性能底下导致的全部宕机

合理的方式是:每间隔一段时间更新20台机器,当上一批机器从冷机预备到热机(进入JIT)后,再进行下一批的更新

解决类冲突

我们常常在启动应用的时候,发现ClassNotFoundException,可是从逻辑来讲,我们的类明明应该被加载;

这种情况,要么是两个jar包中有多个同名的类,两者之间进行相互覆盖,使Spring找到两个类而且不知道使用哪个 或者是根本没有加载该jar包

这是可以通过两种方式:

  • JVM启动参数加入 -XX:+TraceClassLoading参数,打印出JVM加载的所有类的全限定名
  • 在ClassLoader的loadClass(String name, boolean resolve)方法/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/src.zip!/java/lang/ClassLoader.java:401 打断点,设置condition为图:

OOM

如果发生OOM后,JVM是直接挂掉了,我们连查GC日志的机会都没有。如果希望得到OOM时的堆信息,我们需要开启-XX:HeapDumpOnOutOfMemoryError, 让JVM发生异常时能输出堆内信息,特别是对几个月才出现一次OOM的应用来说非常有帮助

可能原因

  • Old区溢出 可能是Xmx过小或者内存泄漏导致,例如循环上万次的序列化,创建大量对象。 还有的时候系统一直频繁FullGC,根本无法响应用户的请求

  • 持久代溢出 动态加载大量Java类导致,只能通过调大 —XX:MaxPermSize

常用的JVM分析工具

jps

查看当前机器运行的JVM线程

[root@96e6a9290fca /]# jps
1 jar
222 Jps
复制代码

jstack

查看某个Java进程内的线程堆栈信息 这个命令可以帮助我们定位到线程堆栈,再找到对应的代码,比如,我们想找出进程ID为1的耗时最大的线程

找到JVM中最耗时的线程

top -Hp 1

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND120 root      20   0 9981.8m 857652  13880 S  7.0  2.6   0:00.76 java124 root      20   0 9981.8m 857652  13880 S  4.0  2.6   0:02.56 java16 root      20   0 9981.8m 857652  13880 S  0.3  2.6   0:00.98 java
复制代码

找到TIME最多的线程号124,计算出16进制数

printf "%x\n" 124

7c
复制代码

找到进程1中的线程ID为7c的堆栈信息

jstack 1 | grep 7c

"XNIO-1 task-41" #108 prio=5 os_prio=0 tid=0x00007f941038b800 nid=0x7c waiting on condition [0x00007f94c8ffb000]
"XNIO-1 task-17" #80 prio=5 os_prio=0 tid=0x00007f941037c800 nid=0x60 waiting on condition [0x00007f94d0d38000]- locked <0x00000006c7427c48> (a java.util.Collections$UnmodifiableSet)- locked <0x00000006c72c67c0> (a io.netty.channel.nio.SelectedSelectionKeySet)
复制代码

jmap

统计当前堆中各个对象的大小,到底是谁占用了内存!

jmap -histo:live 2297 | more

num     #instances         #bytes  class name
----------------------------------------------1:           392       16932248  [B2:         11198        1229280  [C3:          6647         445424  [Ljava.lang.Object;4:          3439         383824  java.lang.Class5:         11148         267552  java.lang.String
复制代码

jstat

实时监测JVM信息

jstat -gc 1 1000

S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
7168.0 7168.0 4200.2  0.0   190464.0 55153.9  3891200.0   149948.4  91648.0 86891.5 11008.0 10242.2    282    3.063   3      0.423    3.486
7168.0 7168.0 4200.2  0.0   190464.0 55227.0  3891200.0   149948.4  91648.0 86891.5 11008.0 10242.2    282    3.063   3      0.423    3.486
7168.0 7168.0 4200.2  0.0   190464.0 57357.0  3891200.0   149948.4  91648.0 86891.5 11008.0 10242.2    282    3.063   3      0.423    3.486
7168.0 7168.0 4200.2  0.0   190464.0 59528.5  3891200.0   149948.4  91648.0 86891.5 11008.0 10242.2    282    3.063   3      0.423    3.486
7168.0 7168.0 4200.2  0.0   190464.0 64430.1  3891200.0   149948.4  91648.0 86891.5 11008.0 10242.2    282    3.063   3      0.423    3.486
复制代码

各列含义:

  • S0C、S1C、S0U、S1U:Survivor 0/1区容量(Capacity)和使用量(Used)
  • EC、EU:Eden区容量和使用量
  • OC、OU:年老代容量和使用量
  • PC、PU:永久代容量和使用量
  • YGC、YGT:年轻代GC次数和GC耗时
  • FGC、FGCT:Full GC次数和Full GC耗时
  • GCT:GC总耗时

JVM性能调优实战

JVM速度慢的很大一部分原因是因为无法及时释放所不需要的内存。在编写java程序时,我们不需要自己释放内存,而是交由GC回收。如此一来,对堆内存和GC算法的掌握决定了是否可以发挥JVM的整体性能。

调优原则

1.Young GC尽可能多地回收新生代对象
2.堆内存越大越好
3.一切以减少Full GC为目的:1.如果大对象过多,新生代没有足够的内存分配,造成新生代的对象往老生代上迁移,老生代逐渐变多,触发Full Gc。2.如果大对象直接放到老生代,也会产生老生的Full GC频繁的问题,所以,一定要尽量减少使用大对象,如果一定要使用,那就保持其最短的生命周期,最好作为临时变量
4.虽然加大内存有利于减少GC收集的次数,但是大内存的一次Full GC时间也会更长。所以,对于大内存的Java应用(现在似乎都是这样),一定要尽量减少Full GC的次数
手段主要有两个:1.避免对象生命周期过长,在不需要的时候及时释放,让对象被Young GC回收,避免对象被移动到老年代2.提高大对象进入老年代的门槛:设置-XX:PretrnureSizeThreshold为一个比较大的值,小于该值的对象都先进入新生代,然后被Young GC回收,只有小概率事件会进入老生代
复制代码

JVM崩溃的几大原因:

1.异步处理请求:对接受到的请求开辟一个线程去保持TCP连接,如果异步处理请求慢,则TCP连接过多,造成线程数过多,JVM崩溃

2.使用了netty等NIO框架,JVM在JVM内存之外分配直接内存,导致直接内存过大,发生内存泄露

高并发例子

JVM使用例子-承受海量访问的动态Web应用

服务器配置:8 CPU, 8G MEM, JDK 1.6.X

参数方案:

-server -Xmx3550m -Xms3550m -Xmn1256m -Xss128k -XX:SurvivorRatio=6 -XX:MaxPermSize=256m -XX:ParallelGCThreads=8 -XX:MaxTenuringThreshold=0 -XX:+UseConcMarkSweepGC

调优说明:

  • -Xmx 与 -Xms 相同以避免JVM反复重新申请内存
  • -Xmn1256m 设置年轻代大小为1256MB。官方推荐配置年轻代大小为整个堆的3/8。
  • -Xss128k 设置较小的线程栈以支持创建更多的线程,支持海量访问,并提升系统性能。
  • -XX:SurvivorRatio=6 设置年轻代中Eden区与Survivor区的比值。系统默认是8,根据经验设置为6,则2个Survivor区与1个Eden区的比值为2:6,一个Survivor区占整个年轻代的1/8。
  • -XX:ParallelGCThreads=8 配置并行收集器的线程数,即同时8个线程一起进行垃圾回收。此值一般配置为与CPU数目相等。
  • -XX:+UseConcMarkSweepGC 设置年老代为并发收集。CMS(ConcMarkSweepGC)收集的目标是尽量减少应用的暂停时间,减少Full GC发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代内存,适用于应用中存在比较多的长生命周期对象的情况。

几点原则

1.Server端设置-Xms和-Xmx为相同值。为了优化GC,最好让-Xmn值约等于-Xmx的1/3或2/3。
2.堆大小并不决定进程的内存使用量。进程的内存使用量要大于-Xmx定义的值,因为Java为其他任务分配内存,例如每个线程的JVM函数栈等。
3.JVM函数栈的设定每个线程都有他自己的JVM函数栈。-Xss为每个线程的JVM函数栈大小
JVM函数栈的大小限制着线程的数量。如果JVM函数栈过大就会导致内存溢漏。-Xss参数决定JVM函数栈大小,例如-Xss1024K。如果JVM函数栈太小,也会导致JVM函数栈溢漏。
复制代码

调优步骤

Young Gc频率高->新生代太小,总是不够->增大新生代

Young Gc时间长->新生代太大,很久才做一次Gc->减少新生代

JVM_内存模型详解相关推荐

  1. Java 内存模型详解

    概述 Java的内存模型(Java Memory Model )简称JMM.首先应该明白,Java内存模型是一个规范,主要规定了以下两点: 规定了一个线程如何以及何时可以看到其他线程修改过后的共享变量 ...

  2. 【JVM】JVM内存模型详解

    一.JVM是什么? JVM是Java Virtual Machine(Java虚拟机)的缩写,是通过在实际的计算机上仿真模拟各种计算机功能来实现的.由一套字节码指令集.一组寄存器.一个栈.一个垃圾回收 ...

  3. Java基础:由JVM内存模型详解线程安全

    1.前言 最近在研究JVM内存模型和Java基础知识.主要讲的是线程共享变量与线程私有变量以及如何写出线程安全的代码.这里列出一条规则,"类中的成员变量,也叫实例变量,也叫全局变量,它是非线 ...

  4. 深度历险:Redis 内存模型详解

    Redis 是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说 Redis 是实现网站高并发不可或缺的一部分. 我们使用 Redis 时,会接触 Redis 的 5 种对象 ...

  5. JMM内存模型详解(一)

    本文开始死磕JMM(Java内存模型)由于知识点较多,分来写 该文为JMM第一篇 技术往往是枯燥的,本文文字较多 1. JMM是什么? 其实JMM很好理解,我简单的解释一下,在Java多线程中我们经常 ...

  6. C++内存模型 详解

    一.C/C++内存模型基本概念 1.两大分区:代码区和数据区 2.四大分区:代码区,全局区(全局/静态存储区),栈区,堆区 3.C语言分区:堆,栈,静态全局变量区,常量区 4.C++语言分区:堆.栈. ...

  7. java 内存指针_java内存模型详解

    借用一句话:Java与C++之间有一堵内存动态分配和垃圾收集技术围成的高墙,墙外面的人想进来,墙里面的人却想出去. 一.我们为什么要了解JAVA内存 因为虚拟机帮我们JAVA程序员管理着内存,我们在n ...

  8. 【C++】C++对象模型:对象内存布局详解(C#实例)

    C++对象模型:对象内存布局详解 0.前言 C++对象的内存布局.虚表指针.虚基类指针解的探讨,参考. 1.何为C++对象模型? 引用<深度探索C++对象模型>这本书中的话: 有两个概念可 ...

  9. ASP.NET Core的配置(2):配置模型详解

    在上面一章我们以实例演示的方式介绍了几种读取配置的几种方式,其中涉及到三个重要的对象,它们分别是承载结构化配置信息的Configuration,提供原始配置源数据的ConfigurationProvi ...

最新文章

  1. 漫画:什么是 HTTPS 协议?
  2. UA MATH577 逻辑与可计算性1 递归函数
  3. 35所大学获批新增「人工智能」本科专业,工学学位、四年制
  4. sublime使用正则匹配
  5. python生成api文档_Django 自动生成api接口文档教程
  6. 使用php最容易犯的11个MySQL错误。
  7. Mybatis JdbcType与Oracle、MySql,javaType数据类型对应列表
  8. Times33算法与最快的Hash表
  9. 50道编程小题目之【质数的个数】
  10. web响应式图片设计实现
  11. day 39 mycql 数据库之约束
  12. BZOJ 2440 完全平方数
  13. tbb::atomic和std::atomic的区别 废弃
  14. 牛客网高级项目课总结
  15. 泰坦尼克号生存者预测(细节篇)
  16. 51Nod_1278 相离的圆【贪心+二分】
  17. 重大利好,区块链技术能保护森林资源?
  18. 计算机管理 服务无响应,电脑任务栏假死点击没反应的解决方法(win7与xp)
  19. 去掉dt和dd默认间隔的方法
  20. 【算法】图解A* 搜索算法

热门文章

  1. php微信商务平台 红包调用,微信平台红包接口怎么调用?微信支付商户平台红包发放接口调用图文教程[多图]...
  2. 小猫爪:i.MX RT1050学习笔记23-FreeRTOS移植之宇宙最详细
  3. 临界频带和听觉滤波器
  4. 招商银行(深圳)专场 — 纯前端表格技术应用研讨会
  5. 一个在你打开色情网站时触发录屏并上传的恶意软件
  6. 单体架构(Monolith)与微服务架构(MicroService)
  7. 程序设计之值班排班程序
  8. 揭秘5G,带你了解通信发展
  9. 癌症的中英文以及英文缩写
  10. 关于频数分布的区间对形成分布的影响