Java Virtual Machine(Java虚拟机)是java程序实现跨平台的⼀个重要的⼯具(部件)。
HotSpot VM,相信所有Java程序员都知道,它是Sun JDK和OpenJDK中所带的虚拟机,也是⽬前使⽤范围最⼴的Java虚拟机。
只要装有JVM的平台,都可以运⾏java程序。那么Java程序在JVM上是怎么被运⾏的?
通过介绍以下JVM的三个组成部分,就可以了解到JVM内部的⼯作机制
类加载系统
运行时数据区
执行引擎
⼀个类被加载进JVM中要经历哪⼏个过程
加载: 通过io流的⽅式把字节码⽂件读⼊到jvm中(⽅法区)
校验:通过校验字节码⽂件的头8位的16进制是否是java魔数cafebabe
准备:为类中的静态部分开辟空间并赋初始化值
解析:将符号引⽤转换成直接引⽤。——静态链接
初始化:为类中的静态部分赋指定值并执⾏静态代码块。
类被加载后,类中的类型信息、⽅法信息、属性信息、运⾏时常量池、类加载器的引⽤等信息会被加载到元空间中。
ext 加载路径:System.getProperty(“java.ext.dirs”);
app 加载路径:System.getProperty(“java.class.path”);
当类加载进⾏加载类的时候,类的加载需要向上委托给上⼀级的类加载器,上⼀级继续向上委托,直到启动类加载器。启动类加载器去核⼼类库中找,如果没有该类则向下委派,由下⼀级扩展类加载器去扩展类库中,如果也没有继续向下委派,直到找不到为⽌,则报类找不到的异常。
应⽤类加载器怎么加载Student和String呢?需要通过双亲委派机制
防⽌核⼼类库中的类被随意篡改
防⽌类的重复加载
当⼀个类被当前的ClassLoader加载时,该类中的其他类也会被当前该ClassLoader加载。除⾮指明其他由其他类加载器加载。
JMM分成了这么⼏个部分
线程栈:执⾏⼀个⽅法就会在线程栈中创建⼀个栈帧。
栈帧包含如下四个内容:
局部变量表:存放⽅法中的局部变量
操作数栈:⽤来存放⽅法中要操作的数据
动态链接:存放⽅法名和⽅法内容的映射关系,通过⽅法名找到⽅法内容
⽅法出⼝:记录⽅法执⾏完后调⽤次⽅法的位置。
2、类加载校验
校验该类是否已被加载。主要是检查常量池中是否存在该类的类元信息。如果没有,则需要进⾏加载。
为对象分配内存。具体的分配策略如下:
Bump the Pointer(指针碰撞):如果内存空间的分配是绝对规整的,则JVM记录当前剩余内存的指针,在已⽤内存分配
Free List(空闲列表):如果内存空间的分配不规整,那么JVM会维护⼀个可⽤内存空间的列表⽤于分配。
对象并发分配存在的问题:
Compare And Swap: ⾃旋分配,如果并发分配失败则重试分配之后的地址
Thread Local Allocation Buffer(TLAB):本地线程分配缓冲,JVM被每个线程分配⼀空间,每个线程在⾃⼰的空间中创建对象(jdk8默认使⽤,之前版本需要通过-XX:+UseTLAB开启)
根据数据类型,为对象空间初始化赋值
为对象设置对象头信息,对象头信息包含以下内瑞:类元信息、对象哈希码、对象年龄、锁状态标志等
类型指针是用来指向元空间当前类的类元信息。⽐如调⽤类中的⽅法,通过类型指针找到元空间中的该类,再找到相应的⽅法。
开启指针压缩后,类型指针只⽤4个字节 储,否则需要8个字节存储
过⼤的对象地址,会占⽤更⼤的带宽和增加GC的压⼒。
对象中指向其他对象所使⽤的指针:8字节被压缩成4字节。 最早的机器是32位,最⼤⽀持内存 2的32次⽅=4G。现在是64位,2的64次⽅可以表示N个T的内存。内存32G即等于2的35次⽅。如果内存是32G的话,⽤35位表示内存地址,这样过于浪费。如果把35位的数据,根据算法,压缩成32位的数据(也就是4个字节)。在保存时⽤4个字节,再使⽤时使⽤8个字节。之前⽤35位保存内存地址,就可以⽤32位保存。这样8个字节的对象,实际上使⽤32位来保存,这样64位就能表示2个对象。如果内存⼤于32G,指针压缩会失效,会强制使⽤64位来表示对象地址。因此jvm堆内存最好不要⼤于32G。
为对象中的属性赋值和执⾏构造⽅法。
在堆空间和元空间中,GC这条守护线程会对这些空间开展垃圾回收⼯作,那么GC如何判断这些空间的对象是否是垃圾,有两种算法:
对象被引⽤,则计数器+1,如果计数器是0,那么对象将被判定为是垃圾,于是被回收。但是这种算法没有办法解决循环依赖的对象。因此JVM⽬前的主流⼚商Hotspot没有使⽤这种算法。
可达性分析算法
:GC Roots根
判断依据:gc在扫描堆空间中的某个节点时,向上遍历,看看能不能遍历到gc roots根节点,如果不能,那么意味着这个对象是垃圾。
Object类中有⼀个finalize⽅法,也就是说任何⼀个对象都有finalize⽅法。这个⽅法是对象被回收之前的最后⼀根救命稻草。
GC在垃圾对象回收之前,先标记垃圾对象,被标记的对象的finalize⽅法将被调⽤
调⽤finalize⽅法如果对象被引⽤,那么第⼆次标记该对象,被标记的对象将移除出即将被回收的集合,继续存活
调⽤finalize⽅法如果对象没有被引⽤,那么将会被回收
注意,finalize⽅法只会被调⽤⼀次。
在jdk1.7之前,对象的创建都是在堆空间中创建,但是会有个问题,⽅法中的未被外部访问的对象这种对象没有被外部访问,且在堆空间上频繁创建,当⽅法结束,需要被gc,浪费了性能。所以在1.7之后,就会进⾏⼀次逃逸分析(默认开启),于是这样的对象就直接在栈上创建,随着⽅法的出栈⽽被销毁,不需要进⾏gc。
在栈上分配内存的时候:会把聚合量
替换成标量,来减少栈空间的开销,也为了防⽌栈上没
有⾜够连续的空间直接存放对象。
标量
:java中的基本数据类型(不可再分)
聚合量
:引⽤数据类型。
-XX:PretenureSizeThreshold
XX:MaxTenuringThreshold
-XX:+UseSerialGC -
XX:+UseSerialOldGC
单线程执⾏垃圾收集,收集过程中会有较⻓的STW(stop the world),在GC时⼯作线程不能⼯作。虽然STW较⻓,但简单、直接。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
-XX:+UseParallelGC
-XX:+UseParallelOldGC
使⽤多线程进⾏GC,会充分利⽤cpu,但是依然会有stw,这是jdk8默认使⽤的新⽣代和⽼年代的垃圾收集器。充分利⽤CPU资源,吞吐量⾼。
-XX:+UseParNewGC
⼯作原理和Parallel收集器⼀样,都是使⽤多线程进⾏GC,但是区别在于ParNew收集器可以和CMS收集器配合⼯作。主流的⽅案:
ParNew收集器负责收集新⽣代。CMS负责收集⽼年代。
-XX:+UseConcMarkSweepGC
⽬标:尽量减少stw的时间,提升⽤户的体验。真正做到gc线程和⽤户线程⼏乎同时⼯作。CMS采⽤标记-清除算法
初始标记: 暂停所有的其他线程(STW),并记录gc roots直接能引⽤的对象。
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较⻓但是不需要STW,可以与垃圾收集线程⼀起并发运⾏。这个过程中,⽤户线程和GC线程并发,可能会有导致已经标记过的对象状态发⽣改变。
重新标记:为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短。主要⽤到三⾊标记⾥的算法做重新标记。
并发清理:开启⽤户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为⿊⾊不做任何处理。
并发重置:重置本次GC过程中的标记数据。
在并发标记阶段,对象的状态可能发⽣改变,GC在进⾏可达性分析算法分析对象时,⽤三⾊来标识对象的状态
灰⾊:这个对象被GC Roots遍历过但其部分的引⽤没有被GC Roots遍历。在重新标记时重新遍历灰⾊对象。
⽩⾊:这个对象没有被GC Roots遍历过。在重新标记时该对象如果是⽩⾊的话,那么将会被回收。
不同的垃圾收集器可以组合使⽤,在使⽤时选择适合当前业务场景的组合。
‐Xms3072M ‐Xmx3072M ‐Xss1M ‐XX:MetaspaceSize=256M
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M
设置元空间⼤⼩,最⼤值和初始化值相同
根据业务场景计算出每秒产⽣多少的对象。这些对象间隔多⻓时间会成为垃圾(⼀般根据接⼝响应时间来判断)
计算出堆中新⽣代中eden、survivor所需要的⼤⼩:根据上⼀条每条产⽣的对象和多少时间成为垃圾来计算出,依据是尽量减少full gc。
结合垃圾收集器:PraNew+CMS,对于CMS的垃圾收集器,还需要加上相关的配置:
-XX:CMSInitiatingOccupancyFraction=85 (默认是92),相当于⽼年代使⽤率达到85%就触发full gc,于是还剩15%的空间允许在cms进⾏gc的过程中产⽣新的对象。
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3
java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -
XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -
XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=85 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=3 -jar device-service.jar
重点作业:
清晰的掌握类加载过程及双亲委派机制
掌握程序在运⾏时 JVM的运⾏时数据区中发⽣了怎样的变化
对象的创建的流程
对象成为垃圾的判断依据
垃圾回收算法有哪些
JVM空间内存分配及垃圾回收器的常⽤参数配置
Heap内存(老年代)持续上涨达到设置的最大内存值
Full GC 次数频繁
GC 停顿(Stop World)时间过长(超过1秒,具体值按应用场景而定)
应用出现OutOfMemory 等内存异常
应用出现OutOfDirectMemoryError等内存异常( failed to allocate 16777216 byte(s) of direct memory (used: 1056964615, max: 1073741824))
应用中有使用本地缓存且占用大量内存空间
系统吞吐量与响应性能不高或下降
应用的CPU占用过高不下或内存占用过高不下
细节可见此博客链接:点我跳转
GC:垃圾回收(Garbage Collection),在计算机领域就是指当一个计算机上的动态存储器(内存空间)不再需要时,就应该予以释放,以让出存储器,便于他用。这种存储器的资源管理,称为垃圾回收。
这三个问题将分别对应接下来的3节一一解答
JVM清理的是哪一块的对象?判断垃圾方法
哪些对象会被清理,为什么清理A而不清理B?
JVM又是如何清理的?回收算法
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
CMS :标记-清除”
G1:标记-整理
在CMS内存中,如果一个对象过大,进入S1、S2区域的时候大于改分配的区域,对象会直接进入老年代。
G1处理大对象时会判断对象是否大于一个Region大小的50%,如果大于50%就会横跨多个Region进行存放回收过程不一样
初始标记
并发标记
重新标记
并发清理
并发重置
初始标记
并发标记
最终标记
筛选回收
**初始标记:**标记GC Roots 可以直接关联的对象,该阶段需要线程停顿但是耗时短
**并发标记:**寻找存活的对象,可以与其他程序并发执行,耗时较长
**最终标记:**并发标记期间用户程序会导致标记记录产生变动(好比一个阿姨一边清理垃圾,另一个人一边扔垃圾)虚拟机会将这段时间的变化记录在Remembered Set Logs 中。最终标记阶段会向Remembered Set合并并发标记阶段的变化。这个阶段需要线程停顿,也可以并发执行
**筛选回收:**对每个Region的回收成本进行排序,按照用户自定义的回收时间来制定回收计划
初始标记和并发标记和CMS的过程是差不多的,最后的筛选回收会首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
因为采用的标记——整理的算法,所以不会产生内存碎片,最终的回收是STW的,所以也不会有浮动垃圾,Region的区域大小是固定的,所以回收Region的时间也是可控的
同时G1 使用了Remembered Set来避免全堆扫描,G1中每个Region都有一个与之对应的RememberedSet ,在各个 Region 上记录自家的对象被外面对象引用的情况。当进行内存回收时,在GC根节点的枚举范围中加入RememberedSet 即可保证不对全堆扫描也不会有遗漏。
以上就是CMS和G1的对比过程
这是本人今年春招找实习工作准备总结,记录在此,如有需要的老铁可以看看,如有问题可以留言指导