JVM

JVM

主要流程图

image

Java源码编译由以下三个过程组成:

  • 分析和输入到符号表
  • 注解处理
  • 语义分析和生成class文件
    image

编译时期-语法糖

语法糖可以看做是编译器实现的一些“小把戏”,这些“小把戏”可能使得效率大大提升

其中最值得说的就是泛型了,这个语法糖可以说被我们经常使用的

  • 泛型只会在Java源码中存在,编译过后会被替换为原来的原生类型(Row Type,也称为裸类型)。这个过程叫做:泛型擦除
    有了泛型这个语法糖后
  • 代码更加简洁【不用强制类型转换】
  • 程序更加强壮【只要编译时期没有警告,那么运行时期就不会出现ClassCastException】
  • 可读性和稳定性【在编写集合的时候,就限定了类型】

JVM实现跨平台

至此,我们通过javac.exe编译器来编译我们的.java文件生成出.class文件
这些.class文件很明显是不能直接运行的,它不像C语音(编译cpp后生成exe文件直接运行)
这些.class文件是交由JVM来解析运行的

  • JVM是运行在操作系统之上的,每个操作系统的指令是不同,而JDK是区分操作系统的,只要你的本地系统装了JDK,这个JDK就是能够和当前系统兼容的。(jdk是区分平台的,class文件会被翻译成不同的平台的机器编码)
  • class字节码运行在JVM之上,所以不用关系class字节码是在哪个操作系统编译的,只要符合JVM规范,那么,这个字节码文件就是可以运行的
  • 所以Java就做到了跨平台性,一次编译,到处运行

class文件和JVM的恩怨情仇

类加载的时机

虚拟机规范严格规定有且只有5种情况必须立即对类进行“初始化”(class文件加载到JVM中)

  • 创建类的实例(new的方式)。访问某个类或接口的静态变量,或者对该静态变量赋值,调用类的静态方法
  • 反射的方式
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类,直接使用Java.exe命令来运行某个主类(包含main()方法的那个类)
  • 当使用jdk1.7的动态语言支持时

所以说:

  • Java类的加载是动态的,它并不是一次性将所有类全部加载后在运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载,这是为了节省内存开销
如何将类加载到jvm

class文件是通过类加载器装载到jvm中的
Java中默认的类加载器有三种

image

各个加载器的工作责任

  • Bootstrap ClassLoader:负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
  • Extension ClassLoader:负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目录下的jar包
  • App ClassLoader:负责记载classpath中指定的jar包及目录中class

工作过程:

  • 当AppClassLoader加载一个class时,它实现不会自己去尝试加载这个类,而是把类加载请求委派发给父加载器ExtClassLoader去完成
  • 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是将类加载请求委派给BootstrapClassLoader去完成
  • 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载
  • 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
  • 如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
    这就是所谓的双亲委派模型,简单的来说:如果一个类加载器收到类加载的请求,它实现不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成,依次向上。
    好处:
  • 防止内存中出现多份同样的字节码(安全角度)

特别说明:

  • 类加载在成功加载某个类之后,会把得到的java.lang.class类的实例缓存起来,下次再请求加载该类的时候,类加载器会直接使用缓存的类实例,而不会尝试再次加载
类加载详细过程

加载器加载到jvm中,接下来其实又分为好几个步骤:

  • 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.class的类对象
  • 连接,连接又包含三块内容:验证,准备,初始化。
    • 验证:文件格式,元数据,字节码,符号引用验证
    • 准备:为类的静态变量分配内存,并将其初始化为默认值
    • 解析:把类中的引用符号转换直接引用
  • 初始化:为类的静态变量赋予正确的初始值

image

JIT即时编辑器

一般我们可能会想:JVM在加载了这些class文件以后,针对字节码,逐条取出,逐条执行–>解析器解析

实际是:

  • 就是把这些Java字节码重新编译优化,生成机器码,让cpu直接执行,这样编译出来的代码效率会更高。
  • 编译也是要花费时间的,我们一般对热点代码做编译,非热点代码直接解析就好了

热点代码的解释:1、多次调用的方法。2、多次执行的循环体

使用热点探测来检测是否为热点代码,热点探测有两种方式

  • 采样
  • 计数器

目前HotSpot使用的是计数器的方式,它为每一个方法准备了两类的计数器:

  • 方法的调用计数器(Invocation Counter)
  • 回边计数器(Back EdgeCounter)
  • 在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译

image

参考文件

类加载完以后JVM干了什么?

在类加载检查通过后,接下来虚拟机为新生的对象分配内存

