一、JVM内存模型
1、为什么jvm在java中如此重要
jvm不仅承担了java字节码的分析(JIT)和执行(Runtime),同时也内置了自动内存分配管理机制。大大降级低了手动分配
内存溢出,内存泄漏的风险,开发人员只需要更专注于业务本身。生成的字节码就可以在多种平台运行。
2、JRE/JDK/JVM是什么关系
JRE:JavaRuntimeEnvironment,java运行环境。所有程序需要在jre下才能运行。
JDK:JavaDevelopmentKit,开发者用来编译,调试java程序的开发工具包。安装jdk的时候,同时在目录下也会有一个jrd目录。
JVM:javavirtualMachine,java虚拟机是jre的一部分。
3、jvm内存模型。
见图-网上找后自己画,加深记忆。
4、jvm由哪些部分组成,运行流程是什么
JVM包含两个子系统和两个组件:两个子系统Class Loader(类装载),Execution engine(执行引擎);连个组件为Runtime data area(运行时数据区),Native Interface(本地接口)。
流程:首先通过编译器把java代码转换成字节码,类加载器(Class Loader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区,而字节码文件只是JVM的
一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行(Execution Engine),将字节码翻译成底层系统指令,在交由CPU去执行,而这个过程中需要调用其他
语言的本地库接口(Native Interface)来实现整个程序的功能
二、即时编译器JIT、优化java编译
java文件被编译成class文件的过程,一般称为前期编译。除了前期编译还有运行时编译,由于机器无法直接运行java生成的字节码,
所以在运行时,JIT或者解释器会将字节码转化成机器码,这个过程就叫做运行时编译。
类编译加载过程,见图-网上找后自己画,加深记忆。
1、类编译:
前期编译是非常复杂的一个过程,包括词法分析、填充符号表、注解处理、语义分析以及生成class文件,这个过程不用太关注。
只要知道编译后的字节码文件主要包括常量池和方法集合这两部分呢。
常量池:主要记录的类文件中出现的字面常量以及符号引用。字面常量包括字符串常量(string str="abc",其中abc就是常量)。声明为
final的属性以及一些基本类型的属性。符号引用包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。
方法表集合:主要包括一些方法的字节码、方法访问权限(public,protect,private),方法名索引(与常量池中的方法引用对应)、描述符索引
jvm指令执行及属性集合等。
2、类加载:
(1)加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。
(2)连接过程:
验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。(文件格式验证,元数据验证,字节码验证,符号引用验证)
准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
解析:虚拟机常量池的符号引用替换为字节引用过程
(3)初始化阶段是执行类构造器()方法的过程。类构造器()方法是由便器器自动收藏类中的,所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。
初始化的总结就是:初始化是为类的静态变量赋予正确的初始值。
(4)当一个类被创建实例或者被其他对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码文件加载到内存中。
(5)加载器
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
虚拟机自带加载器种类::
启动类加载器:也叫根加载器(bootstrap)由C++编写,负责加载存放<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的类,程序中自带的类,jdk中的本地方法类,例如Object类。
扩展类加载器:(Extension)由java编写,这个加载器由sun、misc、Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java、ext、dirs系统变量所指定的路径中的所有类库,
开发者可以直接使用扩展类加载器。
应用程序加载器:(AppClassLoader)即程序中自定义的类,new 出来的。这个类加载器由sun、misc、Launcher$App-ClassLoader实现。它负责加载用户路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器
如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
用户自定义加载器:(UserClassLoaer)用户在编写自己定义的类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。要创建用户自己的类加载器,只需要继承java.lang.ClassLoader类,然后覆盖
它的findClass(String name)方法即可,即指明如何获取类的字节码流。
如果要符合双亲委派规范,则重写findCalss方法(用户自定义类加载逻辑)
要破坏的话,重写loadClass方法(双亲委派的具体实现逻辑)
Java、lang、classloader的子类,用户可以定制类的加载方式。
Java类的加载顺序,启动类加载器->扩展类加载器->应用程序加载器。
类装载分为以下5个步骤:
加载:根据查找路径找到相应的class文件然后导入;
验证:检查加载的class文件的正确性
准备:给类中的静态变量分配内存空间
解析:虚拟机将常量池中的符合引用替换成直接引用的过程。符号引用就理解为一个标识,而直接引用直接指向内存中的地址。
初始化:对静态变量和静态代码块执行初始化工作。
3、类装载的双亲委派机制:
当一个类收到类加载请求,它首先不会尝试自己去加载这个类,而是先把这个请求委派给父类完成,每一个层次类加载器都是如此,因此所有的类加载器
请求都是应该传到启动类加载器里面的,只有当其父类加载器无法完成这个请求的时候(在它的加载路径下没有找到所需加载的class)子类加载器才会尝试
自己去加载。
优点,比如加载rt、jar包中的类java、lang、Object,不管哪个类加载器加载这个类,最终都会委托给顶层的启动类加载器加载它,保证了不同的类加载器都是
加载的同一个class、
4、类加载器沙箱安全机制:
通过双亲委派机制,类的加载永远都是从启动类加载器开始,依次下放,保证你所写的代码不会污染java自带源码,所以双亲委派机制保证了沙箱安全机制。
三、JVM内存模型
jvm执行java程序的过程中会把它所管理的内存区域划分成若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。
1、本地方法栈:
与虚拟机栈一样的,只不过本地方法栈是为虚拟机调用native方法服务的。
native:在java中是一个关键字,有声明无实现。thread、start(),线程是操作系统的,底层其实调用了native void start0()本地方法。线程就绪后,线程的
执行还是有操作系统调度cpu执行的。
2、栈:
线程私有,程序运行时的内存,随线程的创建而创建,线程的结束而销毁。包含栈帧(局部变量表,操作数栈,动态链接,方法出口)并参与方法的调用与返回。
8中基本类型变量+对象的引用变量+实例方法都是在函数的栈内存中分配的。
栈:栈对应线程,栈帧对应方法,栈帧可以认为是虚拟机栈中的单位。
局部变量:就是存放方法参数和方法内部定义的局部变量的区域。(在编译期间完成分配)
操作数栈:赋值号=后面的操作数,进行赋值,运算的时候存放的临时内存区域就是操作数栈
动态链接:每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。
方法出口:正常退出就是return,异常退出的就是抛出异常。
3、程序计数器:
线程私有,用来存储当前正在执行或者即将执行的jvm指令码对应的地址,或者行号位置。
由于java虚拟机的多线程是通过多线程轮流切换并发分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的位置,每条线程都一个独立的程序计数器,
各个线程之间计数器互不影响,独立存储。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。
4、堆:
堆是jVM中最大的一块内存区域,被所有线程共享,几乎所有对象和数组都被分配到堆内存中,堆分为年轻代(8=eden和1=survivorFrom,1=survivor)和老年代。栈管运行,堆管存储。
在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例;java堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”;从内存回收角度可区分为新生代和老年代;当前主流的虚拟机都是可扩展的(通过-Xms控制和-Xmx),
如果堆中没有内存可以完成实例分配,并且堆也无法扩展时,会抛出OutOfMemoryError异常。
5、方法区:
方法区用来存放已经被类加载器加载的类相关信息,包括类信息,运行时常量池,字符串常量池。类信息包含了类版本,方法,接口,字段和父类信息。
常量+静态变量+类元信息
在加载类的时候,JVM会先加载class文件,而在class中除了有类的版本,字段,方法,接口等描述信息外,还有一项常量池,用于存放编译期间生成的各种字面量和符号引用。
字面量包括字符串(string a="b"),基本类型变量(final修饰的变量),符号引用包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。
在解析阶段,JVM会把符号引用替换为直接引用(对象的索引值)。
6、运行原理:
见图-网上找后自己画,加深记忆。
7、深拷贝和浅拷贝
浅拷贝:只是增加了一个指针指向已存在的内存区域
深拷贝:增加了一个指针并且申请了一个新的内存,使整个增加的指针指向这个新的内存。
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟出一块新的内存地址用于存放复制的对象。
四、对象"已死"的判定方法
1、引用计数法:
给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效计数器减1;当计数器为0表示对象已死,可回收。但是它很难解决2个对象
循环引用的情况。
2、可达性分析算法:
通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连
(即对象到GC Roots不可达)则证明对象已死,可回收。注意,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过2次标记。
java中可作为GC Roots的对象:虚拟机栈中引用的对象,本地方法栈中Native引用的对象,方法区静态属性引用的对象、方法区常量引用的对象。
五、JVM垃圾回收
GC(Garbage Collection)的基本原理:将内存中不在使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,JVM在对对象的生命周期分析后,
优点:JVM的垃圾收集器都不需要我们手动处理无引用的对象。缺点:程序员不能实时对某个对象或所有对象调用垃圾回收器进行垃圾回收。
按照新生代,老年代的方法对对象进行收集,以尽可能的缩短GC对应用造成的影响。
(1)对新生代对象回收叫做minor GC
(2)对老年代对象回收叫做Full GC
(3)程序中主动调用System、gc()强制执行的GC为Full GC
不同的对象引用类型,GC会采用不同的方法进行回收,JVM的对象引用分为4种类型。
(1)强引用:默认情况下对象采用的都为强引用。(这个对象的实例没有被其他对象引用时,GC会被回收)
(2)软引用:软引用是java中提供的一种比较适合缓存的引用(只有在内存不够用的情况下才会被GC)需要用SoftReference类来实现。
(3)弱引用:在GC时一定会被回收。需要使用WeakReference类来实现。
(4)虚引用:它的作用是跟踪对象被回收的状态,必须和引用队列联合使用,需要用PhantomRenference类来实现。
六、垃圾回收算法
1、标记清除算法(mark-sweep):
最基础的垃圾回收算法,分为2个阶段,标记和清除。标记阶段标记出所有需要回收的对象,清除阶段回收已被标记的对象所占用的内存。该算法的最大问题就是
内存碎片化严重,后续可能发生大对象找不到内存空间。
2、复制算法(copying):
为了解决mark-sweep算法碎片严重化的问题,而提出了的算法。按照内存大小,将内存等大小分为2块,每次只使用其中一块,当S0存满后将尚且存活的对象赋值到S1中去。
再将S0中需要回收的清理掉。该算法虽然简单,效率高,不易产生碎片,但是最大的问题是可用内存被压缩到原本内存的一半。且存活对象增多的话,Copying算法效率会大大降低。
3、标记整理算法(mark-compact)
结合以上2种算法,为避免缺陷而提出。标记阶段和Mark-Sweep算法一致,标记后不清理对象,而是将对象移向内存的另一端,然后清除端边界外的对象。
该算法仍需要进行局部对象移动,一定程度上降低了效率
4、分代收集算法:
分代收集算法目前是大多数JVM采用的收集算法,其核心思想是根据对象的不同的生命周期将内存划分为不同的区域,新生代,老年代。
(1)新生代与复制算法,新生代需要回收大部分对象,复制操作比较少
(2)老年代与标记整理算法,老年代每次只回收少量对象,因而采用mark-compact算法。
5、分代收集算法VS分区收集算法:
目前主流的都是分代算分,按照根据对象的不同的生命周期,划分为新生代,老年代,元空间,根据个代特点进行回收。分区算法则将整个堆区划分为不同的小区间。
每个小区间独立使用,独立回收,这样做的好处是可以控制一次回收多个小区间,根据停顿时间,每次回收若干个区间(而不是整个堆),从而减少一次GC所产生的影响。
6、MinorGC,Major GC,FUllGC
MinorGC:是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以MinorGC非常频繁,一般回回收都比较快(一般采用复制算法回收垃圾)。
MajorGC:是老年代GC,指的是发生在老年代的GC,通常执行MajorGC会连着MinorGC一起执行。MarjorGC的速度要比MinorGC慢的多(采用标记清除和标记整理)
FullGC:是清理整个对空间,包括年轻代和老年代。
7、两个Survivor
Survivor的存在意义,就是减少被送到老年代的对象,进而减少FullGC的发生。设置两个Survivor区最大的好处就是解决了碎片化。
8、为什么大对象直接进入老年代
所谓大对象是指需要连续内存空间的对象,频繁出现大对象是致命的,会导致在内存中还有不少空间的情况下提前触发GC以获取足够的连续空间来安置新对象。
新生代使用的是标记-清除算法处理垃圾回收的,如果大对象直接在新生代分配就会导致Eden区和两个Survivor区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。
七、GC收集器
1、Serial收集器(单线程、复制算法)
Serial是最基本的垃圾收集器,使用复制算法,曾经是JDK1、3、1之前新生代唯一的垃圾收集器。Serial是一个单线程的收集器。在收集过程中必须暂停其他所有工作线程,直到垃圾回收结束。
但是它简单高效,对于限定单个CPU来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率。因此Serial收集器依然是JVM运行在Client模式下默认的新生代垃圾收集器。
2、ParNew收集器(Serial+多线程)
ParNew收集器其实是Serial收集器的多线程版本,也使用复制算法。除过多线程其余动作和Serial一致。ParNew默认开启和CPU数目相同的线程数。可以通过-XX:ParallelGCThreads参数
限定回收的线程数。ParNew收集器是很多java虚拟机运行在Server模式下的新生代默认收集器。
3、Parallel Scavenge收集器(多线程复制算法,高效)
Parallel Scavenge也是一个新生代收集器,同样使用复制算法。它重点关注的程序达到一个可控制的吞吐量(即吞吐量=cpu运行代码时间/(运行代码时间+垃圾收集时间))。高吞吐量可以
最高效率的利用CPU时间,尽快的完成程序运算任务,主要适用在后台运算不需要太多交互的任务。自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。
4、Serial Old收集器(单线程,标记整理算法)
Serial Old 是Serial收集器老年代版本,同样也是单线程,使用标记整理算法。主要是java虚拟机运行在client模式下的老年代默认收集器。
在Server模式下主要有2个用途:
(1)在jdk1、5之前与Parallel Scavenge搭配使用。
(2)作为老年代中CMS收集器的后备方案。
5、Parallel Old收集器(多线程,标记整理算法)
Parallel Old 是Parallel Scavenge的老年代版本使用多线程标记整理算法,在Jdk1、6才开始提供。
在JDK1、6之前新生代使用Parallel Scavenge收集器只能搭配老年代 的SerialOld收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。ParallelOld真是为了提高老年代吞吐量优先的收集器。
6、CMS收集器(多线程,标记清除算法)
Concurrent mark sweep (CMS)收集器是一种老年代收集器,其主要目的是获取最短垃圾回收停顿时间,和其他标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾回收停顿时间
可以为交互比较高的程序提高用户体验。
(1)初始标记:只是标记下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有线程。
(2)并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停线程。
(3)重新标记:为了修正并发标记期间,因用户线程继续运行导致标记产生变动的那一部分对象的标记记录。仍然需要暂停所有线程。
(4)并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户线程一起执行的,所以CMS收集器的
内存回收是和用户线程一起执行的。
7、G1收集器
Garbage First是目前发展最前沿的收集器,相比CMS
(1)基于标记-整理算法,不产生内存碎片。
(2)可以非常准确的控制停顿时间,在不牺牲吞吐量的前提下,实现低停顿垃圾回收。
G1收集器避免全区域垃圾收集,它把堆内存区域划分为大小固定几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护了一个优先级列表。每次根据所允许的垃圾收集时间,优先
回收垃圾最多的区域。区域划分和优先级区域回收机制,确保G1收集器可以在有限时间内获得最高的垃圾收集效率。
八、JVM参数
1、Boolean类型:
-XX:+/-属性,+表示开启,-表示关闭。
2、KV类型:
-XX:key=value -Xms等价于-XX:InitialHeapSize,-Xmx等价于-XX:MaxHeapSize
3、常用参数:
-Xms:等价于-XX:InitialHeapSize,初始大小内存,默认为物理内存的1/64。
-Xms:等价于-XX:MaxHeapSize,最大分配内存,默认为物理内存的1/4
-Xss:等价于-XX:ThreadStackSize,设置单个线程栈的大小,Linux默认1024KB,OS X默认1024KB,Windows默认取虚拟机内存。
-Xmn:设置新生代大小,默认是对空间的1/3
-XX:MetaspaceSize,默认情况下元空间大小受本地内存限制,也会有OOM,因为其默认内存大小是有限的。
-XX:PrintGCDetails,输出详细GC日志。
-XX:SurvivorRatio,设置新生代中Eden区和S0/S1的比例,默认为8,表示三者比例8:1:1
-XX:PermSize,设置持久代初始值
-XX:MaxPermSize,设置持久代大小
-XX:NewRatio,设置新生代和老年代在堆中的占比,设置的值就是老年代比新生代的比值。
-XX:MaxTenurningThreshold,设置垃圾最大年龄,默认是15。java8中能设置的阀值是0~15,设置较小值适用于年代比较多的应用。设置为较大值,可以增加对象在新生代存活时间,增加新生代存活概率。
4、JVM的GC收集器设置
-XX:+UseSerialGC:设置串行收集器,年轻带收集器
-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。
-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量
-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。
-XX:+UseConcMarkSweepGC:设置年老代并发收集器
-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器
九、GC调优策略
1、降低Minor GC频率
通常情况下新生代空间比较小,Eden区很快被填满,就会导致MinorGC,因此可以增大新生代空间来降级MinorGC。
单次MinorGC的时间是由2部分组成的:T1(扫描新生代)和T2(复制存活对象)。假设一个对象在Eden区的存活时间为500ms,MinorGC的间隔时间是300ms,那么正常情况下MinorGC的时间是T1+T2。
当我们增大新生代空间,MinorGC的间隔时间可能扩展到600ms,此时一个存活500ms的对象就被回收掉了。此时就不存在复制存活对象的时间,所以在发生MinorGC的时间为:2次扫描新生代即2T1。
可见扩容后,MinorGC增加了T1,但省去了T2的时间。JVM中复制对象的成本远高于扫描成本。
如果是长期存活的对象,增加年轻代,反而会增加MinorGC的时间。如果堆中短期对象较多,那么扩容新生代,单次MinorGC时间不会显著增加。
2、降低FUll GC频率
堆内空间不足会触发FullGC,FUllGC会造成STW(Stop the word),增加上下文切换,影响系统性能开销,影响系统使用。
减少创建大对象:数据库查询数据,生成大对象。
增加堆内存空间:增加堆内存空间,设置初始化内存为最大堆内存,也可降级FullGC。
3、选择合适的收集器
比如响应速度较快的垃圾收集器CMS和G1都可以。吞吐量比较高的,可以使用Parallel Scavenge。
十、调优参考指标
GC频率:高频的GC,full gc,minor gc都会给系统造成性能消耗。
内存:堆内存,如果内存不足,或者分配不均匀,会造成full gc,严重的会导致cpu持续爆满。
吞吐量:频繁的full gc会引起线程的上下文切换,增加系统西能开销。从而影响每次请求的线程处理,从而造成吞吐量下降。
延时:GC 持续时间会影响到每次请求的响应时间。