JVM(Java虚拟机)总结
JVM(java虚拟机)
JVM包含两个⼦系统和两个组件,两个⼦系统为Class loader(类装载器)、Execution engine(执⾏引擎);两个组件为Runtime data area(运⾏时数据区)、Native Interface(本地接⼝)。
⽅法区:存储已被虚拟机加载的类元数据信息(元空间)
Java栈(虚拟机栈):虚拟机栈描述的是Java⽅法执⾏的内存模型:每个⽅法被执⾏的时候都会同时创建⼀个栈帧(Stack Frame)⽤于存储局部变量表、操作栈、动态链接、⽅法出⼝等信息
本地⽅法栈:为虚拟机使⽤到的native⽅法服务
程序计数器:当前线程所执⾏的字节码的⾏号指⽰器
深⼊理解Java虚拟机(第⼆版):
类加载器ClassLoader
类加载器分为四种:前三种为虚拟机⾃带的加载器
启动类加载器(Bootstrap):负责加载$JAVA_HOME中jre/lib/rt.jar⾥所有的class,由C++实现,不是ClassLoader⼦类
扩展类加载器(Extension):由Java语⾔实现,负责加载java平台中扩展功能的⼀些jar包,包$JAVA_HOME中jre/lib/*.jar或-dirs指定⽬录下的jar包
应⽤程序类加载器(AppClassLoader):由Java实现,也叫系统类加载器,负责加载classpath中指定的jar包及⽬录中class
⽤户⾃定义加载器: Java.lang.ClassLoader的⼦类,⽤户可以定制类的加载⽅式
⼯作过程:
当AppClassLoader加载⼀个class时,它⾸先不会⾃⼰去尝试加载这个类,⽽是把类加载请求委派给⽗类加载器ExtClassLoader去完成
当ExtClassLoader加载⼀个class时,它⾸先也不会⾃⼰去尝试加载这个类,⽽是把类加载请求委派给BootStrapClassLoader去完成;
如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib⾥未查到该class),会使⽤ExtClassLoader来尝试加载;
若ExtClassLoader也加载失败,则会使⽤AppClassLoader来加载
如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
以上流畅就是所谓的双亲委派模型:如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把请求委托给⽗加载器去完成,依次向上请求
优点:从安全性⾓度来看,双亲委派模型可以防⽌内存中出现多份同样的字节码
执⾏引擎ExecutionEngine
Execution Engine执⾏引擎负责解释命令,提交操作系统执⾏
本地⽅法栈Native Method Stack
它的具体做法是Native Method Stack中登记native⽅法,在Execution Engine 执⾏时加载本地⽅法库。
本地接⼝Native Interface
本地接⼝的作⽤是融合不同的编程语⾔为 Java 所⽤,它的初衷是融合 C/C++程序,Java 诞⽣的时候是 C/C++横⾏的时候,要想⽴⾜,必须有调⽤ C/C++程序(蹭热度),于是就在内存中专门开辟了⼀块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native⽅法,在Execution Engine 执⾏时加载native libraies。
⽬前该⽅法使⽤的越来越少了,除⾮是与硬件有关的应⽤,⽐如通过Java程序驱动打印机或者Java系统管理⽣产设备,在企业级应⽤中已经⽐较少见。因为现在的异构领域间的通信很发达,⽐如可以使⽤ Socket通信,也可以使⽤Web Service等。
PC寄存器Program Counter
即程序计数器
每个线程都有⼀个程序计数器,是线程私有的,就是⼀个指针,指向⽅法区中的⽅法字节码(⽤来存储指向下⼀条指令的地址,就是将要执⾏的指令代码),由执⾏引擎读取下⼀条指令,是⼀个⾮常⼩的内存空间,⼏乎可以忽略不记。
⽅法区Method Area
⽅法区被所有线程共享,所有字段和⽅法字节码,以及⼀些特殊⽅法如构造函数,接⼝代码也在此定义。
简⽽⾔之,所有定义的⽅法的信息都保存在该区域,此区属于共享区间。
静态变量+常量+类信息(构造⽅法/接⼝定义)+运⾏时常量池存在⽅法区中
但是实例变量存在堆内存中,和⽅法区⽆关
栈Stack
栈也叫栈内存,主管Java程序的运⾏,是在线程创建时创建,它的⽣命期是跟随线程的⽣命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程⼀结束该栈就Over,⽣命周期和线程⼀致,是线程私有的。8种基本类型的变量+对象的引⽤变量+实例⽅法都是在函数的栈内存中分配。
栈存储什么?
王珞丹个人资料简介栈帧中主要保存3 类数据:
本地变量(Local Variables):输⼊参数和输出参数以及⽅法内的变量。
栈操作(Operand Stack):记录出栈、⼊栈的操作。
栈帧数据(Frame Data):包括类⽂件、⽅法等等。
栈运⾏原理:
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是⼀个内存区块,是⼀个数据集,是⼀个有关⽅法(Method)和运⾏期数据的数据集,当⼀个⽅法A被调⽤时就产⽣了⼀个栈帧 F1,并被压⼊到栈中,
A⽅法⼜调⽤了 B⽅法,于是产⽣栈帧 F2 也被压⼊栈,
B⽅法⼜调⽤了 C⽅法,于是产⽣栈帧 F3 也被压⼊栈,
……
执⾏完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
遵循“先进后出”或者“后进先出”原则
每执⾏⼀个⽅法都会产⽣⼀个栈帧,保存到栈(后进先出)的**顶部,顶部栈就是当前的⽅法,该⽅法执
⾏完毕
后会⾃动将此栈帧出栈
常见问题栈溢出:Exception in thread "main" java.lang.StackOverflowError
通常出现在递归调⽤时或者⽅法调⽤过深时。
堆Heap
堆栈⽅法区的关系:
HotSpot是使⽤指针的⽅式来访问对象:
Java堆中会存放访问类元数据的地址,
reference存储的就是对象的地址
Java栈保存对象的引⽤地址
堆保存引⽤地址对应的对象信息
⽅法区保存对象对应的class类型
tfboys 解散Java7之前:
⼀个JVM实例只存在⼀个堆内存,堆内存⼤⼩是可以调节的。类加载器读取类⽂件后,把类⽅法常量变量等信息放⼊堆内存中,保存所有引⽤类型的真实信息,以⽅便执⾏器执⾏。
堆内存逻辑分为三部分:
Young Generation Space 年轻代 Young/New
Tenure Generation Space ⽼年代 Old/Tenure
Permanent Space 永久代 Perm
三种JVM:
·Sun公司的HotSpot
·BEA公司的JRockit
·IBM公司的J9 VM
国内还有阿⾥巴巴的taobao JVM
其中JVM堆分为新⽣代和⽼年代
新⽣代
新⽣代是类的诞⽣、成长、消亡的区域,⼀个类在这⾥产⽣,应⽤,最后被垃圾回收器回收,结束⽣命。新⽣代⼜分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间⽤完时,程序⼜需要创建对象,JVM的垃圾回收器将对伊甸园区进⾏垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引⽤的对象进⾏销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进⾏垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再次垃圾回收,满⾜条件后再移动到⽼年代。若⽼年代也满了,那么这个时候将产⽣MajorGC(FullGC),进⾏⽼年代的内存清理。若⽼年代执⾏了Full GC之后发现依然⽆法进⾏对象的保存,就会产⽣OOM异常—“OutOfMemoryError”
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有⼆:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms(初始内存)、-Xmx(最⼤内存)来调整。
(2)代码中创建了⼤量⼤对象,并且长时间不能被垃圾收集器收集(存在被引⽤)。
⽼年代
简美妍默认15次GC后还存在的对象就会放⼊⽼年代,⽼年代对象⽐较稳定,不会频繁的GC
永久代
永久代是⼀个常驻内存区域,⽤于存放JDK⾃⾝所携带的Class,Interface的元数据,也就是说它存储的是运⾏环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收的,关闭 JVM 才会释放此区域所占⽤的内存。
如果出现 java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。⼀般出现这种情况,都是程序启动需要加载⼤量的第三⽅jar包。例如:在⼀个Tomcat下部署了太多的应⽤。或者⼤量动态反射⽣成的类不断被加载,最终导致Perm区被占满。
Jdk1.6及之前: 有永久代,,常量池1.6在⽅法区
Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池1.7在堆
Jdk1.8及之后: ⽆永久代,常量池在元空间(Metaspace)
实际⽽⾔,⽅法区(Method Area)和堆⼀样,是各个线程共享的内存区域,它⽤于存储虚拟机加载的:
类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将⽅法区描述为堆的⼀个逻辑部分,但它却还有⼀个别名叫做Non-Heap(⾮堆),⽬的就是要和堆分开。
对于HotSpot虚拟机,很多开发者习惯将⽅法区称之为“永久代(Parmanent Gen)” ,但严格本质上说两者不同,或者说使⽤永久代来实现⽅法区⽽已,永久代是⽅法区(相当于是⼀个接⼝interface)的⼀个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移⾛。
为什么移除永久代?
内存是启动时固定好的,调优困难,太⼩容易出现永久代溢出,太⼤则容易导致⽼年代溢出
字符串存在永久代中,容易出现性能问题和内存溢出
永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
常量池(Constant Pool)是⽅法区的⼀部分,Class⽂件除了有类的版本、字段、⽅法、接⼝等描述信息外,还有⼀项信息就是常量池,这部分内容将在类加载后进⼊⽅法区的运⾏时常量池中存放。
元空间并不在虚拟机中,⽽是使⽤本地内存,默认情况下,元空间的⼤⼩仅受本地内存限制,可以通过以下参数调优:
-XX:MetaspaceSize,初始空间⼤⼩,达到该值就会触发垃圾收集进⾏类型卸载,同时GC会对该值进⾏调整:如果释放了⼤量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提⾼该值。
-XX:MaxMetaspaceSize,最⼤空间,默认是没有限制的。
除了上⾯两个指定⼤⼩的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最⼩的Metaspace剩余空间容量的百分⽐,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最⼤的Metaspace剩余空间容量的百分⽐,减少为释放空间所导致的垃圾收集
堆参数调优
常⽤JVM参数
参数备注
-Xms初始堆⼤⼩。只要启动,就占⽤的堆⼤⼩,默认是内存的1/64
-Xmx最⼤堆⼤⼩。默认是内存的1/4
-Xmn新⽣区堆⼤⼩
-XX:+PrintGCDetails输出详细的GC处理⽇志
Java代码查看jvm堆的默认值⼤⼩:
设置JVM参数
程序运⾏时,可以给该程序设置jvm参数,不同的⼯具设置⽅式不同。
命令⾏:
java -Xmx50m -Xms10m HeapDemo
eclipse、IDEA:
在 Run Configuration 的 VM Options 中设置。
查看堆内存详情
public class Demo2 {
public static void main(String[] args) {
System.out.print("最⼤堆⼤⼩:");
System.out.Runtime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆⼤⼩:");
System.out.Runtime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("==================================================");
byte[] b = null;
for (int i = 0; i < 10; i++) {
b = new byte[1 * 1024 * 1024];
}
}
}
执⾏前配置参数:-Xmx50m -Xms30m -XX:+PrintGCDetails
执⾏:看到如下信息
新⽣代和⽼年代的堆⼤⼩之和是Runtime().totalMemory()
开发中,eclipse使⽤MAT⼯具、IDEA分析Dump⽂件来定位OOM位置。
GC垃圾回收
JVM垃圾判定算法:
引⽤计数法(Reference-Counting)
可达性分析算法(根搜索算法)
GC垃圾回收算法:
复制算法(Copying)
标记清除(Mark-Sweep)
标记压缩(Mark-Compact),⼜称标记整理
分代回收算法(Generational-Collection)
垃圾判定
引⽤计数法(Reference-Counting)
引⽤计数算法是通过判断对象的引⽤数量来决定对象是否可以被回收
给对象中添加⼀个引⽤计数器,每当有⼀个地⽅引⽤它时,计数器值就加1;当引⽤失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使⽤的
优点:
简单,⾼效,现在的objective-c、python等⽤的就是这种算法。
缺点:
引⽤和去引⽤伴随着加减算法,影响性能
很难处理循环引⽤,相互引⽤的两个对象则⽆法释放
因此⽬前主流的Java虚拟机都摒弃掉了这种算法
可达性分析算法
周杰伦的歌这个算法的基本思想就是通过⼀系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所⾛过的路径称为引⽤链,当⼀个对象到 GC Roots 没有任何引⽤链相连的话,则证明此对象是不可⽤的。
在Java中,可以作为GC Roots的对象包括下⾯⼏种:
虚拟机栈(栈帧中的本地变量表)中的引⽤对象。
⽅法区中的类静态属性引⽤的对象。
⽅法区中的常量引⽤的对象。
本地⽅法栈中JNI(Native⽅法)的引⽤对象
垃圾回收算法
复制算法(Copying)
该算法将内存平均分成两部分,然后每次只使⽤其中的⼀部分,当这部分内存满的时候,将内存中所有存活的对象复制到另⼀个内存中,然后将之前的内存清空,只使⽤这部分内存,循环下去。
优点:
实现简单
不产⽣内存碎⽚
缺点:
将内存缩⼩为原来的⼀半,浪费了⼀半的内存空间,代价太⾼;如果不想浪费⼀半的空间,就需要有额外的空间进⾏分配担保,以应对被使⽤的内存中所有对象都100%存活的极端情况,所以在⽼年代⼀般不能直接选⽤这种算法。
如果对象的存活率很⾼,我们可以极端⼀点,假设是100%存活,那么我们需要将所有对象都复制⼀遍,并将所有引⽤地址重置⼀遍。复制这⼀⼯作所花费的时间,在对象存活率达到⼀定程度时,将会变的不可忽视。所以从以上描述不难看出,复制算法要想使⽤,最起码对象的存活率要⾮常低才⾏,⽽且最重要的是,我们必须要克服50%内存的浪费。
年轻代中使⽤的是Minor GC,这种GC算法采⽤的是复制算法(Copying)。
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认⽐例为8:1:1,⼀般情况下,新创建的对象都会被分配到Eden区。因为年轻代中的对象基本都是朝⽣⼣死的(90%以上),所以在年轻代的垃圾回收算法使⽤的是复制算法。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进⾏GC,Eden区中所有存活的对象都会被复制到“To”,⽽
在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。对象在Survivor区中每熬过⼀次Minor GC,年龄就会增加1岁。年龄达到⼀定值(年龄阈值,可以通过-
XX:MaxTenuringThreshold来设置)的对象会被移动到年⽼代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时
候,“From”和“To”会交换他们的⾓⾊,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会⼀直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年⽼代中。
因为Eden区对象⼀般存活率较低,⼀般的,使⽤两块10%的内存作为空闲和活动区间,⽽另外80%的内存是⽤来给新建对象分配内存的。⼀旦发⽣GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推。
标记清除(Mark-Sweep)
“标记-清除”(Mark Sweep)算法是⼏种GC算法中最基础的算法,是因为后续的收集算法都是基于这种思路并对其不⾜进⾏改进⽽得到的。正如名字⼀样,算法分为2个阶段:
标记出需要回收的对象,使⽤的标记算法均为可达性分析算法。
回收被标记的对象。
缺点:
效率问题(两次遍历)
空间问题(标记清除后会产⽣⼤量不连续的碎⽚。JVM就不得不维持⼀个内存的空闲列表,这⼜是⼀种开销。⽽且在分配数组对象的时候,寻连续的内存空间会不太好。)标记压缩(Mark-Compact)
标记-整理法是标记-清除法的⼀个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第⼆个阶段,该算法并没有直接对死亡的对象进⾏清理,⽽是通过所有存活对像都向⼀端移动,然后直接清除边界以外的内存。吴启华主演的电影
优点:
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的⾼额代价。
缺点:
如果存活的对象过多,整理阶段将会执⾏较多复制操作,导致算法效率降低。
⽼年代⼀般是由标记清除或者是标记清除与标记整理的混合实现。
分代回收算法(Generational-Collection)
从简单情况的时间复杂度来看:
内存效率:复制算法 > 标记清除算法 > 标记整理算法
内存整齐度:复制算法 = 标记整理算法 > 标记清除算法
内存利⽤率:标记整理算法 = 标记清除算法 > 复制算法
可以看出,效率上来说,复制算法是当之⽆愧的⽼⼤,但是却浪费了太多内存,⽽为了尽量兼顾上⾯所提到的三个指标,标记整理算法相对来说更平滑⼀些,但效率上依然不尽如⼈意,它⽐复制算法多了⼀个标记的阶段,⼜⽐标记清除多了⼀个整理内存的过程
分代回收算法实际上是把复制算法和标记整理法的结合,并不是真正⼀个新的算法,⼀般分为:⽼年代和年轻代,⽼年代就是很少垃圾需要进⾏回收的,新⽣代就是有很多的内存空间需要回收,所以不同代就采⽤不同的回收算法,以此来达到⾼效的回收算法。
年轻代(Young Gen)
袁立照片
年轻代特点是区域相对⽼年代较⼩,对像存活率低。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像⼤⼩有关,因⽽很适⽤于年轻代的回收。⽽复制算法内存利⽤率不⾼的问题,通过hotspot中的两个survivor的设计得到缓解。
⽼年代(Tenure Gen)
⽼年代的特点是区域较⼤,对像存活率⾼。
这种情况,存在⼤量存活率⾼的对像,复制算法明显变得不合适。⼀般是由标记清除或者是标记清除与标记整理的混合实现。
垃圾收集器
垃圾回收的具体实现:Serial收集器、 Parallel收集器、 CMS收集器、G1收集器。
发布评论