jdk、jre、jvm是什么关系?
- JRE(Java Runtime Environment),所有的java程序的运行环境,包含了java虚拟机(jvm),java基础类库(Java API)。
- JDK(Java Development Kit),是开发者用来编译、调试程序用的开发包。JDK也是Java 程序需要在JRE上运行。
- JVM(Java Virtual Machine),是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来。
引言
JVM则是JRE中的核心组成部分,承担分析和执行Java字节码的工作;对于Java开发人员来说,jdk、jre、jvm。在程序执行过程中,jvm和程序本身相较于jdk、jre联系更加紧密,加深对于jvm的了解,对于项目中一些看似明白却未解决的问题,会有所帮助,(对于jvm调优原理有一定帮助)所以本文对jvm进行详细说明。
JVM的基本特性:
● 基于栈(Stack-based)的虚拟机: 不同于Intel x86和ARM等比较流行的计算机处理器都是基于寄存器(register)架构,JVM是基于栈执行的。
● 符号引用(Symbolic reference): 除基本类型外的所有Java类型(类和接口)都是通过符号引用取得关联的,而非显式的基于内存地址的引用。
● 垃圾回收机制: 类的实例通过用户代码进行显式创建,但却通过垃圾回收机制自动销毁。
● 通过明确清晰基本类型确保平台无关性: 像C/C++等传统编程语言对于int类型数据在同平台上会有不同的字节长度。JVM却通过明确的定义基本类型的字节长度来维持代码的平台兼容性,从而做到平台无关。
● 网络字节序(Network byte order): Java class文件的二进制表示使用的是基于网络的字节序(network byte order)。为了在使用小端(little endian)的Intel x86平台和在使用了大端(big endian)的RISC系列平台之间保持平台无关,必须要定义一个固定的字节序。JVM选择了网络传输协议中使用的网络字节序,即基于大端(big endian)的字节序。
● Java 字节码
JVM使用Java字节码—一种运行于Java(用户语言)和机器语言的中间语言,以达到WORA的目的。Java字节码是部署Java程序的最小单元。
JVM组成
- JVM总体上是由类装载子系统(ClassLoader)、运行时数据区、执行引擎、垃圾收集组成。
- JVM调优主要是优化Java堆和方法区
类装载子系统(ClassLoader)
Class Loader类加载器负责加载.class文件
运行时数据区
方法区
方法区是各线程共享的内存区域,它用于存储已被JVM加载的类信息、常量、静态变量、运行时常量池等数据;
演变
在JDK 7以前,方法区也称为永久代,而到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替。这两个最大的区别就是:元空间不在虚拟机设置的内存中,而是使用本地内存(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。
设置方法区大小
方法区的大小不一定是固定的,JVM可以根据应用的需要动态调整。
方法区存储内容
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
方法区的内部结构
1.1类型信息
- 完整名称(包类.类名)
- 这个类的直接父类的完整名称(接口和java.long.object没有父类)
- 这个类型的修饰符(public,abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
1.2域(属性)信息
- JVM需要保存类型的域信息和域的声名顺序
- 域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
1.3方法信息
- JVM需要保存所有方法的信息及其声明的顺序
- 方法的名称,返回类型,参数(数量类型,按顺序),修饰符
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表(abstract和native方法除外)
● 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
1.4non-final的类变量(static) - 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
- 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它
●补充说明:全局常量(static final)被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
堆
Java堆是各线程共享的内存区域,在JVM启动时创建,这块区域是JVM中最大的, 用于存储应用的对象和数组,也是GC主要的回收区,一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:新生代、老年代、永久代。
新生代
其实看名称就能看出一些,一般情况下,新创建的对象都会存放到新生代中(大对象除外)。
新生代中对象的特点是:很快就会被GC回收掉的或者不是特别大的对象。
为了方便垃圾收集,新生代又分出了一个Eden区,两个 Survivor区。
JVM 每次只会使用 Eden区 和其中的一块 Survivor 区域来为对象服务,另一块Survivor区域是空的,用于垃圾回收。
举个例子,第一次回收的时候,虚拟机会将 Eden区+Survivor(from)区域的存活对象复制到Survivor(to)上(存活对象小于Survivor(to)的空间),清空Survivor(from),虚拟机使用Eden区+Survivor(to);
第二次回收的时候,虚拟机再将Eden区+Survivor(to)存活的对象复制到Survivor(from)。
这三个区域默认情况下是按照8:1:1分配,也可以手动配置
老年代
在新生代每进行一次垃圾收集后,就会给存活的对象“加1岁”,当年龄达到一定数量的时候就会进入老年代(默认是15,可以通过-XX:MaxTenuringThreshold来设置)。
另外,比较大的对象也会进入老年代,可以-XX:PretenureSizeThreshold进行设置。
如-XX:PretenureSizeThreshold3M,那么大于3M的对象就会直接就进入老年代。
因此,老年代中存放的都是一些生命周期较长的对象或者特别大的对象。
永生代
- 即JVM的方法区。在这里存放着一些被虚拟机加载的类信息(别忘了还有动态生成的类)的静态文件,这就导致了这个区中的东西比老年代和新生代更不容易回收。
- 永久代大小通过-XX:MaxPermSize=进行设置。
- jdk1.8后(包括1.8)永生代改称为元空间,和之前的区别是存储位置的不同,元空间不在虚拟机设置的内存中,而是使用本地内存;元空间存储类型信息,字段,方法,常量,但字符串常量池、静态变量仍然在堆中;
程序计数器
- 每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
- 是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
本地方法栈
- Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
- 本地方法栈,是线程私有的。
- 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
虚拟机栈
主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
执行引擎
执行引擎执行包在装载类的方法中的指令,也就是方法。执行引擎以指令为单位读取Java字节码。它就像一个CPU一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。
不过Java字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被JVM执行的语言。字节码可以通过以下两种方式转换成合适的语言:
● 解释器: 一条一条地读取,解释并执行字节码执行,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行语言的一个缺点。
● 即时编译器:用来弥补解释器的缺点,执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行。执行本地代码比一条一条进行解释执行的速度快很多,编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
垃圾收集
什么是垃圾收集
垃圾收集即垃圾回收,简单的说垃圾回收就是回收内存中不再使用的对象。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用对象),则没有被任何指针给指向,因此占用的内存也可以被回收掉。
垃圾回收的基本步骤分两步:
● 查找内存中不再使用的对象(GC判断策略)
● 释放这些对象占用的内存(GC收集算法)
GC判断策略
引用计数算法
引用计数算法是给对象添加一个引用计数器,每当有一个引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的对象。
缺点:很难解决对象之间相互循环引用的问题。
根搜索算法
根搜索算法的基本思路就是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(也就是说从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
在Java语言里,可作为GC Roots的对象包括以下几种:
● 虚拟机栈(栈帧中的本地变量表)中引用的对象;
● 方法区中类静态属性引用的对象;
● 方法区中常量应用的对象;
● 本地方法栈中JNI(Native方法)引用的对象。
注:在根搜索算法中不可达的对象,也并非是“非死不可”的,因为要真正宣告一个对象死亡,至少要经历两次标记过程:第一次是标记没有与GC Roots相连接的引用链;第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过)。
GC收集算法
标记-清除算法(Mark-Sweep)
标记-清楚算法采用从根集合(GC Roots)进行扫描,首先标记出所有需要回收的对象(根搜索算法),标记完成后统一回收掉所有被标记的对象。
该算法有两个问题:
● 效率问题:标记和清除过程的效率都不高;
● 空间问题:标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
复制算法(Copying)
复制算法是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象复制到另外一块上面, 然后把已使用过的内存空间一次清理掉。
标记-整理算法(Mark-Compact)
标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。
分代收集算法(Generational Collection)
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
新生代(Young Generation)的回收算法(以复制算法为主)
● 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
● 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
● 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
● 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
老年代(Tenured Generation)的回收算法(以标记-清除、标记-整理为主)
● 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
● 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
永久代(Permanet Generation)的回收算法
用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。永久代也称方法区。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过根搜索算法来判断,但是对于无用的类则需要同时满足下面3个条件:
● 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
● 加载该类的ClassLoader已经被回收;
● 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3.4.4 垃圾收集器
Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
ParNew收集器(停止-复制算法)
新生代多线程收集器,其实就是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
Parallel Scavenge收集器(停止-复制算法)
新生代并行的多线程收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
Parallel Old收集器(停止-复制算法)
老年代并行的多线程收集器,Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
CMS(Concurrent Mark Sweep)收集器(标记-清除算法)
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,CMS收集器是基于“标记–清除”(Mark-Sweep)算法实现的,整个过程分为四个步骤:
● 初始标记: 标记GC Roots能直接关联到的对象,速度很快;
● 并发标记: 进行GC Roots Tracing的过程;
● 重新标记: 修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但比并发标记时间短;
● 并发清除: 整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
● 优点:并发收集、低停顿
● 缺点:对CPU资源非常敏感、无法处理浮动垃圾、产生大量空间碎片。
G1(Garbage First)收集器(标记-整理算法)
G1是一款面向服务端应用的垃圾收集器,是基于“标记-整理”算法实现的,与其他GC收集器相比,G1具备如下特点:
● 并行与并发
● 分代收集
● 空间整合
● 可预测性的停顿
G1运作步骤:
● 初始标记(stop the world事件,CPU停顿只处理垃圾)
● 并发标记(与用户线程并发执行)
● 最终标记(stop the world事件,CPU停顿处理垃圾)
● 筛选回收(stop the world事件,根据用户期望的GC停顿时间回收)
3.4.5 垃圾收集结构图