JVM内存结构 VS Java内存模型 VS Java对象模型
JVM内存结构
Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,其中有些区域随着虚拟机进程的启动而存在。
程序计数器
概述:较小的内存空间,为当前线程执行的字节码的行号指示器
作用:通过改变计数器的值来指定下一条需要执行的字节码指令,来恢复中断前程序运行的位置
特点:
- 线程私有化,每个线程都有独立的程序计数器
- 无内存溢出
Java虚拟机栈
概述:每个方法从调用直到执行的过程,对应着一个栈帧在虚拟机栈的入栈和出栈的过程
作用:每个方法执行都创建一个“栈帧”来存储局部变量表、操作数栈、动态链接、方法出口等信息
特点: - 线程私有化
- 生命周期与线程执行结束相同
堆
创建时间:JVM启动时创建该区域
占用空间:Java虚拟机管理内存最大的一块区域
作用:用于存放对象实例及数组(所有new的对象)
特点: - 垃圾收集器作用该区域,回收不使用的对象的内存空间
- 各个线程共享的内存区域
- 该区域的大小可通过参数设置
方法区
作用:用于存储类信息、常量、静态变量、是各个线程共享的内存区域Java内存模型
Java内存模型是根据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是真实存在的。他只是一个抽象的概念,JMM是和多线程相关的,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。
在JMM中,我们把多个线程间通信的共享内存称之为主内存,而在并发编程中多个线程都维护了一个自己的本地内存(这是个抽象概念),其中保存的数据是主内存中的数据拷贝。而JMM主要是控制本地内存和主内存之间的数据交互的。Java对象模型
Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。三者区别
- JVM内存结构,和Java虚拟机的运行时区域有关。
- Java内存模型,和Java的并发编程有关。
- Java对象模型,和Java对象在虚拟机中的表现形式有关。
垃圾回收
GC垃圾收集器
JVM 有哪些垃圾回收器?
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。CMS收集器(多线程标记清除算法)
CMS(Concurrent Mark-Sweep)是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
在启动JVM参数加上-XX:+UseConcMarkSweepGC ,这个参数表示对于老年代的回收采用CMS。
CMS采用的基础算法是:标记–清除
CMS过程
- 初始标记
- 并发标记
- 并发预清除
- 重新标记
- 并发清理
- 并发重置
CMS的缺点
- CMS采用的基础算法是标记–清除。所有CMS不会整理、压缩堆空间。经过CMS收集的堆会产生空间碎片。虽然节约了垃圾回收的停顿时间,但也带来堆空间的浪费。
- 需要更多的CPU资源,为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切换是不靠谱的。
- CMS的另一个缺点是它需要更大的堆空间。因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回
收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,已 避免上面提到的情况:在回收完成之前,堆没有足够空间分配!默认当老年代使用68%的时候,CMS就开始行动了。 – XX:CMSInitiatingOccupancyFraction =n 来设置这个阀值。CMS的使用场景
如果你的应用程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU(也就是硬件牛逼),那么使用CMS来收集会给你带来好处。还有,如果在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS。G1收集器
G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一。早在JDK7就已加入JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术,JDK9 默认就是使用的G1垃圾收集器。
不同于其他的分代回收算法,G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。每块区域既有可能属于O区、也有可能是Y区,且每类区域空间可以是不连续的(对比CMS的O区和Y区都必须是连续的)。
G1有三个明显特点:1、压缩空间强,避免碎片 2、空间使用更灵活 3、GC停顿周期更可控, 避免雪崩
一次完整G1GC的详细过程: - YGC(不同于CMS)
- 并发阶段
- 混合模式
- full GC(一般在G1出现问题时发生)
目前CMS还是默认首选的GC策略、可能在以下场景下G1更适合: - 服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
- 应用在运行过程中产生大量内存碎片、需要经常压缩空间
- 想要更可控、可预期的GC停顿周期:防止高并发应用雪崩现象
G1对比CMS的区别
- G1同时回收老年代和年轻代,而CMS只能回收老年代,需要配合一个年轻代收集器。另外G1的分代更多是逻辑上的概念,G1将内存分成多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。
- CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fully young gc和Mixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。
- G1在压缩空间方面有优势
- G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
- Eden,Survivor,Old区不再固定、在内存使用效率上来说更灵活
- G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象,可驾驭度,G1 是可以设定GC 暂停的 target 时间的,根据预测模型选取性价比收益更高,且一定数目的 Region 作为
CSet,能回收多少便是多少。 - G1在回收内存后会马上同时做,合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
- G1会在Young GC中使用、而CMS只能在O区使用
- SATB 算法在 remark 阶段延迟极低以及借助 RSet 的实现可以不做全堆扫描(G1 对大堆更友好)以外,最重要的是可驾驭度
Major GC和Full GC的区别是什么?触发条件呢?
针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
Partial GC:并不收集整个GC堆的模式
- Young GC:只收集young gen的GC
- Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
- Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。什么时候会触发full gc
- System.gc()方法的调用
- 老年代空间不足
- 永生区空间不足(JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据)
- GC时出现promotion failed和concurrent mode failure
- 统计得到的Minor GC晋升到旧生代平均大小大于老年代剩余空间
- 堆中分配很大的对象
可以作为root的对象
- 类中的静态变量,当它持有一个指向一个对象的引用时,它就作为root
- 活动着的线程,可以作为root
- 一个Java方法的参数或者该方法中的局部变量,这两种对象可以作为root
- JNI方法中的局部变量或者参数,这两种对象可以作为root
例子:下述的Something和Apple都可以作为root对象。Java方法的参数和方法中的局部变量,可以作为root.1
2
3
4
5
6public AClass{
public static Something;
public static final Apple;
''''''
}1
2
3
4
5public Aclass{
public void doSomething(Object A){
ObjectB b = new ObjectB;
}
}新生代转移到老年代的触发条件
- 长期存活的对象
- 大对象直接进入老年代
- minor gc后,survivor仍然放不下
- 动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代
什么情况对象直接在老年代分配
- 分配的对象大小大于eden space。适合所有收集器。
- eden space剩余空间不足分配,且需要分配对象内存大小不小于eden space总空间的一半,直接分配到老年代,不触发Minor GC。适合-XX:+UseParallelGC、-XX:+UseParallelOldGC,即适合Parallel Scavenge。
- 大对象直接进入老年代,使用-XX:PretenureSizeThreshold参数控制,适合-XX:+UseSerialGC、-XX:+UseParNewGC、-XX:+UseConcMarkSweepGC,即适合Serial和ParNew收集器。
高吞吐量的话用哪种gc算法
复制清除GC分代年龄为什么最大为15?
因为Object Header采用4个bit位来保存年龄,4个bit位能表示的最大数就是15Minor GC ,Full GC 触发条件是什么?
Minor GC ,Full GC 触发条件
- 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC;
- 对老年代GC称为Major GC;
- 而Full GC是对整个堆来说的;
在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。下边看看有那种情况触发JVM进行Full GC及应对策略。Minor GC触发条件
- 当Eden区满时,触发Minor GC。
Full GC触发条件
(1) System.gc()方法的调用
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI(Java远程方法调用)调用System.gc。
(2) 老年代空间不足
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
(3) 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC
(4) 对象太大,年轻代容不下动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold(jdk1.7默认是15)中要求的年龄。类加载
Java类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。JVM加载Class文件的原理机制
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
- 隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
- 显式装载, 通过class.forname()等方法,显式加载需要的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。什么是类加载器,类加载器有哪些?
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:
- 启动类加载器(BootstrapClassLoader) 用来加载java核心类库 java_home/jre/lit/rt.jar,无法被java程序直接引用。
- 扩展类加载器(ExtensionClassLoader ) 它用来加载 Java 的扩展库 java_home/jre/lib/ext/*.jar。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(SystemClassLoader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器(User-Definde ClassLoader),比如字节码加密了,自定义calssloader来加载并解密,继承ClassLoader类并重写findClass方法。
类装载的执行过程
类装载分为以下 5 个步骤:
- 加载:根据查找路径找到相应的 class 文件然后导入;
- 验证:检查加载的 class 文件的正确性;
- 准备:给类中的静态变量分配内存空间;
- 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
- 初始化:对静态变量和静态代码块执行初始化工作。
双亲委派模型
在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。双亲委派机制的作用
1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。双亲委派模型中有哪些方法。用户如何自定义类加载器 。怎么打破双亲委托机制
- 双亲委派模型中用到的方法:
- findLoadedClass(),
- loadClass()
- findBootstrapClassOrNull()
- findClass()
- defineClass():把二进制数据转换成字节码。
- resolveClass()
自定义类加载器的方法:继承 ClassLoader 类,重写 findClass()方法 。
- 继承ClassLoader覆盖loadClass方法
原顺序 - findLoadedClass
- 委托parent加载器加载(这里注意bootstrap加载器的parent为null)
- 自行加载
打破委派机制要做的就是打乱2和3的顺序,通过类名筛选自己要加载的类,其他的委托给parent加载器。在什么情况下需要自定义类加载器呢?
- 隔离加载类。 在某些框架内进行中间件与应用的模块隔离 , 把类加载到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar包不会影响到中间件运行时使用的 jar 包。
- 修改类加载方式。 类的加载模型并非强制 ,除Bootstrap 外 , 其他的加载并非定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。
- 扩展加载源。 比如从数据库、网络,甚至是电视机机顶盒进行加载。
- 防止源码泄露。 Java代码容易被编译和篡改,可以进行编译加密。 那么类加载器也需要自定义,还原加密的字节码。
内存溢出
内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。原因
引起内存溢出的原因有很多种,常见的有以下几种: - 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用的第三方软件中的BUG;
- 启动参数内存值设定的过小;
OOM可能发生在哪,怎么查看,怎么调优?
原因不外乎有两点
- 分配的少了:比如虚拟机本身可使用的内存太少。
- 应用用的太多,并且用完没释放
处理方法
- 先把内存镜像dump出来,有两种方法
- 设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息
- 使用jmap命令。”jmap -dump:format=b,file=heap.bin
“ 其中pid可以通过jps获取
2.得到内存信息文件后就使用工具去分析,也有两个工具
- mat: eclipse memory analyzer, 基于eclipse RCP的内存分析工具
- jhat:JDK自带的java heap analyze tool,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等
栈溢出
- 递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。
- 局部静态变量体积太大,局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。
- 指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。
解决办法
- 用栈把递归转换成非递归
- 使用static对象替代nonstatic局部对象
- 增大堆栈大小值
java应用系统运行速度慢的解决方法
问题解决思路: - 查看部署应用系统的系统资源使用情况,CPU,内存,IO这几个方面去看。找到对就的进程。
- 使用jstack,jmap等命令查看是JVM是在在什么类型的内存空间中做GC(内存回收),和查看GC日志查看是那段代码在占用内存。
首先,调节内存的参数设置,如果还是一样的问题,就要定位到相应的代码。 - 定位代码,修改代码(一般是代码的逻辑问题,或者代码获取的数据量过大。)
逃逸分析
逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用。正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象;故由于无法回收,即成为逃逸。
逃逸分析可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配,由于该对象一定是局部的,所以栈上分配不会有问题。泛型和类型擦除的关系
Java泛型的实现方法:类型擦除
Java的泛型是伪泛型。因为,在编译期间,所有的泛型信息都会被擦除掉。
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型。
类型擦除引起的问题:
- 1、先检查,在编译,以及检查编译的对象和引用传递的问题,java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的
- 2、类型擦除与多态的冲突和解决方法:桥方法
- 3、泛型类型变量不能是基本数据类型
- 4、泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
编译
即时编译器的优化方法
字节码可以通过以下两种方式转换成合适的语言:
- 解释器
- 即时编译器
即时编译器把整段字节码编译成本地代码,执行本地代码比一条一条进行解释执行的速度快很多,因为本地代码是保存在缓存里的编译过程的五个阶段
- 第一阶段:词法分析
- 第二阶段:语法分析
- 第三阶段:词义分析与中间代码产生
- 第四阶段:优化
- 第五阶段:目标代码生成
JVM、Java编译器和Java解释器
- Java编译器:将Java源文件(.java文件)编译成字节码文件(.class文件,是特殊的二进制文件,二进制字节码文件),这种字节码就是JVM的“机器语言”。javac.exe可以简单看成是Java编译器。注意,它不会执行代码
Java解释器:是JVM的一部分。Java解释器用来解释执行Java编译器编译后的程序。java.exe可以简单看成是Java解释器。注意,它会执行代码
JVM是Java平台无关的基础。JVM负责运行字节码:JVM把每一条要执行的字节码交给解释器,翻译成对应的机器码,然后由解释器执行。JVM解释执行字节码文件就是JVM操作Java解释器进行解释执行字节码文件的过程。 - JVM:一种能够运行Java字节码(Java bytecode)的虚拟机。
字节码:字节码是已经经过编译,但与特定机器码无关,需要解释器转译后才能成为机器码的中间代码。 - Java字节码:是Java虚拟机执行的一种指令格
Java字节码的执行有两种方式:
1. 即时编译方式:解释器先将字节码编译成机器码,然后再执行该机器码。
2. 解释执行方式:解释器通过每次解释并执行一小段代码来完成Java字节码程 序的所有操作。
无论是采用解释器进行解释执行,还是采用即时编译器进行编译执行,最终字节码都需要被转换为对应平台的本地机器指令。
从表象意义上看,重点就在:
解释:输入程序代码 -> 得到执行结果,从用户的角度看一步到位
编译:输入程序代码 -> 得到可执行代码
要得到执行结果还得再去执行可执行代码
疑问,解释器通过翻译将字节码转换为机器码,即时编译器通过编译将字节码转换为机器码,翻译?编译?为什么都是一样的操作??? - 每次读一代码就将字节码起转换(翻译)为JVM可执行的指令,叫翻译
- 一次性全部将字节码转换为JVM可执行的指令,叫编译
JIT 编译过程
当 JIT 编译启用时(默认是启用的),JVM 读入.class 文件解释后,将其发给 JIT 编译器。JIT 编译器将字节码编译成本机机器代码,下图展示了该过程。
即时编译器是 Java 虚拟机中相对独立的模块,它主要负责接收 Java 字节码,并生成可以直接运行的二进制码。
即时编译器与 Java 虚拟机的交互可以分为如下三个方面。 - 响应编译请求;
- 获取编译所需的元数据(如类、方法、字段)和反映程序执行状态的 profile;
- 将生成的二进制码部署至代码缓存(code cache)里。
Java 源代码是怎么被机器识别并执行的呢?
是目前 OpenJDK 使用的主流 NM, 它采用解释与编译混合执行的模式, 其 JIT 技术采用分层编译, 极大地提升了 Java的执行速度。机器码和字节码区别
高级程序代码 —(编译器)—> 字节码 —(解释器)—> 机器码
机器码:是电脑CPU直接读取运行的机器指令,运行速度最快。
字节码:是一种中间状态(中间码)的二进制代码(文件)。需解释器(也叫直译器)转译后才能成为机器码
字节码在运行时通过JVM(JAVA虚拟机)做一次转换生成机器指令,也就是说虚拟机执行字节码指令时是通过生成机器码交付给硬件执行
Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令(机器码)执行。这就是Java的能够“一次编译,到处运行”的原因。
字节码必须通过类加载过程加载到 JVM 环境后,才可以执行。执行有三种模式 第一,解释执行,第二, JIT编译执行 第三, JIT编译与解释混合执行(主流JVM 默认执行模式)
JVM调优
如何进行JVM调优?
何时进行JVM调优
- Heap内存(老年代)持续上涨达到设置的最大内存值;
- Full GC 次数频繁;
- GC 停顿时间过长(超过1秒);
- 应用出现OutOfMemory 等内存异常;
- 应用中有使用本地缓存且占用大量内存空间;
- 系统吞吐量与响应性能不高或下降。
JVM调优目标:
- 延迟:GC低停顿和GC低频率;
- 低内存占用;
- 高吞吐量;
JVM调优的步骤:
- 分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
- 确定JVM调优量化目标;
- 确定JVM调优参数(根据历史JVM参数来调整);
- 依次调优内存、延迟、吞吐量等指标;
- 对比观察调优前后的差异;
- 不断的分析和调整,直到找到合适的JVM参数配置;
- 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。
JVM性能调优的6大步骤
- 监控GC的状态
2、生成堆的dump文件
3、分析dump文件
4、分析结果,判断是否需要优化
5、调整GC类型和内存分配
6、不断的分析和调整JVM参数
如何查看Dump日志?怎么产生的?命令有哪些?
Dump文件是进程的内存镜像,可以把程序的执行状态通过调试器保存到dump文件中。主要是用来在系统中出现异常或者崩溃的时候来生成dump文件,然后用调试器进行调试。
如何dump出jvm日志。 - 在jvm启动的参数中,新增-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/admin/logs/java.hprof jvm参数。这样在发生jvm 内存溢出时,就会直接dump出java.hprof 文件了。
- 直接导出jvm内存信息。
jmap -dump:format=b,file=/home/admin/logs/heap.hprof javapid
推荐使用Eclipse插件Memory Analyzer Tool来打开heap.hprof文件。如何生成java dump文件
1、JVM的配置文件中配置:
- 在应用启动时配置相关的参数 -XX:+HeapDumpOnOutOfMemoryError,当应用抛出OutOfMemoryError时生成dump文件
2、通过jmap执行指令,直接生成当前JVM的dmp文件 - jmap -dump:file=文件名.dump [pid]
栈上分配和TLAB
栈上分配
JVM提供了一种叫做栈上分配的概念,针对那些作用域不会逃逸出方法的对象,在分配内存时不在将对象分配在堆内存中,而是将对象属性打散后分配在栈(线程私有的,属于栈内存)上,这样,随着方法的调用结束,栈空间的回收就会随着将栈上分配的打散后的对象回收掉,不再给gc增加额外的无用负担,从而提升应用程序整体的性能线程私有分配区TLAB
对象分配在堆上,而堆是一个全局共享的区域,当多个线程同一时刻操作堆内存分配对象空间时,就需要通过锁机制或者指针碰撞的方式确保不会申请到同一块内存,而这带来的效果就是对象分配效率变差(尽管JVM采用了CAS的形式处理分配失败的情况),但是对于存在竞争激烈的分配场合仍然会导致效率变差。因此,在Hotspot 1.6的实现中引入了TLAB技术。
TLAB全称ThreadLocalAllocBuffer,是线程的一块私有内存,如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率。
TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针_top撞上分配极限_end了),就新申请一个TLAB。TLAB空间主要有3个指针:_start、_top、_end。_start指针表示TLAB空间的起始内存,_end指针表示TLAB空间的结束地址,通过_start和_end指针,表示线程管理的内存区域,每个线程都会从Eden分配一大块空间(TLAB实际上是一块Eden区中划出的线程私有的堆空间),标识出 Eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配1
2
3
4
5
6
7
8
9class ThreadLocalAllocBuffer: public CHeapObj<mtThread> {
HeapWord* _start; // address of TLAB
HeapWord* _top; // address after last allocation
HeapWord* _pf_top; // allocation prefetch watermark
HeapWord* _end; // allocation end (excluding alignment_reserve)
size_t _desired_size; // desired size (including alignment_reserve)
size_t _refill_waste_limit; // hold onto tlab if free() is larger than this
.....................省略......................
}
当进行对象的内存划分的时候,就会通过移动_top指针分配内存(TLAB,Eden,To,From 区主要采用指针碰撞来分配内存(pointer bumping)),在TLAB空间为对象分配内存需要遵循下面的原则:
- obj_size + tlab_top <= tlab_end,直接在TLAB空间分配对象
- obj_size + tlab_top >= tlab_end && tlab_free > tlab_refill_waste_limit,对象不在TLAB分配,在Eden区分配。(tlab_free:剩余的内存空间,tlab_refill_waste_limit:允许浪费的内存空间)
- obj_size + tlab_top >= tlab_end && tlab_free < _refill_waste_limit,重新分配一块TLAB空间,在新的TLAB中分配对象
总体流程
对象分配流程图
Java 8: 从永久代(PermGen)到元空间(Metaspace)
在 Java8 中,永久代(PermGen)已经被移除,被一个称为“元空间(Metaspace)”的区域所取代。元空间并不在虚拟机中,而是使用本地内存(Native memory)
类的元数据信息(metadata)转移到Metaspace的原因是PermGen很难调整。PermGen中类的元数据信息在每次FullGC的时候可能会被收集。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。
由于类的元数据可以在本地内存(native memory)之外分配,所以其最大可利用空间是整个系统内存的可用空间。这样,你将不再会遇到OOM错误,溢出的内存会涌入到交换空间。最终用户可以为类元数据指定最大可利用的本地内存空间,JVM也可以增加本地内存空间来满足类元数据信息的存储。方法区和元空间是什么关系?
- 首先,方法区是JVM规范的一个概念定义,并不是一个具体的实现,每一个JVM的实现都可以有各自的实现;
- 然后,在Java官方的HotSpot 虚拟机中,Java8版本以后,是用元空间来实现的方法区;在Java8之前的版本,则是用永久代实现的方法区;
- 也就是说,“元空间” 和 “方法区”,一个是HotSpot 的具体实现技术,一个是JVM规范的抽象定义;
然后多说一句,这个元空间是使用本地内存(Native Memory)实现的,也就是说它的内存是不在虚拟机内的,所以可以理论上物理机器还有多个内存就可以分配,而不用再受限于JVM本身分配的内存了。为什么用元空间代替永久代?
类的元数据信息(metadata)转移到Metaspace的原因是PermGen很难调整。PermGen中类的元数据信息在每次FullGC的时候可能会被收集。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。
由于**类的元数据可以在本地内存(native memory)**之外分配,所以其最大可利用空间是整个系统内存的可用空间。这样,你将不再会遇到OOM错误,溢出的内存会涌入到交换空间。最终用户可以为类元数据指定最大可利用的本地内存空间,JVM也可以增加本地内存空间来满足类元数据信息的存储。