VM规范版

根据VM规范,VM应该被划分为五块区域—VM栈,堆,方法区,程序计数器,本地方法栈五个部分

image

方法区(Method Area):该区域是所有线程共享的,主要用于存放类的信息,常量、静态、即时编译器编译后的代码等。垃圾回收器对这块区域的回收主要是针对常量池和类的卸载。

Java堆(Java Heap):该区域也是所有线程共享的,用于存放对象的实例,绝大多数创建的对象都会被存放在这里(除了部分由于逃逸分析而在外分配的对象,该部分只是在方法体中被引用,故而分配到栈上)。垃圾回收器最主要针对的对象,对这部分的回收效率影响了VM的整体性能。

本地栈方法(Native Methoid Stack):该区域是每个线程独有的,主要用于VM的Native方法。这部分是由VM自行管理。程序员基本上不需要关系该部分。

VM栈(VM Stack):该区域也是每个线程所独有的,与本地方法栈类似,唯一的区别是它为VM执行Java方法服务,该区域主要维护栈针(没调用一个方法,则VM就会创建一个栈针保护当前方法的状态,并将其出栈继续执行未完成的方法),有一定的深度,可能抛出StackOverflowError和OutOfMemoryError

程序计数器:该区域也是每个线程独有的,该区域主要是存放当前执行指令的地址

接下来的JDK8的JVM的内存模型

image

与VM规范的划分并没有特别大的改动,只是结合Java的具体实现改变部分内容的存放位置
下面是Java堆内存的区域划分

image

Eden:该区域是主要的刚创建的对象的内存分配区域,绝大多数都会被创建到这里(除了部分大对象通过内存担保机制创建到Old区域,默认大对象都是能够存活较长时间的),该区域的对象大部分都是短时间都会死亡的,故垃圾回收器针对该部分主要采用标记整理算法回收该区域。

Surviver:该区域也是属于新时代的区域,该区域是将在Eden中未被清除的对象存放在该区域中,该区域分为两块区域,采用复制算法,每次只用一块,Eden与Surviver区域的比例是8:1,是根据大量的业务运行总结出来的规律

Old:该区域是属于老年代,一般能够在Surviver中没有被清除出去的对象才会进入到这块区域,该区域主要采用标记清除算法

总结:Java堆的垃圾回收是垃圾回收器最主要的光顾对象,整体采用分代收集的策略,对不同区域结合其特点采用不同的垃圾回收算法。我们在编程中也应该关注这块区域,尽量不使用大对象,尽可能创建局部对象,使用过后确定废弃不用的对象及时断开引用,尽量避免使用循环对象引用(可达性分析也是比较消耗资源的)

JVM内存区域图解
https://www.processon.com/view/link/5b891cc9e4b0fe81b620b2c0

  • 堆:存放对象的实例,几乎所有的对象实例都在这里分配内存
  • 虚拟机栈:虚拟机栈描述的是Java方法执行的内存结构每个方法被执行时候都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作栈,动态链接方法出口等信息
  • 本地方法栈:本地方法栈是为虚拟机使用到Native方法服务的
  • 方法区:存储已被虚拟机加载的类元数据信息(元空间)
  • 程序计数器:当前线程执行的字节码的行号指示器

例子:

public class test{
    public static void main(String[] args){
        User user = new User();
        user.setName("ndb");
        System.out.println(user)
    }
}

执行流程:
image

宏观简述:

  • 通过
    java.exe
    运行
    test.class
    ,随后被加载到JVM中,元空间存储着类的信息(包括类的名称,方法信息,字段信息等)
  • 然后JVM找到test的主函数入口(main),为main函数创建栈帧,开始执行main函数
  • main函数的第一条命令是
    User user = new User();
    就是让JVM创建一个User对象,但是这时候方法区中没有User类的信息,所以JVM马上加载User类,把User类的类型信息放到方法区中(元空间)
  • 加载完User类之后Java虚拟机做的第一件事情就是在堆区中为一个新的User实例分配内存,然后调用构造函数初始化User实例,这个User实例持有着指向方法区的User类的类型信息(其中包含有方法表,Java动态绑定的底层实现)的引用
  • 当使用
    user.setName(“ndb”)
    的时候。jvm根据user引用找到User对象,然后根据User对象持有的引用定位到方法区中User类的类型信息的方法表,获得setName()函数的字节码地址
  • 为setName()函数创建栈帧,开始运行setName()函数

