jvm虚拟机_春风得意马蹄疾,一文看尽(JVM)虚拟机

前方高能

高能

6915169387a08f16e51b8a9af6870946.png

想了解更多可以关注小编公众号:芝麻代理,里面整理了全套python学习资料,免费分享

1.JVM相关

1.1.概述

JVM是Java Virtual Machine的缩写,中文意思是Java虚拟机。能来看这篇文章的人应该大多都玩过Windows或Linux虚拟机,JVM跟他们的功能也类似:提供一个虚拟的计算机环境,来运行一些程序。

我们都知道,Linux与Windows软件是不通用的,但是如果我们在一台Windows设备上安装了Linux虚拟机,那我们就也可以开心的在Windows上使用Linux软件了,思路延伸一下:所有能够安装JVM的机器都可以运行Java程序。Java语言使用JVM屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。这就是“一次编译,多次运行”。

1.2.本质

JVM说到底,其实就是在物理机上运行的程序,同时他依托物理机而拥有了完善的硬件架构和指令系统。

2.JVM运行时数据区

2.1.虚拟机栈

虚拟机栈是每个Java方法的内存模型,虚拟机栈中元素叫做“栈帧”,每一个方法被执行的时候都会压入一个栈帧,执行完毕则出栈,这个栈帧里面存放着这个方法的局部变量表(包括参数)、操作栈、动态链接、方法返回地址。

我们需要知道的是,局部变量表是在编译的时候就确定大小了,我们在调用一个方法时,局部变量表的大小是已知且确定的,在方法执行的时候不会改变局部变量表的大小。

我们常说的Java内存中的栈内存一般就是指的局部变量表部分。如果变量是基本类型,会直接保存在这个区域,如果是引用类型,那会保存对象的引用地址。

2.2.本地方法栈

Navtive方法是Java通过JNI直接调用本地C/C++库。

本地方法栈(Native Method Stacks)我们可以理解为本地方法的虚拟机栈。他的功能与虚拟机栈类似,只是本地方法栈是为了本地方法服务而不是Java方法(字节码)。JVM通过C/C++暴露给Java的接口来调用本地方法,当线程调用本地方法时,JVM虚拟机栈不会变,更不会进行压栈操作,JVM只是简单地动态连接并直接调用指定的native方法。

*本地方法栈是一个后入先出栈(LIFO)*本地方法栈是线程的私有栈,随线程启动而生,线程结束而死

2.3.程序计数器

程序计数器是每个线程所私有的一小块区域,其中记录了当前线程执行到的字节码的行数(当前程序执行的位置),字节码解释器工作时需要根据当前程序执行的位置来判断下一行要执行的程序。

上面我们说过,JVM中会执行两种方法:java方法和本地方法,在执行不同方法时,程序计数器的表现是不一样的。执行Java方法,计数器工作,其中储存着正在执行的字节码指令的地址;如果当前执行的是本地方法,那程序计数器将会储存空值。这里强调一下:程序计数器区域是虚拟机规范中唯一不存在OutOfMemoryError的内存区域。

作用

*字节码解释器选择下一条指令:字节码解释器通过程序计数器的值来判定下一条指令的地址,从而来实现代码的流程控制。*多线程中的指令位置恢复:线程私有的计数器记录当前线程的位置,在C这个线程重新执行的时候,可以恢复到线

2.3.Java堆

Java堆是JVM中占内存最大的一块区域,他在虚拟机启动时创建,是所有线程所共享的一块区域。

堆的任务就是存放所有的对象实例,几乎所有的对象实例都储存在这里。他可以分为年轻代、永生代,同时Java堆也是GC工作的主要区域,有时候我们也叫他GC堆。

年轻代

年轻代又分为三部分eden、From Survivor、From Survivor,默认情况下这三部分大小按照8:1:1(比例可配置,将在JVM调优中讲到)分布,整个年轻代又占Java堆的1/3。

年轻代的三个模块按照8:1:1是有一定道理的,这与等下要讲到垃圾回收有关系。

大多数对象都在年轻代中创建,而且在年轻代中的对象大多数生命周期很短。

在新生代中,并不是每次存活的对象都少于10%,有时候若是存活的对象大于10%,就会想老年代进行空间分配担保。

