JVM
JVM
1.虚拟机是什么?
java文件通过javac变成class文件,然后进入类加载器中,并且把所需要的的类库的class一并加载,然后通过字节码解释器或者即时编译器进行编译,编译过后交给执行引擎,由执行引擎来驱动os硬件。
javac是一个什么操作?对等信息转换
jVM是解释型的还是编译型的?
都有,jVM会把常用的代码作为本地文件即时编译,然后有改动的进行解释。不用每次都进行字节码解释。
那JVM如何区分代码有无改动?
java是跨平台的语言,虚拟机是跨语言的平台。不完全统计大概有100多种语言可以在jvm上运行。
虚拟机支持多种语言,任何语言只要能被编译成class文件的都可以,JVM其实是一种规范,在不同的平台有不同的实现,比如linux和window,它帮我们屏蔽了操作系统的底层。
jvm java虚拟机
jre(java运行时环境) java虚拟机和核心类库
jdk(java开发工具包) jre和一些开发工具包
2.class文件的加载
1loading:这个过程是双亲委派,主要出于安全的原因使用,防止别人恶意篡改基类。其实使一个类不被多个类加载器重复加载,节约资源。
怎么破坏双亲委派:复写,spi,OSGI.
一个类先要变成一个字节流才能被类加载器获取。
一个类被加载的过程是先从CustomClassLoader开始加载,这时这个加载器会查看缓存中有没有这个类,有的话返回,没有的话不会立刻将该类加入内存中,而是继续向上的父加载器查看这个类有没有被加载,依次类推到了顶层BootStrap还是发现这个类没有被加载的话,就会自上而下再进行委派,给Extension这个加载器,以此类推,到了各个加载器负责扫描的范围的话,这个类就会被找到并且加载进内存,反之,要是接下来的加载器都不能在自己负责扫描的区域获得这个类的话,就会抛出ClassNotFond异常。这个过程就是双亲委派模型。
如果我自己写一个String类,别人保存密码一般都用String,并且在类中写一段获取密码并且发送邮件给我的代码,然后让类加载器加载进内存,这样就产生了极大的不安全因素,双亲委派模型可以让避免这个操作,在加载类时不会立刻加载而是去看上一级有没有加载过这个类,当我们想加入一个String类的时候,查到在父加载器加载过就不会再次加载了,从而保证了安全,和一定的效率(防止重复加载)。
通过classloader将class文件加入内存中,并且生成一个class类的对象。
这些类加载器之间只是一个语法上的父子关系,并没有严格的类继承关系。
我们调用一个方法的时候不是去直接调用class二进制文件,而是通过class对象(这个对象是单例的)来访问class二进制文件,找到对应的方法然后编译调用java指令。
JVM内存模型
栈:先进后出(后进先出)又叫堆栈,是一种只能在一端进行插入和删除的线性数据结构。
为什么要用栈作为线程的储存模型?因为符合程序调用的规范,先进的先加载后进来的加载完后销毁。
栈是线程的储存内存的地方,而栈帧是方法的储存内存的地方,一个栈包含多个栈帧,栈帧一般放方法的局部变量。
栈帧:包含局部变量表,操作数栈,动态链接,方法出口。
局部变量表:存储局部变量的地方(如果局部变量是对象的话那存放的就是这个对象在堆中的地址也叫引用)
操作数栈:用来临时存储进行操作运算数据的地方,算完会清空(每次从栈顶弹出两个数进行运算,然后将结果压入栈中)
动态链接:符号引用转变的直接引用。(调用那些方法符号,也就是方法名,用来标识这些方法符号的直接引用,从而可以从方法区获取这些方法的具体代码)
方法出口:调用完别的方法后,通过方法出口知道回到原方法的哪一行开始执行。
程序计数器:在每个线程中都有,每生成一个线程就从JVM中的程序计数器这个区域分配一个小区域给线程,用来记录当前线程运行到哪一行代码的行号。
为什么要设计程序计数器呢?因为方便了线程之间的切换,多线程运行的话如果当前线程被挂起,下次再切换回来的时候就知道从哪一行开始接着执行了。
堆(survivor):存放对象的地方。分为年轻代和老年代。
堆时JVM内存占用最大,管理最复杂的一个区域。唯一的途径就是存放对象实例:所有的对象实例以及数组都在堆上进行分配。jdk1.7以后,字符串常量从永久代中剥离出来,存放在堆中。堆具有进一步的内存划分。按照GC分代手机角度划分
老年代:2/3的堆空间 年轻代:1/3的堆空间 eden区:8/10 的年轻代 survivor0: 1/10 的年轻代 survivor1:1/10的年轻代
方法区(元空间matespace):存放静态变量,常量,和类信息的地方,如果是静态对象则存放的是引用,常量和一些类的对象放在方法区,java8之前叫永久代之后叫metaspace.
本地方法栈:本地方法执行过程需要的内存空间存放的地方。native
本地方法是什么?被native修饰的方法,是由C++来实现的。
GC
循环引用在引用计数法中会导致内存泄漏。
hotspot使用可达性分析算法:每个新创建的对象都先放在Eden区,然后eden区满了之后就会进行一次普通GC(minorGC),找到GCroot没有引用的对象就作为垃圾干掉,剩下的放入S0,然后如果eden区再次满了,就会把S0里面的垃圾对象也进行回收,然后将剩下的对象放入S1区并且将这些对象的分代年龄加一,然后就是跟之前S0的一样当eden区再次满了就会把S1里面的垃圾对象也进行回收,然后将剩下的对象放入S0区,这样重复下来当分代年龄到达一定的值的时候就会将这些对象放入老年代。每个垃圾收集器的值都不一样一般是15,cms是6.
分代年龄记录在对象内部的对象头里面一个四字节的空间里,跟加锁标记在一块地方。
fullGC:当老年代满了之后就会进行一次fullGC,找垃圾的方法跟minorGC一样只是这次清理的是整个堆空间。如果fullGC之后还是满的就会报OOM异常。
GCROOT:引用的根节点。
Safe Point和safe Region
GC时线程的中断策略 如何在GC生时, 检查所有线程都跑到最近的安全点停顿下来呢?
抢先式中断: ( 目前没有虚拟机采用了) 首先中断所有线程。如果还有线程不在安全点, 就恢复线程, 让线程跑到安全点。 主动式中断: 设置一个中断标志, 各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真, 则将自己进行中断挂起。(客栈门上安装了一个显示器,上面会显示ture,或者false。如果系统需要垃圾回收,就会更新这个状态为true,线程到了客栈后看到为true,就进店别别出来了。)
在GC的时候需要stw(stop the world:也就是停止用户线程只执行GC线程,因为怕用户线程改变GCROOT的引用链关系导致回收掉不该回收的对象或者是该回收的没回收掉),但是用户线程并不是随时都可以停止的,他们需要走到一个Safe Point(安全点)才能停止让GC线程进行GC,这个安全点一般是循环末尾,报异常的位置,方法返回之前,调用方法之后,因为这样可以使其他线程不需要等待太久就都可以进入安全点从而进行GC,如果此时有的用户线程处于Sleep 状态或Blocked 状态,那就需要涉及到一个安全区域Safe Region(安全区域是指在一段代码片段中, 对象的引用关系不会发生变化, 在这个区域中的任何位置开始GC 都是安全的。我们也可以把Safe Region 看做是被扩展了的Safepoint)实际执行时: 1 、当线程运行到 Safe Region 的代码时, 首先标识己经进入了safe Region ,如果这段时间内发生GC ,JVM会忽略标识为 Safe Region 状态的线程(认为它是安全的): 2 、当线程即将离开时, 会检查JVM是否己经完成GC , 如果完成 , 则继续运行, 否则线程必须等待直到收到可以安全离开Safe Region 的信号为止。
jvm调优
jvm自带工具:jvisualvm
为何调优?OOM GC过于频繁 cpu使用过高=线程阻塞或者堆内存使用过高
jps查看当前java进程
G1垃圾收集器:6G以上使用(所需机器最少为4核8G)
JVM常见的调优参数包括:
-Xmx:指定java程序的最大堆内存, 使用java -Xmx5000M -version判断当前系统能分配的最大堆内存;
-Xms:指定最小堆内存, 通常设置成跟最大堆内存一样,减少GC;
-Xmn:设置年轻代大小。整个堆大小=年轻代大小+年老代大小。所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8;
-Xss:指定线程的最大栈空间, 此参数决定了java函数调用的深度, 值越大调用深度越深, 若值太小则容易出栈溢出错误(StackOverflowError);
-XX:PermSize:指定方法区(永久区)的初始值,默认是物理内存的1/64,在Java8永久区移除, 代之的是元数据区,由-XX:MetaspaceSize指定;
-XX:MaxPermSize:指定方法区的最大值, 默认是物理内存的1/4,在java8中由-XX:MaxMetaspaceSize指定元数据区的大小;
-XX:NewRatio=n:年老代与年轻代的比值,-XX:NewRatio=2, 表示年老代与年轻代的比值为2:1;
-XX:SurvivorRatio=n:Eden区与Survivor区的大小比值,-XX:SurvivorRatio=8表示Eden区与Survivor区的大小比值是8:1:1,因为Survivor区有两个(from, to)。
JVM实质上分为三大块,年轻代(YoungGen),年老代(Old Memory),及持久代(Perm,在Java8中被取消)。
年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在年轻代和年老代回收上的时间比例。
减少年轻代和年老代花费的时间,一般会提高应用的效率。
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩。
目前商业化虚拟机中常用的垃圾收集器有8种:新生代:Serial、ParNew、Parallel Scavenge,老年代:CMS、Serial Old、Parallel Old,整堆:G1、ZGC。垃圾收集器在运行方式上又细分为串行、并行、并发三种。 1、串行收集器:GC线程和其他用户线程是串行的,也就是在进行垃圾回收的时候,其他的线程需要排队等待,直到收集器的线程完成工作,这类收集器停顿时间长、吞吐量低,适用于单C机器使用,使用JVM参数-XX:+UseSerialGC开启, 2、并行收集器是采用多线程串行的方式进行垃圾回收,适用于多C机器,在多个线程执行垃圾检测回收时,可能会因为线程之间竞争CPU资源而发生长时间的停顿,体验极差,不推荐使用,可以使用JVM参数-XX:+UseParallelGC、-XX:+UseParallelOldGC开启,并且可以使用-XX:ParallelGCThreads=<thread_nums>指定GC的线程数量,一般不要高于CPU数量,否则就容易gg;是jdk1.8中默认使用的垃圾收集器; 3、并发收集器是目前使用较多的一类收集器,与并行收集器不同的是它采用多个线程并行执行去进行垃圾检测和回收,停顿时间短,吞吐量大,可以使用JVM参数-XX:+UseConcMarkSweepGC开启,可以搭配-XX:+UseParNewGC一同使用; 4、说到整堆收集器就只有说说G1收集器了,G1收集器具有并行与并发、分代收集、空间整合和可预测的停顿等特点。
-
收集器既并行又并发,能够充分利用多CPU,缩短STW的停顿时间
-
因为G1能够管理整个堆,而不需要和其他的收集器搭配使用,虽然依然采用了分代模式,但它把堆分成了大小相等的若干个独立区域,相邻区域很可能是一个是新生代,一个是老年代。
-
整个过程中不会产生内存碎片,整体使用的“标记-整理”算法,局部使用的是复制算法,这两种算法都不会产生内存碎片,适合长时间运行。
-
低停顿的同时实现高吞吐量;G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒?
Tip:垃圾收集器在不断进化的过程实际上就是为了程序运行更快,STW时间更短,所以我们在实际的开发过程中也要掌握一些减少GC次数的手段,比如:
-
对象不使用时将其置为null
-
尽量不用System.gc()
-
尽量少用静态变量
-
使用StringBuffer代替String拼装字符串
-
分散对象创建和删除的时间
-
尽量少用finallize方法
-
使用基本类型替代基本类型封装类
-
注意: 32G是个近似值,这个临界值跟JVM和平台有关,当我们线上真正启动服务的时候直接设置 -Xmx=32GB 的时候很可能导致 CompressedOop 失效,那我们怎么确定当前环境下最大内存设置多大才且最大限度的使用内存才能启动 CompressedOop 呢?我们可以通过增加JVM参数 -XX:+PrintFlagsFinal,验证UseCompressedOops的值,从而得知,到底是不是真的开启了压缩指针,还是压缩指针失效!
-
慎用软引用、弱引用和虚引用
浙公网安备 33010602011771号