JVM内存结构
- 1.
Class Files:字节码文件,由源码编译生成,通过类加载子系统(Class Loader SubSystem)加载到内存当中,生成供程序使用的类(Class)对象- 2.
Class Loader SubSystem:类加载子系统- 3.
Runtime Data Area:运行时数据区- 4.
Native Method Interface:本地方法接口- 5.
Native Method Libraries: 本地方法库- 6.
Execution Engine:执行引擎
1.作用
- 1.负责从文件系统或者网络中加载
字节码(class)文件,即将物理磁盘上的字节码文件加载到内存中,生成供程序使用的类对象- 2.
字节码(class)文件要求在文件开头有特定的文件标识(CA FE BA BE)- 3.
类加载器(ClassLoader)只负责class文件的加载,是否可运行,由执行引擎ExecutionEngine决定- 4.
类加载器是指特定的加载器,而类加载子系统是一个系统流程的统称- 5.加载生成的的类信息存放在一块称为
方法区的内存空间- 6.除了类的信息外,
方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量
2.角色
- 1.
class file存在于本地硬盘上,执行时通过类加载器加载到JVM中,生成一个类对象(Class),根据这个类对象可以实例化出多个一模一样的实例
- 2.
class file加载到JVM中,被称为DNA元数据模板,放在方法区- 3.
.class文件->JVM->元数据模板,该过程通过类加载器(Class Loader)实现- 4.物理磁盘上的
字节码文件通过二进制流的方式加载到内存中
3.类的加载过程
1.加载
- 1.通过一个类的
全限定名获取定义此类的二进制字节流
- 2.将这个字节流所代表的静态存储结构转化为
方法区的运行时数据结构- 3.
方法区:抽象概念;落地实现:1.7及以前叫永久代,之后叫元空间- 4.并在内存中生成一个代表这个类的
java.lang.Class对象,作为方法区该对象各种数据的访问入口
1.验证(Verify)
- 1.确保
class文件的字节流包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机的自身安全- 2.主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证
- 3.例:文件标识验证(
CA FE BA BE)
2.准备(Prepare)
- 1.为
类变量/静态变量分配内存空间并且设置该类变量的默认初始值
成员变量:定义在方法体和语句块之外,不属于任何一个方法,作用域是整个类
- 静态变量/类变量:用
static修饰的成员变量- 全局变量/实例变量:无
static修饰的成员变量局部变量:定义在方法或者方法代码块中的变量,作用域是其所在的代码块- 2.例如:
private static int a = 1,准备阶段会赋默认初始值为0,即a=0,然后在初始化(initial)阶段会赋值a = 1- 3.注意
- 1.这里不包含
final修饰的static类变量,因为final在编译的时候就会分配值,准备阶段会显示初始化- 2.因为被
final修饰是常量而不是变量,常量后期不会再被修改,所以在编译阶段就已经分配值- 3.这里不会为
实例变量默认初始化,因为当前还没创建对象,只是加载过程,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
3.解析(Resolve)
- 1.将
常量池内的符号引用转换为直接引用的过程- 2.事实上解析操作往往会在
JVM执行完初始化后再执行- 3.
符号引用:一组符号来描述所引用的目标,符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中- 4.
直接引用:直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄- 5.解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等
- 6.通过反编译可以查看
class文件中的符号引用和直接引用package com.java; public class HelloApp { private static int a = 1; public HelloApp() { } public static void main(String[] args) { System.out.println(a); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
如下如示:Constant pool是常量池,其中以#开头的是符号引用,其余的是直接引用 F:\文档\笔记\代码\JVMDemo\out\java>javap -v HelloApp.class Classfile /F:/文档/笔记/代码/JVMDemo/out/java/HelloApp.class Last modified 2022-11-9; size 608 bytes MD5 checksum 5964d34bba8f8bf4e817be8fe95a17fe Compiled from "HelloApp.java" public class com.java.HelloApp minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#23 // java/lang/Object."":()V #2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream; #3 = Fieldref #5.#26 // com/atguigu/java/HelloApp.a:I #4 = Methodref #27.#28 // java/io/PrintStream.println:(I)V #5 = Class #29 // com/atguigu/java/HelloApp #6 = Class #30 // java/lang/Object <=符号引用 #7 = Utf8 a <=直接引用 #8 = Utf8 I #9 = Utf8#10 = Utf8 ()V #11 = Utf8 Code #12 = Utf8 LineNumberTable #13 = Utf8 LocalVariableTable #14 = Utf8 this #15 = Utf8 Lcom/atguigu/java/HelloApp; #16 = Utf8 main #17 = Utf8 ([Ljava/lang/String;)V #18 = Utf8 args #19 = Utf8 [Ljava/lang/String; #20 = Utf8 #21 = Utf8 SourceFile #22 = Utf8 HelloApp.java #23 = NameAndType #9:#10 // " ":()V #24 = Class #31 // java/lang/System #25 = NameAndType #32:#33 // out:Ljava/io/PrintStream; #26 = NameAndType #7:#8 // a:I #27 = Class #34 // java/io/PrintStream #28 = NameAndType #35:#36 // println:(I)V #29 = Utf8 com/atguigu/java/HelloApp #30 = Utf8 java/lang/Object #31 = Utf8 java/lang/System #32 = Utf8 out #33 = Utf8 Ljava/io/PrintStream; #34 = Utf8 java/io/PrintStream #35 = Utf8 println #36 = Utf8 (I)V { public com.java.HelloApp(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/atguigu/java/HelloApp; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field >java/lang/System.out:Ljava/io/PrintStream; 3: getstatic #3 // Field a:I 6: invokevirtual #4 // Method >java/io/PrintStream.println:(I)V 9: return LineNumberTable: line 12: 0 line 13: 9 LocalVariableTable: Start Length Slot Name Signature 0 10 0 args [Ljava/lang/String; static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: iconst_1 1: putstatic #3 // Field a:I 4: return LineNumberTable: line 8: 0 } SourceFile: "HelloApp.java"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
3.初始化
- 1.初始化阶段就是执行
类构造器方法()的过程() - 2.此方法不需要定义,是
javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来- 3.类构造器方法中指定按语句在源文件中出现的顺序执行
- 4.
不同于类的构造器,类的构造器方法对应的是() init()方法- 5.若该类具有父类,
JVM会保证子类的执行前,父类的() 已经执行完毕() - 6.虚拟机必须保证一个类的
方法在多线程下被同步加锁(即保证类只会被加载一次,加载后的类对象保存在方法区)() - 7.
只有在类中有对静态变量,静态方法或静态代码块操作时才会有,其他情况不会存在() public class HelloApp2 { private static int num = 1; //prepare:num = 0 ---> initial : num = 1 ---> num = 2 static { num = 2; number = 20; } private static int number = 10; //prepare:number = 0 ---> initial : number = 20 ---> number = 10 public static void main(String[] args) { System.out.println(num); // num = 2 System.out.println(number); // number = 10 } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 8.注意
- 非法的前项引用,可以提前赋值,但不是不能提前引用
4.类加载器
- 1.JVM支持两种类型的类加载器:
- 1.引导类加载器(
Bootstrap ClassLoader)- 2.自定义类加载器(
User-Defined ClassLoader)- 2.一般来说
自定义类加载器指的是程序中由开发人员自定义的类加载器- 3.但是《
Java虚拟机规范》中将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器- 4.程序中最常见的三个类加载器
- 1.
Bootstrap Class Loader:引导类加载器- 2.
Extension Class Loader:扩展类加载器- 3.
System Class Loader:系统类加载器
- 5.以上四者之间(引导,扩展,系统,自定义)是包含关系,不是上层下层,也不是子父类的继承关系
- 6.引导类加载器通过C/C++语言编写,是获取不到的;扩展类加载器包含系统类加载器
public class ClassLoaderTest { public static void main(String[] args) { //获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //获取其上层:扩展类加载器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19 //获取其上层:获取不到引导类加载器 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader);//null //对于用户自定义类来说:默认使用系统类加载器进行加载 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1);//null ClassLoader classLoader2 = Integer.class.getClassLoader(); System.out.println(classLoader2);//null } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 7.通过下列代码可以动态获取到引导类,扩展类,系统类加载器负责加载的类,其中越底层能加载的类就越多
public class ClassLoaderTest1 { public static void main(String[] args) { System.out.println("**********引导类加载器**************"); //获取BootstrapClassLoader能够加载的api的路径 URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (URL element : urLs) { System.out.println(element.toExternalForm()); } //从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器 ClassLoader classLoader = Provider.class.getClassLoader(); System.out.println(classLoader); System.out.println("***********扩展类加载器*************"); String extDirs = System.getProperty("java.ext.dirs"); for (String path : extDirs.split(";")) { System.out.println(path); } //从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器 ClassLoader classLoader1 = CurveDB.class.getClassLoader(); System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d System.out.println("***********系统类加载器*************"); String appDirs = System.getProperty("java.class.path"); for (String path : appDirs.split(";")) { System.out.println(path); } //从上面的路径中随意选择一个类,来看看他的类加载器是什么:系统类加载器 ClassLoader classLoaderTest = ClassLoaderTest1.class.getClassLoader(); System.out.println(classLoaderTest);//sun.misc.Launcher$AppClassLoader@18b4aac2 } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
1.引导类加载器(Bootstrap ClassLoader)
- 1.该类加载器使用
C/C++语言实现,是JVM的一部分,通过Java代码是无法获取的- 2.该类加载器用来加载
Java的核心库,提供JVM自身需要的类
- 1.
JAVA_HOME/jre/lib目录下的rt.jar和resources.jar- 2.
sun.boot.class.path路径下的内容- 3.该类加载器并不继承自
java.lang.ClassLoader,没有父加载器- 4.该类加载器也用来加载扩展类和系统类加载器,并指定为他们的父类加载器
- 5.出于安全考虑,引导类加载器只加载包名为
java,javax,sun等开头的类
2.扩展类加载器(Extension ClassLoader)
- 1.Java语言编写,由
sun.misc.Launcher$ExtClassLoader实现(内部类),该加载器是JVM自带的- 2.
ExtClassLoader派生于ClassLoader抽象类- 3.该类加载器的父类加载器为引导类加载器
- 4.从
java.ext.dirs系统属性所指定的目录中加载类库或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库- 5.如果用户创建的
jar包放在此目录(jre/lib/ext)下,也会自动由扩展类加载类加载,主要用来加载核心包外的扩展目录下的jar包
3.系统类加载器(System Class Loader)
- 1.Java语言编写,由
sun.misc.Launcher$AppClassLoader实现(内部类),该加载器是JVM自带的- 2.
AppClassLoade派生于ClassLoader抽象类- 3.该类加载器的父类加载器为扩展类加载器
- 4.该类加载器负责加载环境变量
classpath或系统属性java.class.path指定路径下的类库- 5.该类加载器是程序中默认的类加载器,一般来说
Java应用的类都是由它来完成加载- 6.通过
ClassLoader.getSystemClassLoader()方法可以获取到该类加载器
4.用户自定义类加载器
- 1.
Java开发可以自定义类加载器,定制类的加载方式- 2.自定义类加载器的优势
- 1.隔离加载类(不同中间件的加载是隔离的,确保加载
jar包时相同名称的路径不会冲突)- 2.修改类加载的方式(修改为需要的时候动态的加载)
- 3.扩展加载源(本地磁盘,网络,扩展其他加载源)
- 4.防止源码泄露(自定义类加载器实现加密解密)
- 3.实现步骤
- 1.通过继承抽象类
java.class.ClassLoader的方式,实现自定义类加载器- 2.
JDK1.2之前,自定义类加载器会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类- 3.
JDK1.2之后,不建议覆盖loadClass()方法,建议把自定义的类加载逻辑写在findClass()方法中- 4.编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承
URLClassLoader类,这样可以避免去编写findClass()方法以及获取字节码流的方式,使自定义类加载器编写更加简洁package com.java; import java.io.FileNotFoundException; /** * 自定义用户类加载器 */ public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] result = getClassFromCustomPath(name); if(result == null){ throw new FileNotFoundException(); }else{ return defineClass(name,result,0,result.length); } } catch (FileNotFoundException e) { e.printStackTrace(); } throw new ClassNotFoundException(name); } private byte[] getClassFromCustomPath(String name){ //从自定义路径中加载指定类:细节略 //如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。 return null; } public static void main(String[] args) { CustomClassLoader customClassLoader = new CustomClassLoader(); try { Class<?> clazz = Class.forName("One",true,customClassLoader); Object obj = clazz.newInstance(); System.out.println(obj.getClass().getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
5.ClassLoader
- 1.
ClassLoader是一个抽象类,除引导类加载器其余的类加载器都继承自ClassLoaser- 2.
sun.misc.Launcher是JVM的入口应用,ExtClassLoader和AppClassLoader都是Launcher的内部类
1.获取ClassLoader的途径
6.双亲委派机制
- 1.
Java虚拟机对class文件采用的是按需加载的方式,当需要使用该类时才会将它的class文件加载到内存生成Class对象- 2.
Java虚拟机加载某个类的class文件时,采用的双亲委派机制,即把请求交由父类处理,它是一种任务委派模式
1.工作原理
- 1.如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 2.如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器
- 3.如果父类加载器可以完成类加载任务,则成功返回,如果父类加载器无法完成此加载任务,子类才会尝试自己去加载,这就是双亲委派机制
2.实例
- 1.创建一个测试类,调用
Java.lang.String类public class StringTest { public static void main(String[] args) { java.lang.String str = new java.lang.String(); System.out.println("hello"); StringTest test = new StringTest(); System.out.println(test.getClass().getClassLoader()); //自定义的类加载器一般是系统类加载器 System.out.println(str.getClass().getClassLoader()); //核心类库加载器是引导类加载器 } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 2.本地创建一个和
Java.lang.String同样包级的String类,如果该类被加载,则会输出静态代码块中的内容package java.lang; public class String { static{ System.out.println("我是自定义的String类的静态代码块"); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 3.测试结果发现自动调用的是核心类库中
String类而不是本地中的String类(只限测试使用)
- 4.可以防止恶意攻击导致项目崩溃
- 5.因为双亲委派机制,最后本地
String类的加载交给引导类加载器去加载核心类库中的String类,而该类不存在main方法,所以会报错
- 6.第三方的
jar包加载一般是通过线程上下文类加载器加载,默认为系统类型加载器
3.优势
- 1.避免类的重复加载
- 2.保护程序的安全,防止核心
API被随意篡改,防止自定义的类使JVM崩溃
- 例:自定义
类java.lang.String
4.沙箱安全机制
- 1.上述自定义
String类,加载的时候会优先使用引导类加载器加载- 2.
引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class)- 3.报错信息说没有
main方法就是因为加载的是rt.jar包中的String类,这样可以保证对java核心源代码的保护,这就是沙箱安全机制
5.判断两个Class对象是否为同一个类
- 1.
JVM中表示两个Class对象是否为同一个类存在两个必要条件
- 1.类的完整类名必须一致,包括包名
- 2.加载这个类的
classLoader(指:ClassLoader实例对象)必须相同- 2.
JVM中即使两个类对象(Class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个对象也是不相等的- 3.
JVM必须知道一个类型是由引导类加载器加载还是由自定义类加载器加载的- 4.如果一个类型是由自定义类加载器加载的,
JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中- 5.当解析一个类型到另一个类型的引用的时候,
JVM需要保证这两个类型的类加载器是相同的
6.类的主动使用和被动使用
- 1.
Java程序对类的使用方式分为:主动使用和被动使用
- 1.主动使用:分为七种情况
- 1.创建类的实例
- 2.访问某个类或接口的静态变量,或者对该静态变量赋值
- 3.调用类的静态方法
- 4.反射(Class.forName等)
- 5.初始化一个类的子类
- 6.
Java虚拟机启动被标明启动类的类- 7.
JDK7开始提供的动态语言支持:
- 1.
java.lang.invoke.MethodHandle实例的解析结果- 2.
REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化- 2.被动使用
- 1.除以上七种情况,其他使用
Java类的方式都被看作是对类的被动使用,都不会导致类的初始化
2.运行时数据区
注意:
- 1.每个
JVM只有一个Runtime(java.lang.Runtime)实例,相当于运行时数据区- 2.
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据结构,其中一些会随着虚拟机进程的启动而创建,随着虚拟机进程退出而销毁- 2.其他一些
运行时数据结构则与线程一一对应,会随着线程开始和结束而创建销毁- 3.如上图所示:
- 1.红色的
方法区和堆区:一个虚拟机进程只有一个,多个线程之间共享- 2.灰色的
程序计数器,虚拟机栈,本地方法栈:一个虚拟机进程有多个,每个线程单独一个- 4.补充说明:
- 1.
方法区1.8之前的落地实现是永久代,1.8之后是元空间,而元空间使用本地内存,空间比较大,出现溢出的概率较低- 2.
JVM优化重点在堆和方法区,垃圾回收一般95%在堆,5%在方法区- 3.
虚拟机栈一般只包含入栈和出栈操作,不存在垃圾回收,可能会出现溢出- 4.
程序计数器一般只是记录下一步操作对应的代码行,所以不存在溢出也不存在垃圾回收
1.JVM中的线程
- 1.线程是一个程序(进程)里的一个运行单元,JVM允许一个程序有多个线程并行执行
- 2.
Hostspot JVM中的每个线程都与操作系统的本地线程直接映射- 3.当一个Java线程准备好执行以后,此时一个操作系统的本地线程也会同时创建,Java线程执行终止后,本地线程也会回收
- 4.操作系统负责安排调度线程到任何一个可用的CPU上,一旦本地线程初始化成功,它就会调用
Java线程中的run()方法- 5.使用
jconsole或其他调试工具,能看到有许多线程在后台运行,其中不包括main线程以及main线程创建的线程- 6.
Hotspot JVM的后台系统线程主要包含以下:
- 1.虚拟机线程:
- 1.该线程的操作需要
JVM达到安全点才会出现,这些操作必须在不同的线程中发生的原因是它们都需要JVM达到安全点,这样堆才不会出现变化- 2.该线程的执行类型包括
stop-the-world的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销- 2.周期任务线程:该线程是时间周期事件的体现(中断),一般用于周期性操作的调度执行
- 3.GC线程:该线程对JVM里不同种类的垃圾收集行为提供了支持
- 4.编译线程:该线程在运行时会将字节码编译成本地机器指令
- 5.信号调度线程:该线程会先接收信号并发送给JVM,内部通过调用适当的方法进行处理
1.介绍
- 1.
JVM的程序计数寄存器(Program Counter Register)并非广义的物理寄存器,而是对物理寄存器的一种抽象模拟
2.作用
- 1.用来存储指向下一条指令的地址,由执行引擎读取下一条指令
- 2.它的内存空间很小,只存储下一条指定的地址,大小几乎可以忽略不记,是运行速度最快的存储区域
- 3.
JVM规范中,每个线程都有自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致- 4.任何时间一个线程都只有一个方法在执行,即当前方法,程序计数器会存储当前线程正在执行的
Java方法的JVM指令地址,如果是执行native方法(本地方法栈),则是未指定值(undefned)- 5.它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器完成
- 6.字节码解释器工作时就是通过改变该计数器的值来选取下一条需要执行的字节码指令
- 7.它是唯一一个在
JAVA虚拟机规范中没有规定任何OutotMemortError情况的区域- 8.
GC:垃圾回收,其中栈和程序计数器不会垃圾回收,只有方法区和堆会进行垃圾回收,因为栈只有出栈和入栈操作,不需要垃圾回收,程序计数器只是记录地址的,不会垃圾回收- 9.
OOM:内存溢出(OutotMemortError),只有程序计数器没有出现,其他数据区域都有可能出现
3.说明
4.问题
- 1.使用程序计数器存储字节码指令地址有什么用?
- 1.因为一般程序都是多线程执行的,
CPU需要不停的切换各个线程,切换回来以后,需要知道接着从哪个指令地址开始继续执行- 2.
JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令
- 2.程序计数器为什么被设定为线程私有?
- 1.多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换
- 2.为了准确地记录各个线程正在执行的当前字节码指令地址,最好的办法是为每一个线程都分配一个程序计数器,使各个线程之间可以独立计算,从而不会出现相互干扰覆盖的情况
- 3.由于
CPU时间片限制,多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令- 4.每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响
1.介绍
- 1.出于跨平台的设计,
Java的指令是根据栈来设计的(寄存器与CPU耦合度高)- 2.
栈的优点:跨平台,指令集小(8位),容易实现;- 3.
栈的缺点:性能下降,实现同样的功能需要更多的指令- 4.
栈是运行时的单位,堆是存储的单位- 5.
栈解决程序的运行问题,即程序如何执行;堆解决的是数据存储问题,即数据如何存放- 6.
Java虚拟机栈(Java Virtual Machine Stack):线程私有,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(Stack Frame),对应着一次次的Java方法调用
2.生命周期
- 虚拟机栈的生命周期和线程保持一致
3.作用
- 负责
Java程序的运行,保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回
4.优点
- 1.栈是一种快速有效的分配存储方法,访问速度仅次于程序计数器
- 2.
JVM直接对虚拟机栈的操作只有两个:每个方法执行,伴随着进栈,执行结束后出栈- 3.对于虚拟机栈来说不存在垃圾回收问题(存在
OOM,不存在GC)
5.栈的常见异常
- 1.《
Java虚拟机规范》允许虚拟机栈的大小是动态的或固定不变
- 1.如果采用固定大小的
Java虚拟机栈,则每个线程的Java虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverFlowError异常(栈溢出)
- 2.如果采用动态扩展的
Java虚拟机栈,并且在尝试扩展的时候无法申请到足够的内存,或者创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将会抛出一个OutofMemoryError异常(内存溢出)
6.设置栈内存大小
- 1.命令参考官方文档:文档地址链接
- 2.使用
-Xss设置线程堆栈大小,栈的大小直接决定了函数调用的最大可达深度-Xss size # 此选项类似于 -XX:ThreadStackSize # 单位 KB : k/K,MB : m/M,GB :g/G 默认值取决于平台 # Linux/x64(64 位):1024 KB # macOS(64 位):1024 KB # Oracle Solaris/x64(64 位):1024 KB # Windows: The default value depends on virtual memory # -Xss1m = -Xss1024k = -Xss1048576
- 1
- 2
- 3
- 4
- 5
- 6
- 7
7.栈的存储单位:栈帧
- 1.
虚拟机栈是线程私有的,每个线程都有独立的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在- 2.线程上正在执行的每个方法都各自对应各自的栈帧(一个方法一个栈帧)
- 3.栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
- 4.栈帧的内部结构
- 1.局部变量表(
Local Variables)- 2.操作数栈/表达式栈
(Operand Stack)- 3.动态链接/指向运行时常量池的方法引用(
Dynamic Linking)- 4.方法返回地址/方法正常退出或异常退出的定义(
Return Address)- 5.一些附加信息
1.局部变量表(Local Variables)
- 1.
局部变量表也称为局部变量数组或本地变量表- 2.本质是一个
数字数组(因为其他类型都会转换为int类型)- 3.主要用于存储方法参数(
形参)和定义在方法体内的局部变量,这些数据类型包括各种基本数据类型,对象引用(reference,实际堆空间的内存地址),以及returnAddress类型(返回值)- 4.由于局部变量表建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题(多线程操作安全问题)
- 5.局部变量表所需的容量大小是在
编译期确定下来的,并保存在方法的Code属性的maximum local variables(通过jclasslib查看class字节码文件)数据项中,方法运行期间是不会改变局部变量表的大小的
- 6.通过
javap -v LocalVariablesTest.class也可以查看解析后的信息
- 7.方法嵌套的次数由栈的大小决定,一般来说,栈越大,方法嵌套调用次数越多,对一个函数而言,它的参数和局部变量越多,使得
局部变量表膨胀,它的栈帧就越大,为满足方法调用所需传递信息增大的需求,进行函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少- 8.
局部变量表中的变量只在当前方法调用中有效,方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销魂
1.Solt
- 1.局部变量表索引
0开始存放变量值,到数组长度-1的索引结束(数组)- 2.局部变量表最基本的存储单元是
Slot(变量槽)- 3.
局部变量表存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型(返回值)的变量- 4.
局部变量表中32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
- 1.
byte,short,char在存储前被转换为int,boolean也被转换为int(0表示false,非0表示true)- 2.
long和double则占据两个slot- 5.
JVM会为局部变量表中的每一个solt都分配一个访问索引,通过这个索引可以访问到局部变量表中指定的局部变量值
- 6.当一个实例方法被调用的时候,它的
方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上- 7.如果访问
局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可,64bit的局部变量占两个solt,所以索引一般是两个- 8.如果当前栈帧是由
构造方法或者实例方法/非静态方法创建的,该对象的引用this将会存放在局部变量表下标为0的slot处,其余的参数按照参数表顺序继续排列
2.Slot重复利用
- 1.栈帧中的
局部变量表的槽位是可以重用的,如果局部变量表中的某个变量超过了其作用域,那么其作用域之后创建的新局部变量就会复用过期局部变量的槽位,从而节省资源
2.静态变量和局部变量对比
- 1.变量的分类
- 1.按照数据类型
- 1.
基本数据类型- 2.
引用数据类型- 2.按照类中声明的位置
- 1.
成员变量:使用前,都经过默认初始化赋值
- 1.
类变量/静态变量:linking的prepare阶段,类变量默认赋值,initial阶段显示赋值(包括静态代码块)- 2.
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并默认赋值- 2.
局部变量:使用前,必须要显示赋值,否则编译不通过- 注意
- 1.栈帧中与
性能调优关系最密切的部分是局部变量表- 2.
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
2.操作数栈(Operand Stack)
- 1.每一个独立的
栈帧中除了包含局部变量表,还包含一个后进先出(Last-In-First-Out)的操作数栈- 2.
操作数栈:方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈- 3.一些字节码指令将操作数压入栈,一些字节码指令将操作数取出栈,使用后再把结果压入栈(复制,交换,求和)
- 4.如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的
操作数栈中,并更新程序计数器中下一条需要执行的字节码指令
- 5.
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证- 6.
Java虚拟机的解释引擎是基于栈的执行引擎,栈指的是操作数栈,执行引擎会将字节码指令翻译成机器指令然后执行
- 7.
操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间- 8.
操作数栈是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧会随之被创建出来,此时该栈帧的操作数栈是空的(已创建,数组长度已确定)- 9.每一个
操作数栈都拥有一个明确的栈深度用于存储数据,其所需的最大深度在编译期就定义好了,保存在方法Code属性的max_stack中
- 10.操作数栈中的任何一个元素都可以是任意的
Java数据类型
- 1.
32bit的类型占用一个栈单位深度- 2.
64bit的类型占用两个栈单位深度- 11.
操作数栈并非采用访问索引的方式来访问数据,而是只能通过标准的入栈和出栈操作来访问数据
1.JVM层面解释++i和i++的区别
2.栈顶缓存(Top of Stack Cashing)技术
- 1.注意:只是提出,是否实现未知
- 2.基于栈式架构的虚拟机所使用的
零地址指令(没有地址)更加紧凑,但完成一项操作需要更多的入栈和出栈操作,即需要更多的指令分派次数和内存读/写次数- 3.由于
操作数是存储在内存中,因此频繁地执行内存读/写操作必然影响执行速度,为解决该问题,HotSpot JVM的设计者提出栈顶缓存(ToS,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器(操作更少,速度更快)中,降低对内存的读/写次数,提升执行引擎的执行效率
3.动态链接(Dynamic Linking)
- 1.
动态链接:指向运行时常量池(存在于方法区)的方法引用- 2.每一个
栈帧内部都包含一个指向运行时常量池(存在于方法区)中该栈帧所属方法的引用,用来支持当前方法的代码能够实现动态链接(Dynamic Linking)
- 3.
Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在字节码文件的常量池(存在于方法区),动态链接就是将这些符号引用转换为调用方法的直接引用
- 4.
常量池:存在于字节码文件中- 5.
运行时常量池:存在于运行时的方法区中,等同于常量池,只是范围不同- 6.常量池的作用:提供一些符号和常量,便于指令的识别,通过引用调用,减少空间占用
1.方法的调用
- 1.
JVM中将符号引用(#序号)转换为调用方法的直接引用与方法的绑定机制相关- 2.
链接类型
- 1.
静态链接
- 当字节码文件被加载到
JVM时,被调用的目标方法在编译期确定(即编译期符号引用就被转换为直接引用),且运行期间保持不变时;这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接(编译期确定)- 2.
动态链接
- 被调用的方法在编译期无法被确定下来,只能在程序运行期间将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也被称为
动态链接(运行期确定)- 3.
绑定:一个字段,方法或者类在符号引用被替换为直接引用的过程,该过程只发生一次(即确定后不能修改)- 4.绑定机制:早期绑定(
Early Binding),晚期绑定(Late Binding);
- 1.
早期绑定:对应静态链接
- 被调用的目标方法在编译期可知,且运行期保持不变,即可将这个方法与所属的类型进行绑定,由于明确了被调用的目标方法,因此可使用静态链接的方式将符号引用转换为直接引用
- 1.
晚期绑定:对应动态链接
- 被调用的方法在编译期无法确定,只能在运行期根据实际的类型绑定相关的方法,由于无法明确被调用的目标方法,因此只能用动态链接的方式将符号引用转换为直接引用
- 5.一般具备
多态特性的编程语言,都具备早期绑定和晚期绑定两种绑定方式- 6.
Java中任何一个普通的方法都具备虚函数(多态:父类可以调用子类的方法)的特征,使用final关键字可禁止方法具备虚函数的特征class Animal{ public void eat(){ System.out.println("动物进食"); } } interface Huntable{ void hunt(); } class Dog extends Animal implements Huntable{ @Override public void eat() { System.out.println("狗吃骨头"); } @Override public void hunt() { System.out.println("捕食耗子,多管闲事"); } } class Cat extends Animal implements Huntable{ public Cat(){ super();//表现为:早期绑定,直接调用父类的方法 } public Cat(String name){ this();//表现为:早期绑定,直接调用无参方法 } @Override public void eat() { super.eat();//表现为:早期绑定,直接调用父类的方法 System.out.println("猫吃鱼"); } @Override public void hunt() { System.out.println("捕食耗子,天经地义"); } } public class AnimalTest { public void showAnimal(Animal animal){ animal.eat();//表现为:晚期绑定,编译期无法直接确定传入的类型,多态,无法确定具体哪个子类 } public void showHunt(Huntable h){ h.hunt();//表现为:晚期绑定,编译期无法直接确定传入的类型,多态,无法确定具体哪个实现类 } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
2.虚方法和非虚方法
- 1.
非虚方法:方法在编译期可以确定具体的调用版本(符号引用转换为直接引用),且运行期不可变
- 1.
静态方法(无法重写)- 2.
私有方法(无法重写)- 3.
final方法(无法重写)- 4.
实例构造器(无法重写)- 5.
父类方法(super调用时可以确定具体调用的是哪个类的方法)- 2.
虚方法:方法在编译期不能确定具体的调用版本(符号引用转换为直接引用),只能在运行期动态确定
- 除以上5种外的其他方法
- 3.子类对象多态性的使用前提
- 1.类的继承关系(声明的是父类,实际调用的是子类对象)
- 2.方法的重写
- 4.虚拟机提供的方法调用指令
- 1.普通调用指令
- 1.
invokestatic:调用静态方法,解析阶段确定唯一方法版本- 2.
invokespecial:调用方法,私有方法及父类方法,解析阶段确定唯一方法版本- 3.
invokevirtual:调用所有虚方法- 4.
invokeinterface:调用接口方法- 2.动态调用指令
invokedynamic:动态解析出需要调用的方法,然后执行- 5.普通调用指令固化在虚拟机内部,方法的调用执行不可人为干预,而动态调用指令支持由用户确定方法版本(多态)
- 6.
invokestatic和invokespecial指令调用的方法为非虚方法,其余的(final修饰的除外)为虚方法class Father { public Father() { System.out.println("father的构造器"); } public static void showStatic(String str) { System.out.println("father " + str); } public final void showFinal() { System.out.println("father show final"); } public void showCommon() { System.out.println("father 普通方法"); } } public class Son extends Father { public Son() { //invokespecial super(); } public Son(int age) { //invokespecial this(); } //不是重写的父类的静态方法,因为静态方法不能被重写! public static void showStatic(String str) { System.out.println("son " + str); } private void showPrivate(String str) { System.out.println("son private" + str); } public void show() { //invokestatic showStatic("atguigu.com"); //invokestatic super.showStatic("good!"); //invokespecial showPrivate("hello!"); //invokespecial super.showCommon(); //invokevirtual showFinal();//因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。 //虚方法如下: //invokevirtual showCommon(); info(); MethodInterface in = null; //invokeinterface in.methodA(); } public void info(){ } public void display(Father f){ f.showCommon(); } public static void main(String[] args) { Son so = new Son(); so.show(); } } interface MethodInterface{ void methodA(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
3.静态语言和动态语言
- 1.静态语言:类型检查在编译期
- 2.动态语言:类型检查在运行期
- 3.静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息
java:String info = "test" JS: var name = "test";var name = 10
- 1
- 2
- 4.
Java7中增加了invokedynamic指令,该命令是为了支持动态类型语言而做的改进;Java本身是静态语言,但是从此开始支持动态类型- 5.
Java7中没有提供直接生成invokedynamic指令的方法,需借助ASM底层字节码工具产生invokedynamic指令- 6.
Java8中Lambda表达式的出现使得invokedynamic指令有了直接生成的表现方式(使用Lambda才会直接显示invokedynamic指令)@FunctionalInterface interface Func { public boolean func(String str); } public class Lambda { public void lambda(Func func) { return; } public static void main(String[] args) { Lambda lambda = new Lambda(); //编译期无法确定调用的具体是哪个类的方法 Func func = s -> { return true; }; //运行时根据使用类确定具体是哪个方法 lambda.lambda(func); lambda.lambda(s -> { return true; }); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
4.方法重写的本质及虚方法表
- 1.
方法重写的本质
- 1.找到操作数栈顶的第一个元素所执行的对象的实际类型,记作
C- 2.如果在类型
C中找到与常量中的描述符合的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常- 3.否则按照继承关系从下往上依次对
C的各个父类进行第2步的搜索和验证过程- 4.如果始终没有找到,则抛出
java.lang.AbstractMethodError异常- 2.
IllegalAccessError:非法访问错误,程序试图访问或修改一个属性或调用一个方法;但该属性或方法没有访问权限,因此会引起编译器异常;该错误如果发生在运行时,说明一个类发生了不兼容的改变虚方法表
- 1.
Java代码中会频繁地使用动态分派,如果每次动态分派的过程都重新在类的方法元数据中搜索合适的目标会影响执行效率,因此为了提高性能JVM在类的方法区建立一个虚方法表(virtual method table),并使用索引表来代替查找- 2.
非虚方法不会出现在表中,因为非虚方法的指向是确定的,无需再向上查找- 3.每个类中都有一个
虚方法表,表中存放着各个方法的实际入口- 4.
虚方法表的创建时机:虚方法表在类加载的链接阶段的解析时被创建并开始初始化,类变量初始化完成后,JVM会把该类的虚方法表也初始化完毕
4.方法返回地址(Return Address)
- 1.存放调用该方法的
程序计数器/PC寄存器的值- 2.一个方法的结束有两种方式:
- 1.正常执行完成
- 2.出现未处理的异常,非正常退出
- 3.无论哪种方式结束,方法退出后都返回到该方法被调用的位置,方法
正常退出时,调用者的程序计数器/PC寄存器的值作为返回地址,即调用该方法下一条指令的地址;而方法异常退出时,返回地址通过异常表确定,栈帧中不保存该信息- 4.本质上方法的退出就是
当前栈帧出栈的过程,此时需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置程序计数器/PC寄存器值等,让调用者方法继续执行- 5.
正常完成出口和异常完成出口的区别:通过异常完成出口退出时不会给上层调用者产生任何的返回值- 6.当一个方法开始执行后,只有两种方式可以退出这个方法
1.执行引擎遇到任意一个方法返回的字节码指令(
return),会有返回值传递给上层的方法调用者,简称正常完成出口
- 1.方法正常调用完成之后使用哪一个返回值指令需要根据方法返回值的实际
数据类型而定- 2.字节码指令中返回值指令包含
- 1.
ireturn:返回值是boolean,byte,char,short和int类型时使用- 2.
lreturn:返回值是long类型时使用- 3.
freturn:返回值是float类型时使用- 4.
dreturn:返回值是double类型时使用- 5.
areturn:返回值是引用类型时使用- 6.
return:声明为void方法,构造器,静态代码块使用public boolean methodBoolean() { return false; //ireturn } public byte methodByte() { return 0; //ireturn } public short methodShort() { return 0; //ireturn } public char methodChar() { return 'a'; //ireturn } public int methodInt() { return 0; //ireturn } public long methodLong() { return 0L; //lreturn } public float methodFloat() { return 0.0f;//freturn } public double methodDouble() { return 0.0;//dreturn } public String methodString() { return null;//areturn } public Date methodDate() { return null;//areturn } public void methodVoid() { //return } static { int i = 10; //return } //默认构造器方法 //return
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
2.方法执行过程中遇到异常(
Exception),且该异常没有在方法内进行处理;即只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口
- 1.方法执行过程中抛出异常时的
异常处理存储在一个异常处理表,方便发生异常的时候找到处理异常的代码
5.附加信息
- 1.该附加信息不一定存在
- 2.
栈帧中允许携带与Java虚拟机实现相关的一些附加信息(例:对程序调试提供支持的信息)
8.栈运行原理
- 1.
JVM直接对虚拟机栈的操作只有两个,即栈帧的压栈和出栈,遵循后进先出的原则- 2.当前栈帧(
Current Frame):当前正在执行的方法的栈帧(栈顶栈帧);当前方法(Current Method):与当前栈帧相对应的方法;当前类(Current Class):定义当前方法的类- 3.
执行引擎运行的所有字节码指令只针对当前栈帧进行操作- 4.如果当前方法中调用了其他方法,对应新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧
- 5.不同线程中所包含的
栈帧是不允许相互引用的,不可能在一个栈帧之中引用另外一个线程的栈帧- 6.如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会弹出当前栈帧,使得前一个栈帧重新成为当前栈帧
- 7.
Java方法有两种返回函数的方式,不管使用哪种方式,都会导致栈帧被弹出
- 1.正常的函数返回,使用
return指令(return可省略,不管加不加编译后都会存在return)- 2.抛出异常(未处理异常),异常会一层层向上抛出,如果没有处理则抛出异常,如果捕捉了异常则属于正常返回
9.栈的相关面试题
- 1.栈溢出的情况(
StackOverflowError)
- 栈固定大小,递归调用方法本身
- 2.调整栈大小,是否能保证不出现溢出
- 不能
- 3.分配的栈内存是否越大越好
- 不是,会减少其他数据区域的空间
- 4.垃圾回收是否会涉及到虚拟机栈
- 不会,因为只存在进栈和出栈两种操作
- 5.方法中定义的局部变量是否线程安全
- 不一定安全,内部产生并且内部消亡则是安全的,其他不是安全的
public class StringBuilderTest { int num = 10; //s1的声明方式是线程安全的,因为是s1在该方法类消亡,不存在多线程调用的情况 public static void method1(){ //StringBuilder:线程不安全 StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); //... } //sBuilder的操作过程:是线程不安全的,因为sBuilder可能被多个线程抢占 public static void method2(StringBuilder sBuilder){ sBuilder.append("a"); sBuilder.append("b"); //... } //s1的操作:是线程不安全的,s1会返回给其他线程使用 public static StringBuilder method3(){ StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); return s1; } //s1的操作:是线程安全的,toString本质是new一个新的字符串,所有s1本身是线程安全的 public static String method4(){ StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); return s1.toString(); } public static void main(String[] args) { StringBuilder s = new StringBuilder(); new Thread(() -> { s.append("a"); s.append("b"); }).start(); method2(s); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
4.本地方法栈
- 1.
Java虚拟机栈用于管理Java方法的调用,本地方法栈用于管理本地方法的调用- 2.本地方法栈是线程私有,本地方法是用C语言实现
- 3.本地方法栈的大小允许是
动态的或固定不变
- 1.如果线程请求分配的栈容量超过
本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverFlowError异常(栈溢出)- 2.如果采用动态扩展的
本地方法栈,并且在尝试扩展的时候无法申请到足够的内存,或者创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将会抛出一个OutofMemoryError异常(内存溢出)- 3.具体做法:
Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库
1.介绍
- 1.一个进程对应一个
JVM实例,一个JVM实例对应一个运行时数据区(Runtime 单例模式),一个运行时数据区只存在一个堆空间- 2.程序运行时,
JVM实例通过引导类加载器创建,此时堆空间也被创建,其空间大小确定,是JVM管理的最大内存空间- 3.堆空间的大小是可调节的(
HeapDemo和HeapDemo1的代码相同,堆空间大小设置不同,通过JDK/bin目录下的jvisualvm.exe查看具体JVM参数)
- 4.《
Java虚拟机规范》规定,堆空间可以是物理上不连续的内存空间,但逻辑上是连续- 5.所有的线程共享
Java堆空间,也可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB),即并不是所有的堆空间都是共享的- 6.《
Java虚拟机规范》规定:所有的对象实例以及数组都应当在运行时分配在堆上;但是随着JVM的发展,通过逃逸分析可以将部分未逃逸对象在栈上分配
- 7.方法执行结束后,
栈帧立马出虚拟机栈,但堆中的对象不会马上移除,仅在垃圾收集时才会被移除- 8.堆:
GC(Garbage Collection,垃圾收集)的重点区域
2.内存结构
- 1.现代垃圾收集器大部分都基于分代收集理论设计
1.
Java7及之前堆内存逻辑上分为三部分:新生代+老年代+永久代
- 1.
Young Generation Space:新生代Young/New(其又被细分为Eden和Survivor)- 2.
Tenure generation space:老年代Old/Tenure- 3.
Permanent Space:永久代 Perm
2.
Java8及之后堆内存逻辑上分为三部分:新生代+老年代+元空间
- 1.
Young Generation Space:新生代Young/New(其又被细分为Eden和Survivor)- 2.
Tenure generation space:老年代Old/Tenure- 3.
Meta Space:元空间Meta
- 2.约定:
新生区=新生代=年轻代;养老区=老年区=老年代;永久区=永久代- 3.堆空间
逻辑上是按上述划分,实际上堆空间只包含新生代和老年代,永久代和元空间则是方法区的落地实现(通过jdk/bin目录下的jvisualvm.exe可验证)
- 1.启动项目(注意:需要持续运行并设置堆空间大小)
- 2.点击
jdk/bin目录下的jvisualvm.exe
- 3.如上图所示,
新生代+老年代占用空间大小刚好等于10MB,主要参考括号内的后者值且其值会动态变化- 4.VisualVM下载地址 --> 点击跳转
- 4.堆空间内存占比也可以通过
JVM参数设置显示-Xms10m -Xmx10m -XX:+PrintGCDetails
- 1
- 5.
JDK7版本的堆空间内存占比
3.设置堆内存与OOM
- 1.
JVM堆区一般用于存储Java对象实例和数组,且堆的大小在JVM启动时已经设定完成,可以通过以下参数进行设置
- 1.
-Xms:表示堆区的初始内存大小(年轻代+老年代),等价于-XX:InitialHeapSize,注意:-X是JVM运行参数,ms是Memory start- 2.
-Xmx:表示堆区的最大内存,等价于-XX:MaxHeapSize- 2.默认情况下
- 1.初始内存大小:
物理电脑内存大小 / 64- 2.最大内存大小:
物理电脑内存大小 / 4
- 3.手动设置堆内存大小为
600MB时,实际使用内存只有575MB,因为为了方便复制,一般S0和S1区大小相等且只会使用其中的一个区域,因此实际大小575MB等于设置大小600MB减去一个S0或S1区大小25MB
- 4.一旦堆区中的内存大小超过
-Xmx所指定的最大内存时,将会抛出OutofMemoryError异常
- 5.注意
- 1.通常会将
-Xms和-Xmx参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新计算堆区的大小,从而提高性能- 2.实际存放对象的是
Eden和from(S0)或to(S1)其中的一个,而from(S0)区和to(S1)区大小一样,是为了方便垃圾回收时的复制操作
4.年轻代与老年代
- 1.存储在
JVM中的Java对象可以划分为两类
- 1.一类是生命周期比较短的瞬时对象,对象的创建和消亡都非常迅速
- 2.一类是生命周期非常长的长期对象,对象的创建和消亡与
JVM的生命周期保持一致- 2.
JVM堆区进一步细分可以分为
- 1.年轻代(
YoungGen)
- 1.
Eden- 2.
Survivor0(from区)- 3.
Survivor1(to区)- 2.老年代(
OldGen)
- 3.
堆空间逻辑上包含方法区,但是实际上并不包含方法区- 4.配置新生代和老年代在堆结构的占比(实际使用中一般使用默认值)
![]()
- 1.默认-XX:newRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
- 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
- 在HotSpot中,Eden空间和另外两个Survivor空间默认所占的比例是8:1:1
- 开发人员可以通过选项-XX:SurvivorRatio调整这个空间比例,比如-XX:SurvivorRatio=8
- 几乎所有的Java对象都是在Eden区被new出来的(如果对象太大放不进Eden区就被放入老年代)
- 绝大部分的Java对象的销毁都在新生代进行了
- IBM公司的专门研究表明,新生代中80%的对象都是朝生夕死的
- 可以使用选项-Xmn设置新生代最大内存大小
- 这个参数一般使用默认值就可以
- -XX:-UseAdaptivePolicy:关闭自适应的内存分配策略,+表示打开,-表示关闭(暂时用不着)
- -XX:SurvivorRatio:设置新生代中Eden区与Survivor区的比例,想要显示为8:1:1需要使用该设置显示设置
4.图解对象分配过程
- 为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片
- 1.new的对象先放伊甸园区,此区有大小限制
- 2.当伊甸园区的空间填满时,程序有需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC/YGC),将伊甸园区中不再被其他对象所引用的对象进行销毁,再加载新的对象放到伊甸园区,(伊甸园区满时会对伊甸园区进行YGC,幸存者区满时,不会触发GC,而是伊甸园区满的时候和伊甸园区一起GC)
- 3.然后将伊甸园中的剩余对象移动到幸存者0区
- 4.如果再次触发垃圾回收,此时上次幸存下来放到幸存者0区的,如果没有回收,就会放到幸存者1区
- 5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区
- 6.啥时候能去老年代,可以设置次数,默认是15次
- 可以设置参数:-XX:MaxTenuringThreshold=进行设置
- 每个对象都分配了一个年龄计数器,将对象从Eden放到S0区时,age为1
- 如果再次YGC,此时的幸存者是放在空的幸存者区域,当前空的区域是S1,所以将Eden和S0中的对象放入S1,此时S1放入的age为2,Eden放入的age为1
- 当幸存者区域中的对象age达到15将被放入到老年代,默认阈值是15,通过-XX:MaxTenuringThreshold=进行设置
- 总结:针对幸存者S0,S1的总结,复制之后有交换,谁空谁是to
- 关闭垃圾回收,频繁在新生代收集,很少在老年代收集,几乎不再永久代/元空间收集
1.对象分配的特殊情况
2.常用调优工具
- JDK命令行:jinfo,jstat,jsp,javap,jmap
- Eclipse:Memory Analyzer Tool
- Jconsole
- VisualVM
- Jprofiler
- Java Flight Recorder,JMC
- GCViewer
- GC Easy
5.Minor GC,Major GC,Full GC
- Eden区满时会进行YGC,等同于Minor GC
- 老年代区空间不足时会进行Major GC
- JVM在进行GC时,并非每次都对上面三个内存区域(新生代,老年代,方法区)一起回收的,大部分时候回收的都是指新生代
- 针对HotSport VM的实现,它里面的GC按照回收区域又分为两大种类型,一种是部分收集(Partial GC),一种整堆收集(Full GC)
- 部分收集:不是完整收集整个Java堆的垃圾收集,其中又分为
- 新生代收集(Minor GC/Young GC):只是新生代(Eden,S0,S1)的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
- 目前,只有CMS GC会有单独收集老年代的行为
- 注意:很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收
- 混合回收(Mixed GC):收集整个新生代以及部分老年代的垃圾收集,目前,只有G1 GC会有这种行为
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集
- 年轻代GC(Minor GC)触发机制
- 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC,(每次Minor GC会清理年轻代的内存)
- 因为Java对象大多具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快,这一定义既清晰又易于理解
- Minor GC会引发STW,暂停其他用户的线程,等待垃圾回收结束,用户线程才恢复运行
- 老年代GC(Major GC/Full GC)触发机制
- 指发生在老年代的GC,对象从老年代消失时,我们说Major GC或Full GC发生了
- 出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)
- 也就是在老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC
- Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
- 如果Major GC后,内存还不足,就报OOM
- Major GC的速度一般会比Minor GC慢10倍以上
- Full GC触发机制
- 触发Full GC执行的情况有如下五种
- 1.调用System.gc()时,系统建议执行Full GC,但是不必然执行
- 2.老年代空间不足
- 3.方法区空间不足
- 4.通过Minor GC后进入老年代的平均水平大于老年代的可用内存
- 5.由Eden区,Servivor0区向S1区复制时,对象大小大于To Speace可用内存,则把改对象转存到老年代,且老年代的可用内存小于该对象大小
- 说明:Full GC是开发或调优中尽量要避免地,这样暂停时间会短一些
6.堆空间分代思想
- 为什么需要把Java堆分代,不分代就不能正常工作吗?
- 经研究,不同对象的生命周期不同,70%~99%的对象是临时对象
- 新生代:有Eden,两块大小相同的Survivor(又称为form/to,S0/S1)构成,to总为空
- 老年代:存放新生代中经历多次GC仍然存活的对象
- 其实不分代完全可以,分代的唯一理由就是优化GC性能,如果没有分代,那所有的对象都在一块,GC的时候要找到哪些对象没用会对堆的所有区域进行扫描,而很多对象都是朝生夕死的,如果分代,把新创建的对象放到某一地方,当GC的时候先把这块存储朝生夕死对象的区域进行回收,这样就会腾出很大的空间
7.总结内存分配策略
- 如果对象在Eden出生并经过第一次MinorGC后仍然存活。并且能被Survivor容纳的话,就被移动到Survivor空间中,并将对象年龄设为1,有专门的年龄计数器,对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当他的年龄增加到一定程度(默认为15岁,其实每个JVM,每个GC都有所不同)时,就会被晋升到老年代中
- 对象晋升老年代的年龄阈值,可以通过选项-XX:MaxTenuringThreshold来设置
- 针对不同年龄段的对象分配原则如下:
- 1.优先分配到Eden
- 2.大对象直接分配到老年代
- 3.尽量避免程序中出现过多的大对象
- 3.长期存活的对象分配到老年代
- 4.动态对象年龄判断
- 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
- 空间分配担保
- -XX:handlePromotionFailure
8.为对象分配内存:TLAB
- 为什么有TLAB(Thread Local Allocation Buffer)
- 1.堆是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 2.由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 3.为了避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
- 什么是TLAB
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配一个私有缓存区域,它包含在Eden空间内
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略
- 据我所知所有的OpenJDK衍生出来的JVM都提供了TLAB的设计
- TLAB的说明
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选,默认开启
- 在程序中,开发人员可以通过-XX:UseTLAB设置是否开启TLAB实例
- 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然可以通过-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小
- 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存
9.小结堆空间的参数设置
- 管网说明:
- 1.-XX:+PrintFlagsInitial:查看所有参数的默认初始值
- 2.-XX:+PrintFlagFinal:查看所有参数的最终值(可能会存在修改,不再是初始值)
- 具体查看某个参数的指令:jps,查看当前运行中的进程,
- jinfo -flag SurvivorRatio 进程id
- 3.-Xms:初始堆空间内存(默认为物理内存的1/64)
- 4.-Xmx:最大堆空间内存(默认为物理内存的1/4)
- 5.-XMn:设置新生代的大小(初始值及最大值)
- 6.-XX:NewRatio:配置新生代与老年代在堆结构的占比
- 7.-XX:SurvivorRatio:设置新生代中的Eden和S0/S1空间的比例
- 8.-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
- 9.-XX:+PrintGCDetails:输出详细的GC处理日志
- 打印gc简要信息:
- 1.-XX:+PrintGC
- 2.-verbose:gc
- -XX:HandlePromotionFailure:是否设置空间分配担保
- 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
- 如果大于,则此次的Minor GC是安全的
- 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否担保失败
- 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
- 如果大于,则尝试进行一次Minor GC,但这次Monor GC依然是存在风险的
- 如果小于,则改进为一次Full GC
- 如果HandlePromotionFailure=false,则改为进行一次Full GC
- 在JDK7及以后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码变化,虽然源码中还定义了HandlePromtionFailure参数,但是在代码中已经不会再使用它,JDK7及之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC
10.堆是分配对象的唯一选择吗
- 在《深入理解Java虚拟机》中关于Java堆内存有这样的描述:随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么绝对了
- 在Java虚拟机中,对象是在Java堆中分配内存的,只是一个普遍的常识,但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样就无需在堆上分配内存,也无需进行垃圾回收了,这也是最常见的堆外存储技术
- 此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创建的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的
- 逃逸分析
- 如何将堆上的对象分配到栈,需要使用逃逸分析手段
- 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
- 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
- 逃逸分析的基本行为就是分析对象动态作用域
- 当一个方法在方法中被定义后,对象只在方法内部使用,这认为没有发生逃逸
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸,例如作为调用参数传递到其他地方中
- 没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除
- 在JDK7及之后,HotSpot找那个默认就开启了逃逸分析
- 如果使用的较早的版本,可以通过
- -XX:+DoEscapeAnalysis显示开启逃逸分析
- 通过选项-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果
- 结论:开发中能使用局部变量的,就不要再方法外定义
1.逃逸分析:代码优化
- 使用逃逸分析,编译器可以对代码做如下优化
- 1.栈上分配,将堆分配转化为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
- 2.同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
- 3.分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
- JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需进行垃圾回收了
- 常见的栈上分配的场景
- 在逃逸分析中,已经说明了,分别是给成员变量赋值,方法返回值,实例引用传递
- 代码优化之同步省略(消除)
- 线程同步的代价是相当高的,同步的后果是降低并发性和性能
- 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步,这样就能大大提高并发性和性能,这个取消同步的过程就叫同步省略,也叫锁消除
- 代码优化之标量替换
- 标量(Scalar):是指一个无法再分解成更小的数据的数据,Java中的原始数据类型就是标量
- 相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为它可以分解成其他聚合量和标量
*在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会吧这个对象拆解成若干个其中包含的成员变量来代替,这个过程就是标量替换
- 可以看到,这个Point这个聚合量经过逃逸分析后发现它并没有逃逸,就会被替换成两个聚合量,那么标量替换的好处:可以大大减少堆内存的占用,因为一旦不需要创建对象了,那么就不需要分配堆内存了
- 标量替换为栈上分配提供了很好的基础
- 标量替换参数设置:
- 参数-XX:+EliminateAllocations:开启标量替换,默认打开,允许将对象打散分配在栈上
- 总结
- -server -Xmx100m -Xms100m -XX:DoEscapeAnalysis -XX:+PrintGC -XX:EliminateAllocations
- 逃逸分析在服务端生效,默认启动的是服务端,通过java -version可以查看

1.栈、堆、方法区的交互关系
2.方法区的理解
- 1.《Java虚拟机规范》中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但是一些简单的实现可能不会选择区进行垃圾收集或者进行压缩,但对于HotSpotJVm而言
,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开- 所以,方法区看作是一块独立于Java堆的内存空间
- 方法区(Method Area)与Java堆一样,收集各个线程共享的内存区域
- 方法区在JVM启动的时候被创建,并且它的实际物理内存空间中的Java堆区一样都可以是不连续的
- 方法区的大小跟堆空间一样,可以选择固定大小或者可扩展
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutofMemoryError:PermGen space或者java.lang.OutofMemoryError:Metaspace
- 例如:加载大量的第三方的jar包,Tomcat部署的工程过多(30-50个),大量动态的生成反射类
- 关闭JVM就会释放这个区域的内存
3.设置方法区大小与OOM
4.方法区的内部结构
- 《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下,它用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等
- 类型信息
- 对每个类加载的类型(类class,接口interface,枚举enum,注解annotation),JVM必须在这个方法区中存储一下类型信息:
- 1.这个类型的完整有效名称(全名=包名.类名)
- 2.这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
- 3.这个类型的修饰符(public,abstract,final的某个子集)
- 4.这个类型直接接口的一个有序列表
- 域(Field)信息,属性信息
- 1.JVM必须在方法区中保存类型的所有域的相关信息以及域的生命顺序
- 2.域的相关信息包括:域名称,域类型,域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
- 方法(Method)信息
- JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序
- 1.方法名称
- 2.方法的返回类型
- 3.方法参数的数量和类型(按顺序)
- 4.方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
- 5.方法的字节码(bytecodes),操作数栈,局部变量表及大小(abstract和natvie方法除外)
- 异常表(abstract和native方法除外)
- 每个异常处理的开始外置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引
- non-final的类变量
- 1.静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分
- 2.类变量被类的所有实例共享,即使没有类实例时你也可以访问它
- 补充说明:全局常量:static final修饰
- 被声明为final的类变量的处理方式则不同,每个全局常量在编译的时候就会被分配了
运行时常量池 vs 常量池
- 方法区:内部包含了运行时常量池
- 字节码文件,内部包含了常量池
- 要弄清楚方法区,需要理解清除ClassFile,因为加载类的信息都在方法区
- 要弄清楚方法区的运行时常量池,需要理解清除ClassFile中的常量池
- 一个有效的字节码文件除了包含类的版本信息,字段,方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包括各种字面量和对类型,域和方法的符号引用
- 为什么需要常量池
- 1.一个Java源文件中的类,接口,编译后产生一个字节码文件,而Java中的字节码需要数据支持,通常这些数据会很大以至于不能直接存到字节码里,换另外一种方式,可以存到常量池,这个字节码包含了指向常量池的引用,在动态链接的时候会用到运行时常量池
- 小结
- 常量池,可以看做时一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型
- 运行时常量池
- 1.运行时常量池(Runtime Constant Pool)是方法区的一部分
- 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
- 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池
- JVM为每个已加载的类型(类或接口)都维护一个常量池,池中的数据项像数组项一样,是通过索引访问的
- 运行时常量池中包含多种不同的常量,报过编译器就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用,此时不再是常量池中的符号地址了,这里换成真实地址
- 运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性,String.intern()
- 运行时常量池类似传统的编译语言中的符号表(symbol table),但是它所包含的数据比符号表更加丰富一些
- 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutofMemoryError异常
5.方法区使用举例
6.方法区的演进细节
- 1.首先明确:只有HotSpot才有永久代,BEA JRockit,IBM,J9等来说,是不存在永久代的概率的,原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一
- Hotspot总方法区的变化
- jdk6及以前:有永久代(permanent generation),静态变量存放在永久代上
- jdk1.7:有永久代,但已经逐步去永久代,字符串常量池,静态变量移除,保存在堆中
- jdk8及之后:无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆
- 随着Java8的到来,HotSpot VM再也见不到永久代了,但是这并不意味着类的元数据信息也消失了,这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)
- 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间
- 这项改动是很有必要的,原因有:
- 1.为永久代设置空间大小是很难确定的,在某些场景下,如果动态加载类过多,容易产生Perm区的OOM,比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误
- 而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制
- 2.对永久代调优是很困难的
- StringTable为什么要调整
- jdk7中将StringTable放到了堆空间中,因为永久代的回收效率很低,在full gc的时候才会触发,而full gc是老年代的空间不足,永久代不足时才会触发。这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率很低,导致永久代内存不足,放到堆里,能及时回收内存
- staticObj随着Test的类型信息存放在方法区,instanceObj随着Test的对象实例存放在Java堆,localObject则是存放在foo()方法栈帧的局部变量表中
- 接着,找到了一个引用该staticObj对象的地方,是在一个java.lang.Class的实例里,并且给出了这个实例的地址,通过Inspector查看该对象实例,可以清楚的看到这确实是一个java.lang.Class类型的对象实例,里面有一个名为staticObj的实例字段
- 从《Java虚拟机规范》所定义的概念模型来看,所有Class相关的信息都应该存放在方法区中,但方法区该如何实现,《Java虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情
- JDK 7及其以后版本的HotSpot虚拟机选把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储于Java堆中,从我们的试验中也明确这一点
7.方法区的垃圾回收
- 有些人人为方法区《如HostSpot虚拟机中的元空间或者永久代》是没有垃圾收集行为的,其实不然,《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集,
- 一般来说这个区域的回收效果是比较难令人满意,尤其是类型的卸载,条件相对苛刻,但是这个部分区域的回收有时又确实是有必要的,以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄露
- 方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型
- 先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用
- 字面量比较接近Java语言层次的常量概念,如文本字符串,被声明为final的常量值等。而符号引用则属于编译原理方面的概率,包括下面三类常量
- 1.类和接口的权限定名
- 2.字段的名称和描述符
- 3.方法的名称和描述符
- HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收
- 回收废弃常量与回收Java堆中的对象非常类似
- 判定一个常量是否废弃还是比较相同简单,而要判定一个类型是否属于不再被使用的类的条件比较苛刻,需要同时满足下面三个条件
- 1.该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
- 2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如:OSGI,JSP的重加载等,否则通常是很难达成的
- 3.该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法
- Java虚拟机被允许堆满足上述三个条件的无用类进行回收
- 关于是否要对类型回收,HostSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading,-XX:TraceClassUnLoading查看类加载和卸载信息
- 在大量使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型防御的能力,以保证不会对方法区造成过大的内存压力
8.总结
对象实例化
对象的内存布局
对象的访问定位
- JVM是如何通过栈帧中的对象引用访问到其内存的对象实例?
- HotSpot采用第二种
直接内存
- 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
- 直接内存是Java堆外的,直接向系统申请的内存空间
- 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
- 通常,访问直接内存的速度会优于Java堆,即读写性能高
- 因此出于性能考虑,读写频繁地场合可能会考虑使用直接内存
- Java的NOI库允许Java程序使用直接内存,用于数据缓冲区
- 也可能导致OutOfMemoryError异常
- 由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存
- 缺点
- 分配回收成本高
- 不受JVM内存回收管理
- 直接内存大小可以通过MaxDirectMemorySize设置
- 如果不指定,默认与堆的最大值-Xmx参数值一致
执行引擎
1.概述
- 执行引擎是Java虚拟机核心的组成部分之一
- 虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器,缓存,指令集合操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式
- JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅是一些能够被JVM所识别的字节码指令,符号表,以及其他辅助信息
- 那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以
- 简单来说,JVM的执行引擎充当了将高级语言翻译成机器语言的翻译者
2.Java代码编译和执行过程
- 1.执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器
- 2.每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址
- 3.当然方法在执行过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆中的对象实例信息以及通过对象头中的元数据指针定位到目标对象的类型信息
- 从外观上看,所有的Java虚拟机的执行引擎输入,输出都是一致的,输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果
- 什么事解释器,什么是JIT编译器
- 解释器(Interpreter):当Java虚拟机启动时会根据预定义的规范堆字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行
- JIT编译器(Just In Time Compiler):就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
- 为什么说Java’是半编译半解释型语言
- JDK1.0时代,将Java语言定位为解释执行还是比较准确的,再后来,Java也发展出可以直接生成本地代码的编译器
- 现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行
3.机器码,指令,汇编语言
4.解释器
5.JIT编译器
3.本地方法接口
- 1.
Java中的Native方法就是本地方法接口,该方法接口由非Java代码实现,用关键字native修饰,无法与abstract连用- 2.定义一个
Native方法接口时,并不提供实现体,因为实现体是由非Java语言实现- 3.本地方法接口的作用是融合不同的编程语言为
Java所用- 4.Native方法存在的原因:
- 1.主要是有时
Java应用需要与Java外面的环境交互- 2.通过本地方法实现
jre与底层系统的交互- 3.
JVM的Sun解释器是C实现的
4.本地方法库
- 本地方法的类库,使用非
Java语言实现
5.执行引擎
##= GC
- GC的作用区域:方法区和堆
- 垃圾回收期可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收
- 其中:java堆是垃圾回收器的工作重点
- 从次数上讲
- 频繁收集Young区(新生代)
- 较少收集old区(老年代)
- 基本不动Perm区(方法区/永久代/元空间)
垃圾回收相关算法
- 标记阶段
- 清除阶段
标记阶段:引用计数算法
垃圾标记阶段:对象存活判断
- 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先要区分出内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段
- 在JVM中如何标记一个死亡对象?当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡
- 判断对象存活一般有两种方式:引用计数算法和可达性分析算法
- 引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况
- 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1,只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收
- 优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟性(随时可以回收)
- 缺点:它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
- 引用计数器有一个严重的问题,即无法处理循环引用的情况,这是一条致命缺陷,导致在JAVA的垃圾回收器中没有使用这类算法
- -XX:+PrintGCDetails查看垃圾回收的细节
- 证明:java使用的不是引用计数算法
- 小结:引用计数算法,是很多语言的资源回收选择,例如Python,他是同时支持引用计数和垃圾收集机制
- 具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试
- Java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系
- Python如何解决循环引用
- 手动解除:在合适的时机,解除引用关系
- 使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用
标记阶段:可达性分析算法
- 也叫根搜索算法,追踪性垃圾收集
- 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄露的发生
- 相对于引用计数算法,这里的可达性分析就是Java,C#选择的,这种类型的垃圾收集通常也叫作跟踪性垃圾收集(Tracing Garbage Collection)
- 所谓GC Roots根集合就是一组必须活跃的引用
- 基本思路
- 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
- 在Java语言中,GC Roots包括以下几类元素
- 虚拟机栈中引用的对象
- 比如:各个线程被调用的方法中使用到的参数,局部变量等
- 本地方法栈内JNI(通常说的本地方法)引用的对象
- 方法区中类静态属性引用的对象
- 比如:Java类的引用型静态变量
- 方法区中常量引用的对象
- 比如:字符串常量池(String Table)里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用
- 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException,OutOfMemoryError),系统类加载器
- 反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等
- 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时性地加入,共同构成完整的GC Roots集合,比如:分代收集和局部回收(Partial GC)
- 如果只针对Java堆中的某一块区域进行垃圾回收(例:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性
- 小技巧:
- 由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放堆内存里面,那它就是一个Root
- 注意:
- 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行,这点不满足的话分析结果的准确性就无法保证
- 这点也是导致GC进行时必须Stop the World的一个重要原因
- 即使是号称几乎不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的
对象的finalization机制
- Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
- 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收对象之前,总会先调用这个对象的finalize()方法
- finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放,通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件,套接字和数据连接等
- 永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,理由如下
- 1.在finalize()时可能会导致对象复活
- 2.finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下。若不发生GC,则finalize()方法将没有执行机会
- 3.一个糟糕的finalize()会引用影响GC的性能
- 从功能上来说,finalize()方法与C++中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize()方法在本质上不同于C++的析构函数
- 由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态
- 如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说。此对象需要被回收,但事实上,也并非是非死不可的,这时候他们暂时处于缓刑阶段,一个无法触及的对象有可能在某一个条件下复活自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中对象可能的三种状态
- 可触及的:从根节点开始,可以到达这个对象
- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活
- 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态,不可触及状态的对象不可能被复活,因为finalize()只会被调用一次
- 以上3种状态中,是由于finalize()方法的存在,进行的区分,只有在对象不可触及时才可以被回收
- 具体过程
- 判定一个对象ObjA是否可回收,至少要经历两次标记过程:
- 1.如果对象objA到GCRoots没有引用链,则进行第一次标记,
- 2.进行筛选,判断此对象是否有必要执行finalize()方法
- 1.如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为没有必要执行,objA被判定为不可触及的
- 2.如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的,低优先级的Finalizer线程触发其finalize()方法执行
- 3.finalize()方法是对象逃脱死亡的最后就会,稍后GC会对F-Queue队列中的对象进行第二次标记,如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被处于即将回收集合。之后,对象会再次出现没有引用存在的情况,在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的iinalize方法只会内调用一次
MAT与JProfiler的GC Roots溯源
- MAT是Memory Analyer的简称,它是一款功能强大的Java堆内存分析器,用于查找内存泄露以及查看内存消耗情况
- MAT是基于Eclipse开发的,是一块免费的性能分析工具
- 获取dump文件
- 方式1:命令行使用jmap
清除阶段:标记-清除算法
- 垃圾清除阶段
- 当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间对新对象分配内存
- 目前在JVM中比较常见的三种垃圾收集算法是标记-清除算法(Mark-Sweep),复制算法(Copying),标记-压缩算法(Mark-Compact)
- 背景:
- 标记-清除suanfa(Mark-Sweep)是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并应用于Lisp语言
- 执行过程:
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被成为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除- 标记:Collection从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的Header中记录为可达对象
- 清除:Collector对堆内存从头到尾进行线性遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
- 缺点:
- 效率不高
- 在进行GC的时候,需要停止整个应用程序,导致用户体验差
- 这种方式清理出来的空间内存是不连续的,产生内存碎片,需要维护一个空闲列表
- 注意,何为清除
- 这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空间的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放(覆盖)
清除阶段:复制算法
- 为了解决标记-清除算法在垃圾收集效率方面的缺陷
- 核心思想:将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
- 优点:没有标记和清除的过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现碎片问题
- 缺点:需要两倍的内存空间
- 对于G1这种分拆成为大量region(分区)的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系(指针位置),不管是内存占用或者时间开销也不小
- 特别:
- 如果系统中的垃圾对象很少,复制算法不会很理想,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行(否则就会造成额外的复制)
- 应用场景:
- 在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间,回收性价比很高,所以现在的商业虚拟机都是用这种收集算法回收新生代
清除阶段:标记-压缩算法
- 或者叫标记整理
- 背景:复制算法的高效性是建立在存活对象少,垃圾对象多的前提下的,这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象,如果依然使用复制算法,由于存活对象较多,复制的成本也将很高,因此,基于老年代垃圾回收的特性,需要使用其他的算法
- 标记-清除算法的确可以应用在老年代,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础上进行改进,标记-压缩(Mark-Compact)算法由此诞生
- 执行过程:
- 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放,之后清理边界外所有的空间
- 标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法
- 二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记压缩是移动式的,是否移动回收后的存活对象是项优缺点并存的风险决策
- 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉,如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销
- 指针碰撞(Bump the Pointer)
- 如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞
- 标记压缩的优点:
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可
- 消除了复制算法当中,内存减半的高额代价
- 缺点
- 从效率上来说,标记整理算法要低于复制算法
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 移动过程中,需要全程暂停用户应用程序,即:STW(Stop the world)
小结
- 效率上:复制算法最快,但是却浪费了太多内存
- 兼顾三个指标,标记整理算法相对来说更平滑一些,但是效率上不高,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段
分代收集算法
- 前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点,分代收集算法应运而生
- 分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的,因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率
- 在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象,线程,Socket连接,这类对象根业务直接挂钩,因此生命周期比较长,但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有效对象甚至只用回收一次即可回收
- 目前几乎所有的GC都是采用分代收集(Generational Collecting)算法执行垃圾回收的
- 在HostSpot中,基于分代的概念,GC所使用的内存回收算法必须节后年轻代和老年代各自的特点
- 年轻代(Young Gen)
- 年轻代特点:区域相对老年代较小,对象生命周期短,存活率低,回收频繁
- 这种情况复制算法的回收整理,速度是最快的,复制算法的效率只和当前存活对象大小有关,因此很合适年轻代的回收,而复制算法内存利用率不高的问题,通过HotSpot中的两个survivor的设计得到缓解
- 老年代(Tenured Gen)
- 老年代特点:区域较大,对象生命周期长,存活率高,回收不及年轻代频繁
- 这种情况存在大量存活率高的对象,复制算法明显变得不合适,一般是由标记-清除或者是标记-清除与标记整理的混合实现
- Mark阶段的开销与存活对象的数量成正比
- Sweep阶段的开销与所管理区域的大小成正相关
- Compact阶段的开销与存活对象的数据成正比
- 以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。
- 而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old 回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理
- 分代的思想被现有的虚拟机广泛使用,几乎所有的垃圾回收器都区分新生代和老年代
增量收集算法
- 上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop the world的状态,在stop the world状态下,应用程序所有的线程都会挂起,暂停一起正常的工作,等待垃圾回收的完成,如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性,为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生
- 基本思想
- 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成
- 总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记,清理或复制工作
- 缺点:使用这种方式,由于在垃圾回收过程中,间断性还执行了应用程序代码,所以能减少系统的停顿时间,但是因为线程切换和上下文切换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降
分区算法
- 一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长,为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿
- 分代算法将安装对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间
- 每一个小区间都独立使用,独立回收,这种算法的好处时可以控制一次回收多少个小区间
垃圾回收概念
System.gc()的理解
- 在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显示触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存
- 然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不一定立马执行)
- JVM实现者可以通过System.gc()调用来决定JVM的GC行为,而一般情况下,垃圾回收应该是自动进行的,无需手动触发,否则就太过麻烦了,在一些特殊情况下,如我们在编写一个性能基准(测试),我们可以在运行之间调用System.gc()
内存溢出
- 内存溢出(OOM)相对于内存泄露来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一
- Java doc中对OutOfMemortyError的解释是:没有空闲内存,并且垃圾收集器也无法提供更多内存
- 首先说没有空闲内存的情况,说明Java虚拟机的堆内存不够,原因有二:
- 1.Java虚拟机的堆内存设置不够:比如,可能存在内存泄露问题,也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显示指定JVM堆大小或者指定数值偏小,我们可以通过参数-Xms,-Xmx来调整
- 2.代码中创建了大量大对象,并且长时间不同被垃圾收集器收集(存在被引用)
- 对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如:常量池回收,卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError非常多见,尤其是在运行时存在大量动态类型生成的场合,类似intern字符串缓存占用太多空间,也会导致OOM问题,对应的异常信息,会标记出来和永久代相关:java.lang.OutOfMemoryError:PermGen space
- 随着元数据的引入,方法区内存已经不再那么窘迫,所以相应的OOM有所改观,出现OOM,异常信息则变成了:java.lang.OutOfMemoryError:Metaspace.直接内存不足,也会导致OOM
- 这里隐含着一层意思是,在抛出OutOfMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间
- 例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等
- 在java.nio.BIts.reserveMemory()方法中,我们能清除的看到,System.gc()会被调用,以清理空间
- 当然,也不是在任何情况下垃圾收集器都会被触发的
- 如果,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError
内存泄露(Memory Leak)
- 也称作:存储泄露。严格来说,只有对象不会再被程序用到了(自己的代码无法再被引用了),但是GC又不能回收他们的情况,才叫内存泄露
- 但实际情况很多时候一些不太好的实践会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的内存泄露
- 尽管内存泄露并不会立即引起程序崩溃,但是一旦发生内存泄露,程序中可用内存就会逐步蚕食,直至耗尽所有内存,最终出现OutOfMemory异常,导致程序崩溃
- 注意:这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小
- 举例
- 1.单例模式
- 单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄露的产生(例:runtime)
- 2.一些提供close的资源未关闭导致内存泄露
- 数据库连接(dataSource.getConnection()),网络连接(socket)和io连接必须手动close,否则是不能被回收的
Stop The World
- Stop the world简称STW,指的是GC时间发生过程中,会产生应用程序的停顿,停顿产生时整个应用程序线程都会被暂停,没有任何响应,优点像卡死的感觉,这个停顿称为STW
- 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿
- 分析工作必须在一个能确保一致性的快照中进行
- 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
- 如果出现分析过程中对象引用关系还不断变化,则分析结果的准确性无法保证
- 被STW中断的应用程序线程会在完成GC后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生
- STW事件和采用哪款GC无关,所有的GC都有这个事件
- 哪怕是G1也不能完全避免Stop-the-world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间
- STW是JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉
- 开发中不要用System.gc();会导致Stop-the-world的发生
程序的并行与并发
- 并发(Concurrent)
- 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行
- 并发不是真正意义上的同时进行,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行
- 并行(Parallel)
- 当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel)
- 其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU有多个核也可以并行
- 并发:指多个事情在同一时间段内同时发生
- 并行:指多个事情在同一时间点上同时发生了
- 并发的多个任务之间是互相抢占资源的
- 并行的多个任务之间是不互相抢占资源的
- 只有在多CPU或者一个CPU多核才会发生并行
垃圾回收的并行与并发
- 并行(Paralle):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态,如ParNew,Paralel Scavenge,Parallel Old
- 串行(Serial):相比较并行的概念,单线程执行
- 如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收,回收完,再启动程序的线程
- 并发(Concurrent)::指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能交替执行),垃圾首先线程在执行时不会停顿用户程序的运行
- 例如:CMS,G1
安全点与安全区域
- 安全点(Safepoint)::程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置成为安全点(Safepoint)
- Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题,大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间”为标准,比如:选择一些执行时间较长的指令作为Safe Point,如方法调用,循环跳转和异常跳转等
- 如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
- 抢先式中断(目前没有虚拟机采用)
- 首先中断所有线程,如果还有线程不再安全点,就恢复线程,让线程跑到安全点
- 主动式中断
- 设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起
- 安全区域(Safe Region)
- Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的SafePoint。但是程序不执行的时候呢,例如线程处于Sleep状态或Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Refion)来解决
- 安全区域是指在一段代码片段中,对象的引用的关系不会发生变化,,在这个区域中的任何位置开始GC都是安全的,我们也可以把Safe Region看做是被扩展了的Safe Point
- 实际执行时
- 1.当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程
- 2.当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止
JAVA中集中不同引用
- 我们希望能描述这一类对象,当内存空间还足够时,则能保留在内存中,如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这些对象
- JDK1.2版本之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱
- 除了强引用外,其他3种引用均可以在java.lang.ref包中找到他们的身影,如下图,显示了3种引用类型对应的类,开发人员可以在应用程序中直接使用它们
- Reference子类只有终结器引用时包内可见的,其他3种引用类型均为pulic,可以在应用程序中直接使用
- 强引用(StrongReference):最传统的引用的定义,是指在程序代码之中普遍存在的引用赋值,即类似Object obj = new Object()这种引用关系,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
- 软引用(SoftReference):在系统将要发生内存溢出之前,将会吧这些对象列入回收,范围之中进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常
- 弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象
- 虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
强引用(不回收)
- 在Java程序中,最常见的引用类型是强引用,也就是普通对象引用,即默认的引用类型
- 当在Java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用
- 强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象
- 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示地将相应强引用赋值为null,就是可以当做垃圾被收集了,当然具体的回收时机还是地看垃圾收集策略
- 相对的,软引用,弱引用和虚引用的对象是软可触及的,弱可触及的和虚可触及的,在一定条件下,都是可以被回收的,所以,强引用时造成Java内存泄露的主要原因之一
- 特点
- 强引用可以直接访问目标对象
- 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象
- 强引用可能导致内存泄露(其他引用不会导致)
软引用(内存不足即回收)
- 软引用时用来描述一些还有用,但非必须的对象,只被软引用关联着的对象,在系统将要发生内存溢出的异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存异常
- 软引用通常用来实现内存敏感的缓存,比如:高速缓存就有用到软引用,如果还有空闲内存,就可以暂时保存缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存
- 垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue)
- 类似弱引用,只不过Java虚拟机尽量让软引用的存活时间长一些,迫不得已才清理
- 总结:当内存足够,不会回收软引用的可达对象,当内存不够,会回收软引用对象
弱引用(一发现即回收)
- 弱引用也是用来描述哪些非必须对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止,在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象
- 但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象,在这种情况下,弱引用对象可以存在较长的时间
- 弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况
- 软引用,弱引用都非常适合来保存那些可有可无的缓存数据,如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出,而到内存资源充足时,这些缓存数据又可有存在相当长的时间,从而起到加速系统的作用
- Entry是weakReference类型的
虚引用(对象回收跟踪)
- 也称为幽林引用或者幻影引用,是所有引用类型中最弱的一个
- 一个对象是否有虚引用的存在,完全不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收
- 他不能单独使用,也无法通过虚引用来获取被引用的对象,当试图通过虚引用的get()方法取得对象时,总是null
- 为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收的过程,比如:能在这个对象被收集器回收时收到一个系统通知
- 虚引用必须和引用队列一起使用,虚引用在创建时必须提供一个引用队列作为参数,当垃圾回收准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况
- 在JDK1.2版之后,提供了PhantomReference类来实现虚引用
终结器引用
- FinalReference
- 它用以实现对象的finalize()方法,也可以成为终结器引用
- 无需手动编码,其内部配合引用队列使用
- 3.在GC时,终结器引用入队,由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象
垃圾回收器
GC分类与性能指标
- 按线程数分,可以分为串行垃圾回收器和并行垃圾回收器
- 串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束
- 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器,所以,串行回收默认被应用在客户端的client模式下的JVM中
- 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器
- 和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用Stop-the-world机制
- 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器
- 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少引用程序的停顿时间
- 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,知道垃圾回收过程完全结束
- 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器
- 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片(再分配对象空间使用:指针碰撞)
- 非压缩式的垃圾回收器不进行这步操作(再分配对象空间使用:空闲列表)
- 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器
评估GC的性能指标
- 吞吐量:运行用户代码的时间占总运行时间的比例
- (总运行时间=程序的运行时间+内存回收时间)
- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
- 收集频率:相对于应用程序的执行,收集操作发生的频率
- 内存占用:Java堆区所占的内存大小
- 快速:一个对象诞生到被回收所经历的时间
- 主要的是吞吐量和暂停时间
吞吐量与暂停时间
- 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
- 暂停时间是指一个时间段内应用程序线程暂停,让GC线程执行的状态
- 暂停时间优先,意味着尽可能让单次STW的时间最短
- 高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做生产性工作,直觉上,吞吐量越高程序运行越快
- 低暂停时间(低延迟)较好因为从最终用户的角度来看不管事GC还是其他原因导致一个应用被挂起始终是不好的
- 标准:在最大吞吐量优先的情况下,降低停顿时间
历史
7款经典收集器与垃圾分代之间的关系
- CMS并发收集,如果用户线程速度大于垃圾回收速度,可会会回收失败,需要使用Serial Old GC配合回收(后备方案)
- 两个收集器间有连线,表明他们可以搭配使用
- 其中Serial Old作为CMS出现Concurrent Mode Failure失败的后备预案
- (红色虚线)由于维护和兼容性测试的成本,在JDK 8 时将Serial+CMS,ParNew+Serial Old这两个组合声明为废弃,并在JDK 9中完全取消了这些组合的支持
- (绿色虚线),JDK 14中,弃用Parallel Scanvenge和SerialOld GC组合
- (青色虚线),JDK 14中,删除CMS垃圾回收器
- 为什么要有很多收集器:因为Java的使用场景很多,需要针对不同的场景提供不同的垃圾收集器,提高垃圾收集的性能
###如何查看默认的垃圾回收器
- -XX:+PrintCommandLineFlage:查看命令行相关参数(包含使用的垃圾收集器)
- 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID
Serial回收器:串行回收
- Serial收集器是最基本,历史最悠久的垃圾收集器了,JDK1.3之前回收新生代唯一的选择
- Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器
- Serial收集器采用复制算法,串行回收和Stoptheworld机制的方式执行内存回收
- 除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial Old收集器同样也采用了串行回收和Stop the World机制,只不过内存回收算法使用的是标记-压缩算法
- Serial Old是运行在Client模式下默认的老年代的垃圾回收器
- Serial Old在Server模式下主要有两个用途:1.与新生代Parallel Scanvenge配合使用2.作为老年代CMS收集器的后备垃圾收集方案
- 优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率
- 在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集,只要不频繁发送,使用串行回收器可以接收
- 在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年代和老年代都使用串行收集器
- 等价于新生代用Serial GC,且老年代用Serial Old GC
- 了解即可,限定单核CPU在使用,现在一般都不是单核
ParNew回收器:并行回收
- 如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本
- ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别,ParNew收集器在年轻代中同样也是采用复制算法,Stop-the-World机制
- ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器
- 由于ParNew收集器是基于并行回收的,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效
- ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU,多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量
- 但是在单个CPU的环境下,ParNew收集器不比Serial收集器更高效,虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销
- 因为除Serial外,目前只有ParNew GC能与CMS收集器配合工作
- 在程序中,开发人员可以通过选项-XX:+UseParNewGC 手动指定使用ParNew收集器执行内存回收任务,它表示年轻代使用并行收集器,不影响老年代
- -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数据相同的线程数
Parallel回收器:吞吐量优先
- HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法,并行回收和Stop the World机制
- 那么Parallel收集器的出现是否多此一举
- 和ParNew收集器不同,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器
- 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别
- 高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后天运算而不需要太多交互(低延迟)的任务,因此常见在服务器环境中使用,例如批量处理,订单处理,工资支付等
- Paralel收集器在JDK1.6时提供了用户执行老年代垃圾收集的Paralel Old收集器,用来代替老年代的Serial Old收集器
- Paralel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和Stop-the-World机制
- 在程序吞吐量优先的应用场景中,Parallel 收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很不错
- 在Java8中,默认是此垃圾收集器
- 参数配置
- -XX:+UseParallelGC 手动指定年轻代使用Paralel并行收集器执行内存回收任务
- -XX:+UseParalelOldGC 手动指定老年代都是使用并行回收收集器
- 分别适用于新生代和老年代,默认是jdk8是开启的
- 上面两个参数,默认开启一个,另一个也会被开启(互相激活)
- -XX::ParalerGCThreads 设置年轻代并行收集器的线程数。一般地最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能
- 在默认情况下,当CPU数量小于8个,ParallelGCThread的值等于CPU数量
- 当CPU数量大于8个,ParalelGCThreads的值等于3+{[5*CPU_count]/8}
- -XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间,单位是毫秒)
- 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数
- 对于用户来讲,停顿时间越短体验越好,但是在服务器端,我们注重高并发,整体的吞吐量,所以服务端适合Parallel,进行控制
- 该参数使用需谨慎
- -XX:GCTimeRatio 垃圾收集时间占总时间的比例(i/(N+1))。用于衡量吞吐量的大小
- 取值范围(0,100),默认值99,也就是垃圾回收时间不超过1%
- 与前一个-XX:MaxGCPauseMillis参数有一定矛盾性,暂停时间越长,Radio参数就越容易超过设定的比例
- -XX:+UseAdaptiveSizePolicy 设置Parallel Scanvenge收集器具有自适应调节策略
- 在这种模式下,年轻代的大小,Eden和Survivor的比列,晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小,吞吐量和停顿时间之间的平衡点
- 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆,目标的吞吐量(GCTimeRatioo)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作
- +表示使用,-表示不使用
CMS回收器:低延迟
- 在JDK1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作
- CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提高用户体验
- 目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS收集器就非常符合这类应用的需求
- CMS的垃圾收集算法采用标记-清除算法,并且也会Stop-the-world
- 不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作
- 所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个
- 在G1出现之前,CMS使用还是非常广泛的
- CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段,并发标记阶段,重新标记阶段和并发清除阶段
- 初始阶段(initial-Mark)阶段,在这个阶段中,程序中所有的工作线程都将会因为Stop-the-World机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联的对象,一旦标记完成之后就会恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里的速度非常快
- 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
- 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
- 并发清除(Concurrent-Sweep)阶段:此阶段清除删除掉标记阶段判断的已经死亡的对象,释放内存空间,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
- 尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行Stop-the-World机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要Stop-the-World,只是尽可能地缩短暂停时间
- 由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收时低停顿的
- 另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用,因CMS收集器不能像其他收集器那样等到老年代机会完全被填满再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行,要死CMS“运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure失败,这时虚拟机将启动后备预案,临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样就停顿时间就很长了
- CMS收集器的垃圾收集算法采用的是标记-清除算法,这意味着每次执行完内存回收后,由于被执行的内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将产生一些内存碎片,那么CMS在为新对象分配内存空间时,将无法使用指针碰撞技术,而只能够选择空闲列表执行内存分配
- 为什么不使用Mark-Compact算法:因为当并发清除时,用Compact整理内存的话,原来的用户线程使用的内存没法用,要保证用户线程能继续执行,前提的它运行的资源不受影响,Mark-Compact更适合STW场景使用
- CMS的优点
- 低延迟
- 并发收集
- 缺点
- 1.会产生内存碎片,导致并发清理收,用户线程可用的空间不足,在无法分配大对象的情况下,不得不提前触发Full GC
- 2.CMS收集器对CPU资源非常敏感,在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低
- 3.CMS收集器无法处理浮动垃圾,可能回车Concurrent Mode Failure失败而导致另一次Full GC的产生,在并发标记阶段由于程序的工作线程和垃圾收集线程是同步运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间
- 参数设置
- -XX:++UseConcMarkSweepGC 手动指定使用CMS收集器执行内存任务
- 开启该参数后会自动将-XX:+UseParNewGC打开,即ParNew(Young区用),CMS(Old区),Serial Old(备用)
- -XX:CMSlnitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到阈值,便开始进行回收
- JDK 5及以前版本的默认值是68,即当老年代的空间使用率达到68%时,会执行一次CMS回收,JDK 6及以上版本默认值为92%
- 如果内存增长缓存,可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代的回收的次数可以较为明细地改善应用程序性能,反之,如果应用程序内存使用率增长的很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器,因此通过该选项可以有效降低Full GC的执行次数
- -XX:+UseCMSCompactAtFullCollection 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生,不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了
- -XX:CMSFullGCsBeforeCompaction 设置在执行多少次FullGC后对内存空间进行压缩整理
- -XX:ParallelCMSThreads 设置CMS的线程数量
- CMS 默认启动的线程数是(ParallelGCThreads + 3)/4
- ParallelGCThreads是年轻代并行收集器的线程数,当CPU资源比较紧张时,收到CMS收集器线程的影响,应用程序的性能垃圾回收阶段可能会非常糟糕
总结
- 想要最小化使用内存和并行开销:Serial GC
- 想要最大化应用程序的吞吐量:Paralel GC
- 最小化GC的中断或挺短时间:CMS GC
- JDK 9时CMS被标记为Deprecate
- 如果JDK9及以上版本虚拟机使用参数 -XX:++UseConcMarkSweepGC来开启CMS收集器时,会受到警告信息
- JDK14删除了CMS垃圾收集器
- 如果在JDK 14中使用-XX:++UseConcMarkSweepGC来开启CMS收集器,JVM不会报错,只是给出一个warning信息,但是不会exit,JVM会自动回退以默认GC方式启动JVM
G1回收器:区域化分代式
- Garbage First(G1):延迟可控的情况下获得尽可能高的吞吐量
- G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的),使用不同的Region来表示Eden,幸存者0区,幸存者1区,老年代等
- G1 GC有计划的避免在整个Java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值的大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据运行的收集时间,优先回收价值最大的Region
- 由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)
- G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征
- 在JDK1.7版本正式启用,移除了Experimental的标识,是JDK 9以后默认垃圾回收器,取代了CMS回收器以及Parallel+Parallel Old组合。被Oracle官方称为全功能的垃圾收集器
- 与此同时,CMS已经在JDK9 中被标记为废弃(deprecated),在JDK 8中还不是默认的垃圾回收器,需要使用-XX:+UseG1GC来启用
G1垃圾回收器的优势与不足
- 与其他GC收集器相比,G1使用了全新的分区算法,其特点如下所示
- 并行与并发
- 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力,此时用户线程STW
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
- 分代收集
- 从分代上看,G1依然属于分代垃圾回收器,它会分区年轻代和老年代,年轻代依然有Eden区和Survivor区,但从堆的结构上看,它不要求整个Eden区,年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量
- 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代
- 和之前的各类回收器不同,它同时兼顾年轻代和老年代,对比其他回收器,或者工作在年轻代或者工作在老年代
- 空间整合
- CMS:标记-清除算法,内存碎片。若干次GC后进行一次碎片整理
- G1将内存划分为一个个的region,内存的回收时以region作为基本单位的。Region之间是复制算法,但整体上实际可以看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC,尤其是当Java堆非常大的时候,G1的优势更加明细
- 可预测的停顿时间模型(即:软实时soft real-time)
- 这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制
- G1跟踪各个Region里面的垃圾堆积的价值的大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,保证了G1收集器在有限2的时间内可以获取尽可能高的收集效率
- 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多
- 缺点
- 相比较于CMS,G1还不具备全方位,压倒性优势,比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(OverLoad)都要比CMS要高
- 从经验上来说,在小内存应用上CMS的表现大概率会由于G1,而G1在大内存应用上则发挥其优势,平衡点在6-8GB之间
G1的参数设置
- -XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务
- -XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000
- -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200ms
- -XX:ParallelGCThread 设置STW时GC线程数的值。最多设置为8
- -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParalelGCThreads)的1/4左右
- -XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC,默认值是45
- G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优
- 第一步:开启G1垃圾收集器
- 第二步:设置堆的最大内存
- 第三步:设置最大的停顿时间
- G1中提供了三种垃圾回收模式:YoungGC,Mixed GC 和Full GC,在不同的条件被触发
G1回收器的适用场景
- 面向服务端的应用,针对具有大内存,多处理器的机器(在普通大小的堆里表现并不突出)
- 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案
- 如:在堆大小约6GB或更大时,可预测的暂停时间可低于0.5秒(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿的时间不会过长)
- 用来替换掉JDK1.5中的CMS收集器
- 以下情况,使用G1可能比CMS好
- 1.超过50%的Java堆被活动数据占用
- 2.对象分配频率或年代提示频率变化很大
- 3.GC停顿时间过长(长于0.5至1秒)
- HotSpot垃圾收集器里。除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台的GC工作,即当JVM的GC线程处理速度慢,系统会调用应用程序线程帮助加速垃圾回收过程。
region的适用介绍
- 分区Region:化整为零
- 使用G1收集器时,它将整个Java堆划分为约2048个大小相同的独立Region块,每个Region块根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB,可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变
- 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合,通过Region的动态分配方式实现逻辑上的连续
- 一个Region有可能属于Eden,Survivor或者Old/Tenured 内存区域。但是一个region只可能属于一个角色。图中的E表示该region属于Eden内存区域,s表示属于Survivor内存区域,o表示属于Old内存区域,图中空白表示未使用的内存空间
- G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块,主要用于存储大对象,如果超过1.5个region,就放到H
- 设置H的原因:
- 对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响,为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储,为了能找到连续的H区,有时候不得不启动Full GC,G1的大多数行为都把H区作为老年代的一部分来看待
G1垃圾回收器的主要回收环节
- G1 GC的垃圾回收过程主要包括如下三个环节
- 年轻代(Young GC)
- 老年代并发标记过程(Concurrent Marking)
- 混合回收(Mixed GC)
- (如果需要,单线程,独占式,高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收)
- 顺时针,young gc -> young gc + concurrent mark ->Mixed GC顺序,进行垃圾回收
- 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程:G1年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Surivivor区间或者老年区间,也有可能是两个区间都会涉及
- 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程
- 标记完成马上开始混合回收过程,对于一个混合回收期,G1 GC从老年代区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收期不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的
- 举个例子:一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存,G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收
记忆集与写屏障
- Remembered Set 记忆集
- 一个对象被不同区域引用的问题
- 一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保存准确
- 在其其得分代收集器,也存在这样的问题(G1更突出)
- 回收新生代也不得不同时扫描老年代?
- 这样的话会降低Minor GC的效率
- 解决方法
- 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描
- 每个Region都有一个对应的Remembered Set
- 每次Reference类型数据写操作时,都会产生一个Write Brrier暂时中断操作
- 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象)
- 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中
- 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏
- G1比其他垃圾回收器多占用10%到20%的空间用于记忆集
G1垃圾回收过程的详细说明
G1垃圾回收的优化建议
垃圾回收器总结
GC日志分析
垃圾回收器的新发展