1.51 JVM的虚拟机
-
这个技术是什么
-
JVM是JAVA的运行环境的虚拟机,包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);
-
Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
-
Execution engine(执行引擎):执行classes中的指令。
-
JVM基础数据类型
-
基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress
-
引用类型包括:类类型,接口类型和数组。
-
两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
-
Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
-
Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。内存空间分为若干个区域
-
程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
-
Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
-
物理地址:栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
-
内存:栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
-
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
-
可见度:栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
-
队列和栈的区别
-
队列和栈都是被用来预存储数据的。
-
操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。
-
可操作的方式不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
-
操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。
-
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
-
Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;只有堆需要GC回收
-
物理地址:堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)
-
内存:堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
-
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
-
程序的可见度:堆对于整个应用程序都是共享、可见的。
-
方法区(Methed Area):用于存储已被虚拟机加载的类文件信息,和class文件常量池、常量、静态变量、即时编译后的代码等数据。
-
Java会存在内存泄漏吗?
-
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
-
但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
-
这个技术的应用场景
-
JVM是JAVA特有的运行环境,JAVA需JVM在运行
-
这个技术的优缺点
-
安装JDK之后,JVM实际上开发人员很少用到
-
垃圾回收也不用手动操作
-
只需了解运行机制和常见的调优就可以
-
这个技术的核心是什么
-
Java垃圾回收机制
-
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。
-
Java 提供的 GC (垃圾收集)功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。
-
程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
-
怎么判断对象是否可以被回收?
-
引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
-
可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
-
方法区回收
-
上面说的都是对堆内存中对象的判断,方法区中主要回收的是废弃的常量和无用的类。
-
判断常量是否废弃可以判断是否有地方引用这个常量,如果没有引用则为废弃的常量。
-
JVM 有哪些垃圾回收算法(可后续完善垃圾回收器)
-
标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
-
复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
-
标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
-
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
-
JAVA的内存分配
-
对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵循以下几种「普世」规则:
-
类加载器
-
类加载器是什么
-
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
-
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
-
类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。
-
JAVA中有几种类加载器
-
启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
-
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
-
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
-
用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
-
类装载的执行过程?类装载分为以下 5 个步骤:
-
加载:根据查找路径找到相应的 class 文件然后导入;
-
验证:检查加载的 class 文件的正确性;
-
准备:给类中的静态变量分配内存空间;
-
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
-
初始化:对静态变量和静态代码块执行初始化工作。
-
双亲委派模型?
-
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。为了保证加载避免重复以及安全性。
-
这个技术的原理
-
JAVA程序运行步骤
-
首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
-
再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;
-
运行字节码的工作是由解释器(java命令)来完成的
-
类加载器(ClassLoader)根据指定全限定名称将 class 文件加载到 JVM 内存,将其放在运行时数据区(Runtime data area)的方法区内,转化为 class 对象
-
执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行
-
调用其他语言的本地库接口(Native Interface)来实现整个程序的功能
-
这个技术的使用方式
-
JVM垃圾回收和程序运行都不需要人为操作
-
CPU占满的解决方式
-
先用top命令找出CPU占比最高的PID:top -c 按下P按照使用率排序,PID为进程ID
-
定位具体的线程ID:ps -mp 进程ID -o THREAD,tid,time
-
也可以通过 top -H -p pid 找到导致cpu高的线程
-
这是十进制的数据,转成十六进制为0xbda,使用printf "%x\n" 线程号将异常线程号转化为16进制
-
printf "%x\n" 11912
-
使用JVM自带的jstack命令导出当前所有线程的运行情况和线程当前状态
-
jstack 进程ID | grep tid(16进制线程ID小写英文)-A60
-
cat 3033.stack |grep 'bda' -C 8
-
JVM的垃圾回收器
-
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。
-
Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
-
ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
-
Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
-
Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
-
Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
-
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
-
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
-
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
-
G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
-
总结:新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
-
JVM中年轻代里的对象什么情况下进入老年代?
-
躲过15次GC之后进入老年代
-
通过年龄计数器判断一个对象是否需要转移。对象每经过一个GC仍然存活,年龄计数器加一。当年龄超过设定的值,会转移到老年代
-
动态对象年龄判断
-
如果再survivor区,有某一年龄的对象的总大小超过了survivor区大小的50%,则将这个年龄以上的对象全部转移到老年代
-
大对象直接进入老年代
-
所谓大对象就是指需要比较大的连续内存空间的Java对象,比如很长的字符串或数组
-
我们可以通过JVM参数-XX:PretenureSizeThreshold指定大对象的容量,单位是字节。
-
Minor GC后的对象太多,无法放入Survivor区
-
在MinorGC之后存活的对象超过了survivor区的大小,会将这些对象直接转移到老年代
-
老年代空间分配担保规则
-
老年代垃圾回收算法
-
总结:所谓的JVM优化,就是尽可能的让对象都在年轻代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免年轻代频繁的进行垃圾回收。
-
JVM调优
-
JVM调优目标:使用较小的内存占用来获得较高的吞吐量或者较低的延迟。
-
程序在上线前的测试或运行中有时会出现一些大大小小的JVM问题,比如cpu load过高、请求延迟、tps降低等,甚至出现内存泄漏(每次垃圾收集使用的时间越来越长,垃圾收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃,因此需要对JVM进行调优,使得程序在正常运行的前提下,获得更高的用户体验和运行效率。
-
重要指标
-
* 内存占用:程序正常运行需要的内存大小。
-
* 延迟:由于垃圾收集而引起的程序停顿时间。
-
* 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。
-
应用场景优化
-
CAP原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。
-
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具
-
调优可以依赖、参考的数据有系统运行日志、堆栈错误信息、gc日志、线程快照、堆转储快照等
-
jconsole:用于对 JVM 中的内存、线程和类等进行监控;
-
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
-
主要优化总结
-
年轻代大小选择
-
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
-
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
-
年老代大小选择
-
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
-
并发垃圾收集信息
-
持久代并发收集次数
-
传统GC信息
-
花在年轻代和年老代回收上的时间比例
-
减少年轻代和年老代花费的时间,一般会提高应用的效率
-
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
-
较小堆引起的碎片问题
-
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
-
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
-
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
-
JVM 调优的参数
-
堆设置
-
-Xms2g:初始化推大小为 2g;
-
-Xmx2g:堆最大内存为 2g;
-
-Xmn:年轻代内存最大允许大小,也可以缩写
-
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:2;
-
Xss512k:线程堆栈大小,jdk1.5及之后默认1M,之前默认256k
-
-XX:SurvivorRatio=8:设置新生代 Eden(生成区) 和 Survivor (幸存区)比例为 8:2;
-
收集器设置
-
-XX:+UseSerialGC:设置串行收集器
-
-XX:+UseParallelGC:设置并行收集器
-
-XX:+UseParalledlOldGC:设置并行年老代收集器
-
-XX:+UseConcMarkSweepGC:设置并发收集器,CMS + Serial Old 垃圾回收器组合
-
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-
垃圾回收统计信息
-
-XX:+PrintGC 开启打印 gc 信息;
-
-XX:+PrintGCDetails:打印 gc 详细信息。
-
-XX:+PrintGCTimeStamps
-
-Xloggc:filename
-
并行收集器设置
-
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-
并发收集器设置
-
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线
-
针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值
-
年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小
努力不一定成功,但不努力一定会失败~