JVM虚拟机

Posted miraclemaker

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM虚拟机相关的知识,希望对你有一定的参考价值。

1.基础知识:

  • java代码执行流程:java源代码编译后为字节码,每一行字节码为一条jvm指令;程序计数器(由寄存器组成)记住当前执行的jvm指令地址,解释器从程序计数器中取得jvm指令地址后解释成为机器码,之后交给cpu运行。
  • 栈和栈帧:线程运行需要的内存空间,内部有栈帧组成(方法调用时占用的内存)。栈内存方法调用结束后自动回收,不需要垃圾回收。StackOverFlowError:线程请求栈的深度超过虚拟机栈的最大深度。
  • 本地方法栈:java为了调用更底层或其他语言编写的代码,我们需要调用很多本地方法(用native修饰的,比如Thread的start(),hashCode(),clone(),getClass()等),这些方法使用的空间。
  • :new出来的对象,jdk1.8后还包括stringtable(串池),需要垃圾回收。OutOfMemoryError:对内存不足,并且GC也无法清理足够的内存。
  • 方法区

载类的一些信息如,静态变量,字段信息,类信息,常量池(保存字面量和符号引用的表),运行时常量池;存在垃圾回收。数据的引用在栈中,先去堆里面找对象实例,再去方法区找对象的类数据。

。jdk8之后移除永久代,出现元空间,包含方法区的基本功能,最大区别在于元空间所占用的是jvm本地内存,不占用虚拟机运行时数据区。

。在主机内存中有一块被称为JVM,JVM内部包括运行时数据区,类加载器,执行引擎等,永久代在运行时数据区内,元空间在运行时数据区外,JVM内。

  • 线程与资源:私有资源:虚拟机栈,本地方法栈,程序计数器,共享资源有:堆,元空间。因为线程切换后回到正确的执行位置,pc是线程私有的,而栈私有是因为防止其他线程访问,修改自己的局部变量。
  • 单核多线程:单核不存在真正的并发,按理说多线程存在上下文切换效率会更低,但多线程提高效率和IO密切相关,IO操作时将数据从磁盘读取到内存这一步巨慢,我们用这个时间去执行其他任务就可以提升效率。

2.内存溢出的情况:

  • 堆溢出:对象一直创建二未被回收,虚拟机栈的线程越来越多,加载的类越来越多
  • 栈溢出:方法调用次数过多,一般都是递归不当造成。

3.如何判断对象可以被回收:

  • 引用计数法:为每个对象添加引用计数器,有人引用就+1,循环引用时无法回收,已淘汰。
  • 可达性分析法:将 虚拟机栈中引用的对象,方法区中静态变量引用的对象,本地方法栈中引用的对象作为GCRoot,从GCRoot向下搜索,没有经过任何引用链的对象可被回收。

4.垃圾回收算法:

  • 标记清除算法:第一步先标记垃圾,第二步在清除垃圾(会造成内存碎片化)。
  • 标记整理算法:第一步标记清除法,第二步把占用内存的对象向前移动整理(但操作复杂,速度慢)。多用在老年代。
  • 复制法:第一步标记垃圾,第二步把占用内存的对象内存(FROM)复制到另一块内存(TO)中,第三步把垃圾清理后内存作为TO。多用在新生代。

5.垃圾回收流程:

  • 定义:整个堆内存被分为新生代(伊甸园+幸存区FROM+幸存区TO)与老年代
  • 流程:当有新变量来了,先分配到伊甸园;伊甸园满了之后会进行一次Minor GC,将伊甸园与幸存区FROM中无用的垃圾清除,有用的转存到幸存区TO中,并将幸存区TO中所有对象代+1,复制有用内存到TO后交换FROM与TO指针指向的内存(一次Minor GC后伊甸园与幸存区TO都是空的)。当代数达到某个阈值,将其转移到老年代中。当老年代与新生代内存都不够用时,进行一次FULL GC。若对象过大,则直接放入老年代

6.垃圾回收器:

  • Parallel:关注吞吐量,新生代采用复制算法,老年代采用标记-整理算法
  • CMS:以最小停顿时间为目标,采用标记清除算法,可以并发;
  • G1:jdk9后的默认垃圾回收器,同时注重吞吐量和最小停顿时间,采用可达性分析算法找到垃圾,标记整理-复制为清除算法清除垃圾。支持并发。

7.引用:

  • 强引用:new出来的对象,内存溢出的时候也不会回收,报OOM也不回收。
  • 软引用:用软引用创建的对象,只有在内存不足时才会被回收(经常使用,加速垃圾回收,保护系统安全)
  • 弱引用:每次垃圾回收都会被回收。
  • 虚引用:不是引用,配合引用队列,只是作为变量是否被回收的标志(先将虚引用放入队列,再回收对象)

