JVM整理

1.什么是JVM?

概述:Java程序的跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行。因此在运行时,Java源程序需要通过编译器编译成为.class文件。众所周知java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dll(windows下,下皆以windows平台为例,linux下和solaris下其实类似,为:libjvm.so),这个动态连接库才是java虚拟机的实际操作处理所在。

2.Java内存结构

2.1 类装载器(ClassLoader)子系统

每一个Java虚拟机都由一个类加载器子系统(class loader subsystem),负责加载程序中的类型(类和接口),并赋予唯一的名字。每一个Java虚拟机都有一个执行引擎(excution engine)负责执行被加载类中包含的指令。
JVM的两种类装载器包括:启动类装载器和用户自定义类装载器,启动类装载器是JVM实现的一部分,用户自定义类装载器则是Java程序的一部分,必须是ClassLoader类的子类。
在这里插入图片描述

  • 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
    JVM加载class文件的原理机制:
    JVM中的类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数据读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完成,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包含验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:
  • 1 )如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
  • 2 )如果类中存在初始化语句,就依次执行这些初始化语句。类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java2(JDK1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是跟加载器,其他的加载器都有且仅有一个父类加载器。下面是关于几个类加载器的说明:
    – Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
    – Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
    –System:又叫应用类加载器.其父类是Extension。它是应用最广泛的类加载器。他从环境变量classpath或者系统属性java.class.path所指定的目录中记载类.是用户自定义加载器的默认父加载器。

2.2 运行时数据区

在这里插入图片描述

  • 方法区和堆由所有线程共享
    堆:存放所以程序在运行时创建的对象
    方法区:当JVM的类加载器加载.class文件.并进行解析,把解析的类型信息放入方法区。
  • Java栈和PC寄存器由线程独享
    JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址
  • 本地方法栈:存储本地方法调用的状态

2.3 JVM运行时数据区详解

  • 方法区域(Method Area)
    在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。
    方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下他也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

  • 堆(Heap)
    它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。
    堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。
    Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
    TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。

  • JavaStack(java的栈):虚拟机只会直接对Javastack执行两种操作:以帧为单位的压栈或出栈
    每个帧代表一个方法,Java方法有两种返回方式,return和抛出异常,两种方式都会导致该方法对应的帧出栈和释放内存。
    帧的组成:局部变量区(包括方法参数和局部变量,对于instance方法,还要首先保存this类型,其中方法参数按照声明顺序严格放置,局部变量可以任意放置),操作数栈,帧数据区(用来帮助支持常量池的解析,正常方法返回和异常处理)。

  • ProgramCounter(程序计数器)
    每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的饿地址,这里的地址可以是一个本地指针,也可以是方法区中相对应于该方法起始指令的偏移量。
    若thread执行Java方法,则PC保存下一条执行指令的地址。若thread执行native方法,则PC的值为undefined

  • Nativemethodstack(本地方法栈):保存native方法进入区域的地址
    依赖于本地方法的实现,如某个JVM实现的本地方法借口使用C连接模型,则本地方法栈就是C栈,可以说某线程在调用本地方法时,就进入了一个不受JVM限制的领域,也就是JVM可以利用本地方法来动态扩展本身。

2.4 执行引擎

它或者在执行字节码,或者执行本地方法
主要的执行技术有:解释,即时编译,自适应优化、芯片级直接执行其中解释属于第一代JVM,即时编译JIT属于第二代JVM,自适应优化(目前Sun的HotspotJVM采用这种技术)则吸取第一代JVM和第二代JVM的经验,采用两者结合的方式。
自适应优化:开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行仔细优化。若方法不再频繁使用,则取消编译过的代码,扔对齐进行解释执行。

2.5 JVM垃圾回收

Sun的JVMGenerationalCollectiong(垃圾回收)原理是这样的:把对象分为年轻代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析)
通常我们说的JVM内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是之前提到的MethodArea,不属于Heap。
GC的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停

  • 1.对新生代的对象的收集称为minor GC;
  • 2.对旧生代的对象的收集称为Full GC;
  • 3.程序中主动调用System.gc()强制执行的GC为Full GC。
    不同的对象引用类型,GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
  • 1.强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)
  • 2.软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
  • 3.弱引用:在GC时一定会被GC回收
  • 4.虚引用:由于虚引用只是用来得知对象是否被GC

1.Yong(年轻代)
年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。并且,Survivor区总有一个是空的。)