老年代

在新生代中经过N(N可配置,默认15)次垃圾回收仍然存活的对象就会被放进老年代,老年代中的对象存活率都比较高。

2.4.元数据区(永久代)

它用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,是各个线程共享的内存区域。

在JDK8之前的HotSpot JVM,存放这些”永久的”的区域叫做“永久代(permanent generation)”。永久代是一片连续的堆空间,在JVM启动之前通过在命令行设置参数-XX:MaxPermSize来设定永久代最大可分配的内存空间,默认大小是64M(64位JVM默认是85M)。。

3.JVM垃圾回收算法(GC)

3.1.标记-清除算法

标记-清除算法是最基本的回收算法,该算法分两个步骤:标记、清除。 首先将那些不可到达的对象进行标记,然后将有标记的对象进行清除,复制-清除算法不是一个比较好的算法,他有两个比较严重的问题

*标记清除算法过后的内存空间有很多碎片,如果下次有一个较大的对象需要实例化时,虽然新生代中的内存足够储存它,但因为没有足够的连续空间,JVM会再次调用GC。

*标记和清除的过程效率都比较低,

1

2

3.2.复制算法

模型

复制算法将内存空间分为两个区,所有的对象实例都储存在其中一个区(活动区间),而另一个区叫做(空闲区间)。

过程

当活动区间的内存空间不足时,JVM将会暂停程序的运行,开启复制算法的GC线程。GC线程将活动区间内所有存活的实例复制到空闲区间(依次排列),完成复制后活动区间的指针指向原空闲区间,空间区间的指针指向原活动区间,此时完成了两个区间的对换,同时一次性清除原活动区间中的对象,待下次GC开启时重复上面的操作。

使用场景

复制算法优化了标记-清除算法中的两个问题,特别适合那些对象存活率很低的内存空间,比如新生代,新生代中绝大多数对象的都是朝生夕死的。

上文我们说过,新生代的三个空间不是平均分配的,原因就在这里,对象都在Eden中创建,Eden内存空间不足时就开始使用一个S区来进行复制算法GC(新生代的GC又叫做Minor GC),GC结束后Eden区间就被清空了,大部分对象都无法到达S区,所以S区相对于Eden区就不需要那么大的空间。

当GC过程中使用的那个S区满了之后,这个区里面还不能晋升的对象就会复制到另一个S区中。

3.3. 标记-整理算法

标记-整理算法也分两步:标记、整理

所谓标记,就是标记不可达对象。整理指的是,将存活的对象往一端移动,然后清理掉存货对象边界以外的对象,从而达到GC的目的。 其实移动的效率也不高,但是如果需要移动的对象比较少,那标记-整理算法也是个好办法。

使用场景

老年代中的对象大多数都是存活率比较高的对象,如果老年代使用复制算法,那每次GC都将需要在复制的过程中耗费大量时间,老年代用的就是标记-整理算法。

3.4. GC算法总结

标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

不难看出,标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。

不过任何算法都会有其缺点,标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。

3.5. GC新生代晋升老年代的情形(内存分配)

3.5.1.新生代满

我们知道新生代在进行GC时,会向两个S区转移对象,但是当两个S区都已经没有空间,那就无法将新生代的对象再转移到S区,这时候就会分配担保机制将新生代中的对象直接晋升到老年代。

3.5.2. 对象过大

新生代产生了一个对象,这个对象大小大于规定的最大对象大小(在Serial及ParNew中可通过-XX:PretenureSizeThreshold配置),新生代在就不会保存这个对象,而是绕过新生代,直接在老年代为该对象分配空间。

3.5.3. 满足晋升条件

虚拟机中的每一个对象都有对象年龄(Age)计数器,当对象的年龄达到晋升条件(年龄到达-XX:MaxTenuringThreshold配置值)时,就会在GC时晋升代老年代。

3.5.4. 动态对象年龄判断

当在S区中处于某一个年龄的对象占用了很大内存空间(大于S区大小的1/2)时,大于或等于该年龄的对象将直接进入老年代,而不是等年龄到达晋升年龄才进入老年代

4. JVM异常

