jvm笔记(自用)

JVM怎么执行java代码
首先把java文件编译成class文件(比如从jar包加载),然后通过类加载器加载到jvm,然后jvm去执行。下图是一个大概的流程:

 

 

1. 类加载器:
1)根类加载器(Bootstrap ClassLoader):加载java的核心类,也就是加载java安装目录下,"lib"目录里面的核心类库(win环境)
2)扩展类加载器(Extendsion ClassLoader):加载JRE的扩展目录,也就是java安装目录下,"jre/lib/ext"目录里面的类库(win环境)
3)应用程序类加载器(Application ClassLoader):加载"ClassPath"环境变量所指定路径中的类,大致理解就是加载写好的代码
4)自定义类加载器:
 
1.1 类加载器机制:
1)全盘负责:就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
2)双亲委派:当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

 

 

3)缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么代码修改重新编译Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
 
1.2 类加载过程:
1)验证:校验class文件是否符合规范
2)准备:为这个类分配内存空间,然后为类变量(被static修饰的变量)赋值一个默 认的初始值。但是如果类变量同时被final修饰的话,就不是赋值初始值而是具体的值。
3)解析:解析阶段就是jvm将常量池的符号引用替换为直接引用。(了解就行)
4)初始化:在准备阶段我们已经为加载到jvm的类分配了内存空间并且为类变量赋予了初始值。而到了初始化阶段,才真正开始执行类中定义的java程序代码。主要有以下步骤:为类的静态变量赋予正确的初始值;执行类的静态代码块;按照顺序自上而下运行类中的类变量赋值语句和静态语句,并且只有类或接口被Java程序首次主动使用时才初始化他们;如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。
 
2. jvm内存模型

 

 

  1. 程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  1. java虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  1. 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
  1. java堆
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。
  1. 方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。
 
2.1 jvm内存相关参数
1)-Xms:Java堆内存的大小
2)-Xmx:Java堆内存的最大大小
3)-Xmn:Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了
4)-XX:PermSize(jdk1.8改为-XX:MetaspaceSize):永久代大小
5)-XX:MaxPermSize(jdk1.8改为-XX:MaxMetaspaceSize):永久代最大大小
6)-XSS:每个线程的栈内存大小
 
3. 垃圾回收
我们从jvm内存模型了解到对象是分配在堆上的,也就是说堆分配的对象占满堆内存后,jvm进行对象的回收。垃圾回收一般使用分代算法,根据对象存活周期不同,分为:年青代、老年代。这是因为不同对象存活时间不一致,有些可能只使用一次,使用后就需要回收,而有些对象却会伴随整个程序的生命周期。分代有利于堆不同生命周期的对象进行管理,减少GC次数,提高运行效率。
 
3.1 新生代垃圾回收算法——ParNew
ParNew用于新生代的垃圾回收,包含一个eden区,两个survivor区,默认比例为8:1:1。如果要指定jvm使用ParNew垃圾回收器回收新生代,使用"-XX:+UseParNewGC"。当Eden内存区域被占满不能继续分配内存给对象,那么就会触发yong gc。

 

3.2 对象啥时候进入老年代
1)躲过15次young gc后进入老年代。就是说,新生代垃圾回收了15次该对象还存活那么直接晋升。默认对象15岁后进入老年代中,可以使用"-XX:MaxTenuringThreshold"来设置。
2)动态年龄计算。-XX:TargetSurvivorRatio 目标存活率,默认为50%。比如说:年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),那么年龄2和年龄3的对象都要晋升。
3)大对象直接进入老年代。jvm有个参数:"-XX:PretenureSizeThrehold" 后面的值为字节数,比如1048576就代表是1M。他的意思是如果你要创建一个大于这个值的对象,那么这个对象直接就放入到老年代中,都不经过新生代了。
4)经过yong gc后存活对象太多无法放入另外一个空的Survivor区,那么这些对象就会直接转移到老年代中去。
 
3.3老年代空间分配担保规则
  1. 每次yong gc先判断老年代剩余空间大小是否大于新生代所有对象的大小综合,防止极端情况下所有对象都存活下来。
  2. 第一步的判断成功,那么直接执行yong gc。
  3. 第一步判断失败,那么jvm检查"-XX:-HandlePremotionFailure"是否有设置,如果没有设置,那么进行old gc。
  4. "-XX:-HandlePremotionFailure"有设置,那么判断老年代内存大小是否大于之前每次yong gc进入老年代对象的平均大小,判断成功,那么直接进行yong gc。
  5. 进行yong gc会出现几种情况:
1)yong gc后,剩余存活对象的大小小于Survivor区的大小,那么存活对象直接进入Survivor区
2)yong gc后,剩余存活对象的大小大于Survivor区的大小,但是小于老年代可用空间大小,那么存活对象直接进入老年代
3)如果以上都不满足,那么就会触发old gc。
4)如果old gc后,内存还是不足,那么就会出现OOM(Out Of Memory)。

 

 

3.4 老年代垃圾回收——标记清理算法CMS(Concurrent Mark Sweep)
CMS就是在老年代需要进行old gc时,先通过追踪GC ROOTS的方法,看看各个对象是否被GC ROOTS引用,如果是的话,那就是存活对象,否则就是垃圾对象,然后一次性把垃圾对象都回收掉。这种垃圾回收算法最大的问题就是会产生很多内存碎片。
1)初始标记(CMS initial mark):让系统的工作线程全部停止,进入"stop the world",然后标记出所有被GC ROOTS直接引用的对象。
2)并发标记(CMS concurrent mark):系统工作线程继续运行并创建对象,同时对已有的对象进行GC ROOTS追踪,看对象最终是否被GC ROOTS间接引用。由于要追踪所有的对象,所以这个阶段是最耗时的一个阶段。
3)重新标记(CMS remark):由于第二阶段标记的过程中系统还会继续创建对象并且对象还可能会变成垃圾,所以在这个阶段就是重新标记在阶段二产生的垃圾对象。由于只追踪少数的对象,所以这个阶段执行时间会很快结束。这个阶段也会让系统停止运行。
4)并发清理(CMS concurrent sweep):系统会正常运行,并行清理之前标记的垃圾对象。
posted @ 2020-07-20 15:57  YzdFly  阅读(146)  评论(0编辑  收藏  举报