基本组成
1.8之前为程序计数器、堆、方法区(永久代)、栈,1.8后为程序计数器、堆、元空间(metaSpace)、栈。
程序计数器,用来记录当前线程执行到程序的哪一步,Java程序在jvm中以机器码执行,程序计数器保存了具体执行到哪一条指令。Java的多线程实现就是依赖程序计数器来记住当前线程执行到哪一步,然后通过轮转时间片并分配处理器来完成多线程并发。因此程序计数器是属于线程的私有部分。
其中,公共部分的堆内存和栈内存,堆内存分为年轻代(Eden)和老年代(Old),新生代内还划分为(Eden、From Survivor、To Survivor),from和to也叫s0,s1,其中的默认占比为8:1:1,可以通过-XX:SurvivorRatio命令更改。对象创建一般存放在年轻代,年轻代中会发生minorGc,老年代中会发生majorGc。由堆承担了存放存放新建对象这点来看,堆空间是对所有线程共享的。
栈空间,分为虚拟机栈和本地的方法栈。虚拟机栈是跟随线程生命周期的,当虚拟机执行Java程序的时候,每个方法都会创建一个栈帧,存放在虚拟机栈中,对应的调用会通过出栈入栈的方式调用。每个栈帧包括局部变量表、操作数栈、动态连接、方法出口等,对应方法中的变量就会存放到局部变量表中,其中如果是基本类型(byte,short,int,long,float,double,char,boolean)就存放对应值,如果是引用类型的对象,则存放引用地址。虚拟机栈中存放普通方法的相关信息,本地方法栈则存放的是**本地方法(nativeMethod)**的相关信息。
本地方法是什么?本地方法指的是使用本地程序语言编写的特殊的方法,在Java中通过native关键字修饰,使用JavaNativeInterface(JNI)技术来支持Java应用程序来调用本地方法,本地方法在本地语言中执行结束后把结果再返回到Java程序中。虽然本地JNI提供了调用方式,但是本地语言不安全,本地方法依赖于本地语言,更难调试等缺点,限制了本地方法在Java程序中的出现频率。
栈内存是跟随方法的,当一个方法被调用时就会创建栈帧,所以,栈内存也时跟随线程的,属于线程的私有部分。
方法区,用于存储Java程序中被虚拟机加载的类信息、常量、静态变量,此类信息时属于共享信息,所以方法区也是线程共享的。JDK1.8前,方法区也被称为永久代,因为在一次Java程序的生命周期中,这里的信息是不会被改变的,所以是被称为永久代的。在1.8前,这个空间的默认大小是64M(64位JVM默认是85M),而且这个空间是连续的。1.8后,方法区被元空间(MetaSpace)取代,元空间的大小不再是固定的,因为这个部分空间被移到了本地内存中,物理机的内存大小限制了元空间的大小,同时元空间在内存中也不再是连续的了。
一些参数
-
-Xms 设置堆最小空间
-
-Xmx 设置堆最大空间
-
-Xmn 设置年轻代空间
-
-XX:NewSize 设置新生代(Eden)最小空间
-
-XX:MaxNewSize 设置新生代(Eden)最大空间
-
-XX:PermSize 设置永久代最小空间
-
-XX:MaxPermSize 设置永久代最大空间
-
-Xss 设置线程堆栈大小
-
-XX:+UseParallelGC 选择垃圾收集器为并行收集器,仅对年轻代有效(PS:年轻代使用并行收集器,老年代仍使用串行收集器)
-
-XX:ParallelGCThreads=20 配置并行收集器的线程数(PS:线程数最好与处理器数目一致)
垃圾回收机制
Java的垃圾回收针对的是对象,所以垃圾回收的地方是堆,jvm的堆又分为年轻代,老年代。所以引入了垃圾回收分代收集理论。而垃圾回收时判断什么对象是需要回收,什么对象不需要回收,所以垃圾回收器判断时会使用算法方式和对象类型方式,算法一般为引用计数法和可达性分析,对象类型分为强引用,弱引用,软引用和虚引用。
回收算法
引用计数算法:当一个对象被创建并被别的对象引用时,在对象内部添加一个引用计数器,当被引用一次该计数器就会加一,当该计数器为0时,表示该对象是一个可被回收的对象,当垃圾回收器执行时就会对该对象进行清理。但是引用计数法在对象循环依赖时会出现问题,因为是循环依赖,所以这两个对象的引用计数器永不为0,所以垃圾回收器就不会回收。
可达性分析算法:可达性分析算法会在jvm内部定义一个GCRoots的根节点,并由该根节点将引用的对象添加到引用链上,所以会形成完整的引用链,当其中一个节点与引用链断开后,就认为断开的子链是不再被需要的,垃圾收集器可以回收。而GCRoot的选定,一般从以下几个中选择:
- 栈帧中的本地变量和参数:正在执行的方法中的本地变量和参数可以作为GC Root,因为它们是当前线程活动的一部分。
- 静态变量:类的静态变量通常存储在方法区中,它们随着类的加载而被创建,并且在整个程序运行期间一直存在,因此它们被视为GC Root。
- 虚拟机内部的特殊引用:例如常量引用、系统类加载器等。
- JNI引用:在Java代码中使用了JNI(Java Native Interface)时,如果在本地代码中使用全局引用或弱全局引用,它们也会作为GC Root。
引用类型
-
强引用:一般指的是new出来的对象,强引用的对象是不会被垃圾回收器回收的,就算内存不足,也不会回收,会直接报out of memory错误终止程序
当强引用对象不存在被使用的情况时,会通过弱化方式来使强引用对象被回收:
- 使强引用对象指向null,此时垃圾回收器会认为该对象不存再引用,比如arraylist的clear方法,会使数组中的对象都指向null。
- 对象超出了作用域,作用域体现在Java程序中被大括号抱住的代码块,当一个作用域结束后,认为对象不能再通过原有的引用被引用,所以再这个作用域外就是不需要该对象了。例如方法,当一个方法被执行完成后,Java虚拟机栈会执行出栈操作,该方法对应的栈帧被出栈后,作用域内的对象引用就失效了。即可被垃圾回收器回收。
-
软引用:使用SoftReference softReference = new SoftReference<>(new Object())方式创建。软引用会在内存不足时被回收。
-
弱引用:无论内存是否充足,弱引用一定会被回收。
-
虚引用:被虚引用的对象就像没有被引用一样,主要用来跟踪对象被垃圾回收的活动,可以在垃圾收集时收到一个系统通知。
分代收集理论
依据对象的生命周期,将垃圾回收按堆的分块分类,年轻代中的大部分对象都是临时对象,会在短时间后就不被使用了, 只有部分对象是被长期使用的,这部分对象会进入老年代。
垃圾回收算法
-
标记清理算法:将不需要回收的对象做标记,清理没有标记的对象
- 效率低,如果清理区域中有很多需要清理的对象,则会重复大量的标记、清理过程
- 空间碎片多,标记清理算法会导致很多空间不连续,而新分配大对象时可能会因为没有足够的连续空间从而触发新的垃圾收集行为
- 会暂停程序执行
-
标记复制算法:将内存一分为二,将对象区域需要保留的对象复制到空闲区域
- 步骤
- 分内存,一分为二,为对象区域和空闲区域
- 复制所有可达对象到空闲区域
- 清空对象区域,并将两个区域调换,空闲区域将成为下一个对象区域
- 高效分配,复制对象只需要更改指针
- 只有复制时暂停程序
- 内存利用率低,会有内存浪费
- 对象存活率高的程序中效率低
- 步骤
-
标记整理:从根对象开始,依次向一端移动对象,移动过程中清理无效对象
- 内存成块使用,解决碎片化
- 分配高效,新对象只需要移动指针,效率高
- 标记和整理阶段可以分别进行,有效限制暂停时间