JVM异常主要是StackOverFlowError和OutofMemoryError(OOM)。

4.1. StackOverFlowError(栈溢出)

StackOverFlowError的出现表明:程序因为某些原因已经把JVM分配给线程的栈耗尽

4.1.1. 原因

最常见的导致栈溢出的原因是程序里的递归,递归执行的过程中会反复的调用自己,在制定跳出递归条件时一定要慎重,避免出现死递归。在虚拟机栈中给递归方法分配的栈帧深度是有限的,递归方法会不断的在这个栈帧的局部变量表中写入数据,最终到达栈低,然后抛出栈溢出Error.

4.1.2. 死递归示例

代码

public class StackOverTest {    static long l = 0L;    public static void printT(){        System.out.println(l);        l++;        //死递归        printT();    }    public static void main(String[] args) {        printT();    }}

————————————————

结果

.......884788488849*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844Exception in thread "main" java.lang.StackOverflowError    at java.io.PrintStream.write(PrintStream.java:480)    at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)    at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)    at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)    at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)    at java.io.PrintStream.write(PrintStream.java:527)    at java.io.PrintStream.print(PrintStream.java:611)    at java.io.PrintStream.println(PrintStream.java:750)    at Prototype.StackOverTest.printT(StackOverTest.java:7)    at Prototype.StackOverTest.printT(StackOverTest.java:10)    at Prototype.StackOverTest.printT(StackOverTest.java:10)    ......

4.1.3. 解决办法

*合理设置跳出递归的条件,避免死递归*如果你确定你递归的条件没有问题,可能只是你的程序需要递归的次数比较多,默认的线程栈大小可能是 512KB 或者 1MB。那么你可以使用 -Xss 标识来增加线程栈的大小,参数的格式:-Xss[g|G|m|M|k|K]

4.1.4.关于死循环导致的栈溢出

网上资料有很多观点是死循环也会引起栈溢出,这种说法是不准确的,正确的说法是,死循环中不断向内存中放置新对象的情形才会导致栈溢出。

本质上来说,所有的操作系统都是一个死循环,那么我们的程序中出现死循环时,我们就要谨慎考虑死循环中的资源的申请与释放,避免因死循环而导致的栈溢出。

4.2. OutOfMemoryError(OOM)

OOM直白点的意思就是内存用完了。

4.2.1 原因

当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error。发生OOM的原因根本在于两点

*JVM内存分配少:JVM内存达不到我们程序所需要的内存,那程序在运行的时候就会OOM*程序内存得不到释放:应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。

内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。

内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。

得益于Java的GC机制,我么不用手动其释放资源,理论上是不会发生内存泄露的。但是有些程序逻辑上有漏洞,GC机制无法清理某些对象,当这些存活的对象大小超过JVM的内存时,就会会出现内存溢出。

最近工作中恰巧出现了内存溢出的情景,在这里跟大家分享一下,因为有保密协议,所以这里不再贴源码,只是描述一下

在我的代码中,我引入了一个缓存机制,把所有的用户类实例都放到这个系统缓存区,便于下一次用户进来的时候直接访问,设计程序时没有考虑太多,所以没有设置缓存失效时间,程序刚上线的时候设置缓存区的效果很好,减少了用户访问登录的时间,但是最近我们的用户量激增,每次项目启动一周后都会邮件警示,我们修改代码重新上传后系统又能保持一段时间,但一周左右又开始警示,于是我专门抽出来一天加班来检查错误,最终是检查到了BUG,就是由于这个系统缓存区存放了太多的User对象,昨天已经将更新过的代码上传,现在看是否还会发生同样的问题。

4.2.2. 常见OOM及解决办法

Java堆内存溢出

这个算是比较常见的OOM了,在Java一般是由于堆内存分配不足引起的。可以通过虚拟机参数-Xms,-Xmx等修改对内存大小来避免这个问题。

永久代(元数据)溢出

一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区,同时常量过多也会引起永久代溢出。与堆内存类似,我们也可以通过参数来修改永久代的内存大小,比如-XX:PermSize=64m -XX:MaxPermSize=256m。

4.3. JVM异常总结