2. Tenured(年老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。

3.Perm(持久代)
用户存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置

3.JVM性能调优

  • 设定堆内存大小
    -Xmx:堆内存最大限制。
  • 设定新生代大小。新生代不宜太小,否则会有大量对象涌入老年代;
  • XX:NewSize:新生代大小(建议设为整个堆大小的1/3或者1/4,两个值设为一样大)
    -XX:NewRatio新生代和老生代占比(建议比例为1:2)
    -XX:SurvivorRatio:伊甸园空间和幸存者空间的占比 (建议比例8:1)
  • 设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC

4. 常见的垃圾回收算法

4.1 标记清除法:

分为标记和清除两个步骤。第一步根据可达性算法标记被回收的对象,第二步回收被标记的对象
缺点:容易产生内存碎片
在这里插入图片描述

  • 标记整理法

4.2 标记整理法

在标记清除法的基础上多了一步整理的过程;整理步骤的时候,将所有存活的对象都往左边移动,然后清理另一端的所有区域,这样就不会产生内存碎片
缺点:虽然不会产生内存碎片,由于频繁地移动存活的对象,所以效率十分低下。
在这里插入图片描述

4.3 复制算法

把内存分成两份,分别是A区域和B区域,第一步根据可达性性算法把存活的对象标记出来,第二步把存活的对象复制到B区域,第三步把A区域全部清空。这就是复制算法。
缺点:复制算法不会产生内存碎片,并且不需要频繁移动存活的对象,而缺点就是内存利用不充分,比如一块500M的内存,要分成浪费,只能利用到250M。
在这里插入图片描述

4.4 分代收集算法

分代搜集算法是针对对象的不同特性,而使用适合的算法,这里面并没有实际上的新算法产生。与其说分代手机算法是第四个算法,不如说它是对前三个算法的实际应用。
内存中的对象其实可以根据声明周期的长短大致分为三种:

  • 1.夭折对象(新生代):朝生夕死的对象,比如方法里的局部变量。
  • 2.持久对象(老年代):存活的比较久但还是要死的对象,比如缓存对象,单例对象等等。
  • 3.永久对象(永久代):对象生成后几乎不灭的对象,例如String池中的对象(享元模式)、加载过的类信息等等。
    上述的对象对应在内存中的区域就是,夭折对象和持久对象在Java堆中,永久对象在方法区。
    分代算法的原理就是根据对象的存货周期不同将堆分为年轻代和老年代。新生代又分为Eden区,from Survivor区(SO区),to Survivor区(S1区),比例为8:1:1。
    在这里插入图片描述
    先看年轻代的GC,年轻代采用的回收算法是复制算法。新建的对象被创建后就会分配在Eden区,当Eden去将满时,就会触发GC。

在这里插入图片描述
在这一步GC会把大部分夭折对象回收,根据可达性算法标记出存活的对象,把存活对象复制到SO区,然后清空Eden区。
在这里插入图片描述
接着继续到下一次触发GC时,就会把Eden区和SO区的存活对象复制到S1区,然后清空Eden区和SO区。每次垃圾回收后SO和S1区的角色互换。每次GC后,如果对象存活下来则年龄加一。
在这里插入图片描述
我们知道在年轻代中存活得越久的对象,年龄会越大,如果存活对象的年龄达到了我们设定的阈值,则会从SO(或S1)晋升到老年代。由于老年代的对象一般不会经常回收,所以采用的算法是标记整理法,老年代的回收次数相对较少,每次回收时间比较长。

5.常见的垃圾收集器

垃圾收集器其实就是上面讲的算法的具体实现,目前没有说那个垃圾收集器是最好的,只有根据应用的特点选择最合适的,所以说合适的才是最好的。
``常见的垃圾收集器除了G1垃圾收集器外,都是只作用于一个区域,要么年轻代要么老年代,所以一般是配合使用,总共有七种,怎么配合使用,请看下面这张图,有连线的就是可以配合使用的。
在这里插入图片描述

5.1 Serial收集器(年轻代--单线程)

Serial收集器作用于年轻代,单线程的垃圾收集器,单线程意味着它只会使用一个CPU或者一个线程去完成垃圾回收的工作,当它在垃圾回收时,由于SWT机制,其他工作线程都会被暂时挂起,直到垃圾回收完成。这种垃圾收集器适用于Client模式的应用,在单CPU的环境下,由于没有和其他线程交互的开销,可以专心垃圾回收的工作,能够把单线程的优势发挥到极致,简单高效。通过-XX:+UseSerialGC可以开启这种回收模式。
在这里插入图片描述

5.2 ParNew收集器(年轻代--多线程)

ParNew收集器是Serial收集器的多线程版本,作用于年轻代,默认开启的收集线程数和cpu数量一样,运行数量可以通过修改ParallelGGThreads设定。
在这里插入图片描述

5.3 Parallel Scavenge收集器(年轻代--多线程--复制算法--关注吞吐量)

Parallel Scavenge收集器也被称为吞吐量优先收集器,作用于年轻代,多线程采用复制算法的垃圾收集器,跟ParNew收集器有些类似。和ParNew收集器不同的是,Parallel Scavenge收集器关注的是吞吐量,它提供了两个参数来控制吞吐量,分别是-XX:MaxGCPauseMillis(控制最大的垃圾收集停顿时间)、-XX:GCTimeRatio(直接设置吞吐量大小)。
如果设置了-XX:+UseAdaptiveSizePolicy参数,虚拟机就会根据系统的运行情况收集监控信息,动态调整新生代的大小,Eden,Survivor比例等,以尽可能地达到我们设定的最大垃圾收集时间或吞吐量大小这两个指标,这种调节方式称为GC的自适应调节策略。这也是Parallel Scavenge收集器和ParNew 收集器最大的区别。

5.4 Serial Old收集器(老年代--单线程)

Serial Old收集器是工作在老年代的单线程垃圾收集器,采用的算法是标记整理算法。在Client模式下可以和Serial收集器配合使用,如果在Server模式的应用,在JDK1.5之前可以和Parallel Scavenge收集器配合使用,另一种使用场景则是CMS垃圾收集器的后备预案,在发生Concurrent Mode Failure使用。

5.5 Parallel Old收集器(老年代--多线程)

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,多线程收集,采用标记整理算法。下图是Paraller Scavenge收集器和Parallel Old收集器配合工作的过程图。
在这里插入图片描述

5.6 CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,采用标记-清除算法。适用于希望系统停顿时间短,给用户更好的体验的场景。
CMS收集器运行时主要分为四个步骤:

  • 初始标记:标记GC Roots能直接关联的对象。存在Stop The World。
  • 并发标记:GC Roots Tracing,可以和用户线程并发执行。
  • 重新标记:标记期间产生的对象存活的在此判断,修正对这些对象的标记,执行时间相对并发标记短,存在Stop The World。
  • 并发清除:清除对象,可以金额用户线程并发执行。
    -
    CMS收集器的缺点在于:
    • 对CPU资源比较敏感
    • 无法处理浮动垃圾。可能出现[Concurrent Mode Failure] 而导致另一次Full GC的产生,由于在并发清理时用户线程还在运行,所以清理垃圾同时新的垃圾也不断产生,这部分垃圾(即浮动垃圾)只能在下一次GC时再清理掉。
    • 采用的是标记清除算法,所以会产生内存碎片。内存碎片会导致大对象无法分配到连续的内存空间,然后会产生Full GC,影响应用的性能。

5.7 G1收集器

G1垃圾回收器主要是面向服务端的垃圾回收器,年轻代和老年代都可以使用。运作时,整体上采用标记整理算法,局部上看是采用复制算法,两种算法都不会产生内存碎片,所以回收器在回收后能产生连续的内存空间。

它是专门针对以下场景设计的:

  • 像CMS手机器一样,能与应用程序线程并发执行。
  • 整理空闲空间更快。
  • 需要GC停顿时间更好预测。
  • 不希望牺牲大量的吞吐性能。
  • 不需要更大的Java Heap。
    G1垃圾回收器的内存分区不再采用传统的内存分区,将新生代,老年代的屋里空间划分取消了。
    在这里插入图片描述
    取而代之的是,把堆内存分为若干个Region(区域),每次收集的时候,只收集其中几个区域,以此来控制垃圾回收产生的STW。G1垃圾回收器和传统的垃圾回收器的最大区别就在于,弱化了分代概念,引入了分区的思想。
    在这里插入图片描述
    G1中每代的存储地址都不是连续的,而是使用了不连续的大小相同的Region。除此之外G1中还多了一个H,h代表Humongous,用于存储巨大对象(humongous object),当对象大小大于等于region一半的对象,就直接分配到了老年代,防止了反复拷贝移动。
    G1垃圾回收过程可分为四步:
  • 初始标记。收集所有GC根(对象的起源指针,根引用),STW,在年轻代完成。
  • 并发标记。标记存活对象。
  • 最终标记。是最后一个标记阶段,STW,很短,完成所有标记工作。
  • 筛选回收。回收没有存活对象的Region并加入可用Region队列。
    在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值