从微观上其实还做了很多东西,正如上面所说的类加载过程(加载–>连接(验证,准备,解析)–>初始化),在类加载完之后jvm为其分配内存(分配内存中也做了非常多的事)。由于这些步骤并不是一步一步往下走,会有很多的“混沌bootstrap”的过程,所以很难描述清楚。

常量池

  • 运行时常量池位于堆中
  • 字符串常量池位于堆中
    常量池存储的是:
  • 字面量(Literal):文本字符串等 -------->用双引号引起来的字符串字面量都会进这里面
  • 符号引用(Symbolic References)
  • 类和接口的全限定名(Full Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

现在我们的运行时常量池只是换了一个位置(原本在方法区,现在在堆中),但可以明确的是:类加载后,常量池中的数据会在运行时常量池中存放!

别人总结的常量池:

它是Class文件中的内容,还不是运行时的内容,不要理解它是个池子,其实就是Class文件中的字节码指令

字符串常量池:

HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容

字符串常量池只存储引用,不存储内容!

intern方法

/**
* Returns a canonical representation for the string object.
*


* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
*


* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
*


* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
*


* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* The Java™ Language Specification.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/

  • 如果常量池中存在当前字符串,那么直接返回常量池中它的引用。
  • 如果常量池中没有此字符串, 会将此字符串引用保存到常量池中后, 再直接返回该字符串的引用!

垃圾回收(GC)

垃圾回收简单介绍

jvm回收的是垃圾,垃圾就是我们程序中已经是不需要的了。垃圾收集器在堆进行回收前,第一件事情就是要确定这些对象之间那些还存活者,那些已经死去。判断那些对象死去常用有两种方式:

  • 引用计数法:这种难以解决对象之间的循环引用的问题
  • 可达性分析算法:主流jvm采用这种方式

image

现在已经判断哪些对象已经死去,我们现在要对这些死去的对象进行回收,以下为几种算法:

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代收集算法
    无论是可达性分析算法还是垃圾回收算法,jvm使用的都是准确式GC。jvm是使用一组称为OopMap的数据,来存储所有的对象引用(这样就不用遍历整个内存去查找,空间换时间),并且不会将所有的指令都生成OopMap,在安全区域上开始GC
  • 在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举(可达性分析)

上面所讲的垃圾回收算法只能算是方法论,落地实现的就是垃圾回收器:

  • Serial收集器
  • ParNew收集器
  • Parallel Scavenge收集器
  • Serial Old收集器
  • Parallel Old收集器
  • CMS收集器
  • G1收集器

大部分收集器都是组合使用的

image

3y关于jvm的内存结构补充
  • 基本与上面讲的差不多
  • jdk1.7以前的PermGen(永久代),替换成Metaspace(元空间)
  • 原本永久代存储的数据:符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap
  • Metaspace(元空间)存储的是类的元数据信息(metadata)
  • 元空间的本质和永久代类似,都是对jvm规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
  • 替换的好处:
    • 字符串存在永久代中,容易出现性能问题和内存溢出。
    • 永久代会为GC带来不必要的复杂度,并且回收效率偏低

image

上面情况下会出现内存泄漏,内存溢出

原因:

  • 对象可达(一直被引用)
  • 但是对象不会被使用
    例子:

内存溢出的原因:

  • 内存泄漏导致堆栈内存不断增大,从而引发内存溢出
  • 大量的jar,class文件加载,装载类的空间不够,溢出
  • 操作大量的对象导致堆内存空间已经用满了,溢出
  • nio直接操作内存,内存过大导致溢出

解决:

  • 查看程序是否存在内存泄漏的问题
  • 设置参数加大空间
  • 代码中是否存在死循环或者循环产生过多重复的对象实体
  • 查看是否使用了nio直接操作内存
线程栈(虚拟机栈)

jvm规范让每一个Java线程拥有自己的独立的jvm栈,也就是Java方法的调用栈

当方法调用的时候,会生成一个栈帧。栈帧是保存在虚拟机栈中的,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息

线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。

jvm年轻代到老年代的晋升过程的判断条件
  • 部分对象会在From和To区域中复制来复制去,如此交换15次(由jvm参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
  • 如果对象的大小大于Eden的二分之一会直接分配在old,如果old也分配不下,会做一次majoGC,如果小于eden的一半但是没有足够的空间,就进行minorgc也就是新时代GC
  • minor gc后,survivor仍让放不下,则放到老年代
  • 动态年龄判断,大于等于某个年龄的对象超过了survivor空间的一半,大于等于某个年龄的对象直接进入老年代
jvm出现fullgc很频繁,怎么线上排查问题

根据fullgc的触发条件来做:

  • 如果有perm gen的话(jdk1.8后就没了),要给perm gen分配空间,但没有足够的空间时候会触发full gc --所以看看是不是perm gen区的值设置得太小了。
  • System.gc()方法得调用 --一般没人会调
  • 当统计得到得Minor GC晋升到旧生代得平均大小大于老年代得剩余空间,则会触发full gc --是不是频繁创建了大对象(也有可能eden区设置过小)(大对象直接分配在老年代中,导致老年代空间不足—>从而频繁GC) --是不是老年代得空间设置过小(Minor GC几个对象就大于老年代得剩余空间)

image

类加载为什么要使用双亲委派模式,有没有什么场景是打破了这个模式?

双亲委托模型得重要用途就是为了解决类载入过程中得安全性问题

  • 假设有一个开发者自己编写了一个名为 java.lang.Object的类,想借此欺骗JVM。现在他要使用自定义 ClassLoader来加载自己编写的 java.lang.Object类。
  • 然而幸运的是,双亲委托模型不会让他成功。因为JVM会优先在 BootstrapClassLoader的路径下找到 java.lang.Object类,并载入它

Java的类加载是否一定遵循双亲委托模型?

  • 在实际开发中,我们可以通过自定义ClassLoader,并重写父类的loadClass方法,来打破这一机制。
  • SPI就是打破了双亲委托机制的(SPI:服务提供发现)。
    • https://zhuanlan.zhihu.com/p/28909673
    • https://www.cnblogs.com/huzi007/p/6679215.html - https://blog.csdn.net/sigangjun/article/details/79071850
类得实例化顺序
  1. 父类静态成员和静态初始化块,按在代码中出现得顺序依次执行
  2. 子类静态成员和静态初始化块,按在代码中出现得顺序依次执行
  3. 父类实例成员和实例初始化块,按在代码中出现得顺序依次执行
  4. 父类构造方法
  5. 子类实例成员和实例初始化块,按在代码中出现得顺序依次执行
  6. 子类构造方法
JVM垃圾回收机制,何时触发MinorGC等操作

当young gen中得eden区分配满得时候触发MinorGC(新生代得空间不够放得时候)

jvm中一次完整得gc流程(从ygc到fgc)是怎么样的

ygc:对新生代堆进行gc,频率较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较少。
fcg:全堆范围的gc。默认堆空间使用了达到80%(可调整)的时候会触发fcg。

什么时候执行ygc和fcg

  • eden空间不足,执行ycg
  • old空间不足,perm空间不足,调用System.gc(),ygc时的悲观策略,dump live的内存信息时(jmap –dump:live),都会执行full gc
回收算法
  • 标记-清除算法
  • 复制算法
  • 标记-压缩算法
  • 常用的垃圾回收器一般采用分代收集算法(其实就是组合上面的算法,不同的区域使用不同的算法)

具体:

  • 标记-清除算法(Mark-Sweep),分为标记和清除两个阶段:首先标记所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象
  • 复制算法(Copying),将可用内存按容量划分为大小相等的两块,当着一块用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内容空间一次清除掉。
  • 标记-压缩算法,标记过程和标记清除算法一样,之后将所有存活的对象向一端移动,然后直接清除掉端边界以外的内存
  • 分代收集算法:(Generational Collection),将Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法
各种回收器,优缺点

图中两个收集器之间有连线,说明它们可以配合使用.

image

  • Serial收集器,串行收集器是最古老,最稳定以及效率最高的收集器,但可能会产生较长的停顿,只使用一个线程去回收
  • ParNew收集器,Serial收集器的多线程版本
  • Parallel收集器,类似于ParNew收集器,但更关注系统的吞吐量
  • Parallel Old收集器,是Parallel Scavenge收集器的老年代版本,使用多线程标记-整理算法
  • CMS收集器(Concurrent Mask Sweep),是一种以获取最短回收停顿时间为目标的收集器,他需要消耗额外的cpu和内存资源,在cpu和内存资源紧张,cpu较少时,会加重系统负担。CMS无法处理浮动垃圾。CMS的标记-清除算法,会导致大量的空间碎片的产生
  • G1收集器(Garbage-First)是一款面向服务器的垃圾回收器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐性能特征。
stackoverflow错误,permgen space错误

stackoverflow错误:在虚拟机栈中线程请求的栈深度大于虚拟机栈锁允许的最大深度
permgen space错误(针对jdk之前1.7版本):大量加载class文件,常量池内存溢出

编辑

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值