“知己知彼,百战不殆”我们了解了JVM异常发生的原因,那么解决他也不是那么困难了。

5.Java类文件结构

首先希望你把类与对象的关系搞清楚,类文件是一组以8位字节为基础单位的二进制流,他是由.Java文件编译而来的,而对象是根据类来创建的,你可以把类理解为对象的模板。

5.1.Class平台无关性

Class文件运行于JVM上,只要平台安装有JVM虚拟机即可,与平台的类型无关。

5.2. Class类结构

整个Class文件本质上就是一张表,它由如下所示的数据项构成。

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据。根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1、2、4、8个字节的无符号数。表是由多个无符号数或其他表作为数据项构成的符合数据类型,所有的表都习惯性地以“_info”结尾。

从表中可以看出,无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的该数据项的形式,称这一系列连续的某一个类型的数据为某一类型的集合,比如,fields_count个field_info表数据构成了字段表集合。这里需要说明的是:Class文件中的数据项,都是严格按照上表中的顺序和数量被严格限定的,每个字节代表的含义,长度,先后顺序等都不允许改变。

5.2.1. magic

每个Class文件的头四位数据被称为魔数,他的唯一作用是判断这个文件是不是能够被JVM运行的.class文件,.class文件的头四位应该被固定为0xCAFEBABE,就是大佬口中说的咖啡宝贝。我们打开一个.class文件来验证一下

5.2.2. minor_version

这2个字节存储的是Class文件的次版本号

在上面那张图中我们可以看着,这2个字节的值是:00 00,这个次版本号目前还没有明确的含义,可能是设计者留用的。

5.2.3. major_version

这两个字节储存着class文件的主版本号,我们可以看出,我们这个class的主版本号是:00 34,他作为16进制数对应着十进制的52,这是对应着JDK8.详情可以去了解一下JDK版本对照表,以下是我一些对应信息

5.2.4.constant_pool_count

这是下面常量池的计数器,占用一个字节,代表常量池容量计数值,这个容量计数值是从1开始而不是从0开始的。

5.2.5 constant_pool

常量池中主要存放两大类常量:字面量和符号引用,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是在Class文件中第一个出现的表类型数据项目。

字面量

字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。

符号引用

而符号引用总结起来则包括了下面三类常量

    类和接口的全限定名(即带有包名的Class名,如:cn.yzstu.testClass)    字段的名称和描述符(private、static等描述符)    方法的名称和描述符(private、static等描述符)

常量池的内容也比较多,大多数时候我们或许没有必要这么深入,在此也不再详细描述。

5.2.6. access_flag

用来标记类或接口的访问信息。

主要信息:这个Class是类还是接口,是否定义为public类型,abstract类型,如果是类的话,是否声明为final,等等。每种访问信息都由一个十六进制的标志值表示,如果同时具有多种访问信息,则得到的标志值为这几种访问信息的标志值的逻辑或。

5.2.7. this_class

类索引,用于确定这个类的全限定名。

5.2.8. super_class

父类索引,用于确定这个类的父类的全限定名。

Java规定,不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。

5.2.9 interfaces_count

类的接口计数器

5.2.10. interfaces

接口索引集合,里面描述了当前类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口的索引集合中。

5.2.11. fields_count

字段计数器

5.2.12. fields

字段表(field_info)用于描述接口或类中声明的变量。字段包括了类级变量或实例级变量,但不包括在方法内声明的变量。字段的名字、数据类型、修饰符等都是无法固定的,只能引用常量池中的常量来描述。

5.2.13. methods_count

方法表的计数器

5.2.14. methods

方法表(method_info)的结构与属性表的结构相同。

5.2.15 attributes_count

属性表的计数器

5.2.16 attributes

属性表(attribute_info)在前面已经出现过多系,在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

6. JVM调优

有句话叫做“绝大部分项目用不到JVM调优”,我是很赞同这句话的。在实际参与项目的过程时,我很少用到JVM调优,但是耐不住每次面试必问,所以还是深入了解了下JVM调优。

想了解更多可以关注小编公众号:芝麻代理,里面整理了全套python学习资料,免费分享

8f75b906e29a477b126b23757b6927e2.png
885912bd08e0b48d461e34dda61d8206.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值