8.jvm内存参数:

  • -Xmx[]:空间最大内存。当空余堆内存过少时,堆内存扩大至此。
  • -Xms[]:空间最小内存。当空余堆内存过多时,堆内存会缩小至此,服务端一般设置Xmx和Xms一致。
  • -Xmn[]:新生代最大内存。
  • -xx[survivorRatio=4],伊甸园区与from+to区的内存占比,默认为4;
  • -xx[use G1]:指定垃圾回收器
  • -xss:设置单个线程大小,适中即可,过大会导致线程数偏少。
  • 一般我们设置堆空间为最大物理地址的1/4。

9.对象的创建过程:

  • 去常量池中定位该类符号引用,看符号引用对应的类是否被加载过,若未加载,先加载类。
  • 为对象以cas形式分配空间,防止分配空间时,指针还未修改就同时被另一个对象拿来分配内存。
  • 先为属性赋值为0或者null。
  • 设置对象头。
  • 为属性赋值,执行构造方法,引用指向该内存。

10.类加载过程与初始化顺序:

  • 加载:java代码编译后形成字节码,记载就是将字节码以二进制形式读入内存
  • 链接:

。验证:验证字节码的正确性和安全性

。准备:为静态变量分配内存,final修饰的编译时就已经赋值,实例变量实例化对象的时候才分配

。解析:将符号引用解析为直接引用(指向类的地址)

  • 初始化:执行类构造器,为静态变量赋初值并初始化静态代码块。
  • 初始化顺序:父类静态变量和静态代码块->子类静态代码块和静态变量->父类代码块和普通变量->父类构造方法->子类代码块和普通变量->子类构造方法。

11.类加载器和双亲委派机制:

  • 从父类加载器到子类加载器分别为:

。BootStrapClassLoader 加载路径为:JAVA_HOME/jre/lib

ExtensionClassLoader 加载路径为:JAVA_HOME/jre/lib/ext

。ApplicationClassLoader 加载路径为:classpath:

。自定义类加载器

  • 双亲委派机制:

机制:当一个类加载器收到类加载请求时,会先把这个请求交给父类加载器处理,若父类加载器找不到该类,再由自己去寻找。

。作用:能够保证同名的类都只加载一次,避免篡改java核心class造成安全问题。

12.对象头中的信息:

  • MarkWork:存储运行时的数据,如hashcode,gc代数,gc标记,锁的状态,获取到锁的线程id等。
  • ClassPointer:记录对象所属类的内存地址。
  • 如果是数组,还会有数组长度。

13.StringTable:

  • 储存字符串常量。如果我们创建的部分变量已经在Stringtable存在,就会去里面取,减少了内存使用
  • 程序中所有“”中的字符串都已经放入串池中。如果一个新定义的String有确定结果(s3),那就放到串池中。如果新定义的无确定结果,就会创建新对象到堆中。
  • intern():将字符串对象尝试放入串池,有就不放,没有就放。会返回一个串池中的对象。

14.方法区与元空间:

  • 方法区加载类的一些信息并保存,JDK8前实现为永久代,jdk8后实现为元空间;
  • 永久代包括类信息,字段信息,静态常量,常量池,运行时常量池,串池等,需要进行FullGC进行类的回收,处于运行时数据区。
  • 元空间包括类信息,字段信息;静态变量和串池被转移到堆中,JVM的Full GC不需要考虑元空间,处于JVM直接内存中。
  • 为什么要替换:

永久代的对象需要Full GC,Full GC会导致程序暂停,放于直接内存中就减少了压力。

。直接内存中内存更大,不容易导致数据溢出,能够加载更多的类。

  • 为什么串池要放到堆中:永久代的对象回收需要Full GC,GC回收效率太低,导致创建的字符串大量无人引用仍然不会回收,放到堆中就能高效的回收。

15.cpu飙升你怎么解决:

  • cpu上下文切换过多(文件传输导致线程阻塞),cpu资源过度消耗(运行的程序太多了,有死锁)
  • 用top命令查看线程情况,如果是几个线程占用的平均分布,那可能是开的程序太多了,如果是一个占用的特别多,可能是发生了bug。我们用jstack命令生成线程的快照,在快照中可以看到发生问题的代码。

以上是关于JVM虚拟机的主要内容,如果未能解决你的问题,请参考以下文章

JVM虚拟机与Android虚拟机

JVM虚拟机与Android虚拟机

Java虚拟机(JVM)

Java虚拟机(JVM)

Java虚拟机(JVM)

JVM常用虚拟机命令汇总