深入理解jvm

2 篇文章 0 订阅

1. JVM体系结构

类装载器ClassLoader:用来装载.class文件
执行引擎:执行字节码,或者执行本地方法
运行时数据区:方法区、堆、Java栈、程序计数器、本地方法栈

1.1 程序计数器

内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成;如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

1.2 java虚拟机栈

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)。

1.3 本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

1.4 java堆

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

1.5 方法区

属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

1.6 运行时常量池

属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译期和运行期都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

2 垃圾回收器与内存分配

程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,垃圾回收期所关注的就是这部分内存。

3. 类的加载

3.1 加载

1. 通过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法去这个类的各种数据的访问入口。

3.2 验证

3.2.1 文件格式验证

是否以魔数 0xCAFEBABE 开头
主、次版本号是否在当前虚拟机处理范围之内
常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
Class 文件中各个部分集文件本身是否有被删除的附加的其他信息

只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。

3.2.2 元数据验证

这个类是否有父类(除 java.lang.Object 之外)
这个类的父类是否继承了不允许被继承的类(final 修饰的类)
如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)

3.2.3 字节码验证

3.3 准备

这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。

3.4 解析

3.5 初始化

前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。

 

4. JVM生命周期

一个运行时的Java虚拟机(JVM)负责运行一个Java程序。
当启动一个Java程序时,一个虚拟机实例诞生;当程序关闭退出,这个虚拟机实例也就随之消亡。
如果在同一台计算机上同时运行多个Java程序,将得到多个Java虚拟机实例,每个Java程序都运行于它自己的Java虚拟机实例中。

JVM的生命周期:
1. JVM实例的诞生
当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
2. JVM实例的运行
3. JVM实例的消亡
当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出。

启动main线程
在JVM实现中,线程为Execution Engine的一个实例,main函数是JVM指令执行的起点,JVM会创建main线程来执行main函数,以触发JVM一系列指令的执行,真正地把JVM run起来。在创建main线程时,会为其分配私有的PC Register、JVM Stack、Native Method Stack
mian方法执行完之后
释放main线程所占用资源及内存空间,如PC register、JVM Stack等,释放JVM所占用的内存空间,如Heap、Method area,JVM退出。另外,web项目是由容器启动的。也就是服务器,tomcat之类。

5. jvm垃圾回收
分代收集(Generational Collection)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和年轻代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

老年代的特点是每次垃圾收集时只有少量对象需要被回收,而年轻代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前主流的虚拟机实现都采用了分代收集的思想,把整个堆区划分为新生代和老年代;新生代又被划分成Eden 空间、 From Survivor 和 To Survivor 三块区域。 我们把Eden : From Survivor : To Survivor 空间大小设成 8 : 1 : 1 ,对象总是在 Eden 区出生, From Survivor 保存当前的幸存对象, To Survivor 为空。一次 gc 发生后: 1)Eden 区活着的对象 + From Survivor 存储的对象被复制到 To Survivor ;  2) 清空 Eden 和 From Survivor ; 3) 颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To , To 变 From 。可以看出,只有在 Eden 空间快满的时候才会触发 Minor GC 。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以降低。

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC或Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

6. 降低GC的调优的策略

最关键参数:-Xms、 -Xmx 、-Xmn 、-XX:SurvivorRatio、-XX:MaxTenuringThreshold、-XX:PermSize、-XX:MaxPermSize
-Xms、 -Xmx 通常设置为相同的值,避免运行时要不断扩展JVM内存,这个值决定了JVM heap所能使用的最大内存。
-Xmn 决定了新生代空间的大小,新生代Eden、S0、S1三个区域的比率可以通过-XX:SurvivorRatio来控制(假如值为 4  表示:Eden:S0:S1 = 4:3:3 )
-XX:MaxTenuringThreshold 控制对象在经过多少次minor GC之后进入老年代,此参数只有在Serial 串行GC时有效。
-XX:PermSize、-XX:MaxPermSize 用来控制方法区的大小,通常设置为相同的值。

1. 避免新生代大小设置过小
当新生代设置过小时,会产生两种比较明显的现象,一是minor GC次数频繁,二是可能导致 minor GC对象直接进入老年代。当老年代内存不足时,会触发Full GC。
2. 避免新生代大小设置过大
新生代设置过大,会带来两个问题:一是老年代变小,可能导致Full  GC频繁执行;二是 minor GC 执行回收的时间大幅度增加。
3. 合理设置对象在新生代存活的周期
新生代存活周期的值决定了新生代对象在经过多少次Minor GC后进入老年代。因此这个值要根据自己的应用来调优,Jvm参数上这个值对应的为-XX:MaxTenuringThreshold,默认值为15次。

总结:jvm调优常用设置:
-Xms:初始堆大小 
-Xmx:最大堆大小 
-XX:NewSize=n:设置年轻代大小 
-XX:NewRatio=n:设置年轻代和年老代的比值.如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值.注意Survivor区有两个.如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 
-XX:MaxPermSize=n:设置持久代大小

典型设置:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M.
-Xms3550m:设置JVM促使内存为3550m.此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存.
-Xmn2g:设置年轻代大小为2G.整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-Xss128k:设置每个线程的堆栈大小,JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值