JVM(HotSpot1.8)总结
2022-04-06 11:18:21 9 举报
AI智能生成
jvm 1.8总结
作者其他创作
大纲/内容
内存分配
常量池(常量池主要存放两大类常量:字面量和符号引用。)
Class文件常量池
javap -v(查看编译后的字节码文件)
运行时常量池(元空间)
全局共享
运行时常量池的作用是存储 Java class文件常量池中的符号信息。运行时常量池 中保存着一些 class 文件中描述的符号引用,同时在类加载的“解析阶段”还会将这些符号引用所翻译出来的直接引用(直接指向实例对象的指针)存储在 运行时常量池 中
具有动态性
java 规范并不要求常量只能在运行时才产生,也就是说运行时常量池中的内容并不全部来自 class 常量池,class 常量池并非运行时常量池的唯一数据输入口;在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的较多的是String.intern()
全局字符串常量池(堆内存)
专门针对String类型设计的常量池
JVM之所以单独设计字符串常量池,是JVM为了提高性能以及减少内存开销的一些优化:
String对象作为Java语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。
创建字符串常量时,首先检查字符串常量池是否存在该字符串,如果有,则直接返回该引用实例,不存在,则实例化该字符串放入常量池中。
String问题探索!!!
字符串常量池的容量到底有多大
常量池本质上是一个hash表(详细见备注),这个hash表示不可动态扩容的。也就意味着极有可能出现单个 bucket 中的链表很长,导致性能降低。
在JDK1.8中,这个hash表的固定Bucket数量是60013个,可以通过下面这个参数配置指定数量-XX:StringTableSize=N
在JDK1.8中,这个hash表的固定Bucket数量是60013个,可以通过下面这个参数配置指定数量-XX:StringTableSize=N
可以增加下面这个虚拟机参数,来打印常量池的数据。
-XX:+PrintStringTableStatistics
为什么要设计针对字符串单独设计一个常量池?
- String这个类是被final修饰的,代表该类无法被继承。
- String这个类的成员属性value[]也是被final修饰,代表该成员属性不可被修改。
(也是String为什么是final不可变的原因)因此String具有不可变的特性,也就是说String一旦被创建,就无法更改
- 方便实现字符串常量池:在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!
- 线程安全性,在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
- 保证 hash 属性值不会频繁变更。确保了唯一性,使得类似HashMap容器才能实现相应的key-value缓存功能,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
总结一下:提高性能和减少内存开销
字面量是什么时候进入到字符串常量池的(String.intern())
详见Mic文章(https://mp.weixin.qq.com/s/lw3T5b6Oc70aX0N9RhQ98w)
基本类型包装类对象常量池(堆内存)
Java的基本类型的封装类大部分也都实现了常量池。包括Byte,Short,Integer,Long,Character,Boolean
注意,浮点数据类型Float,Double是没有常量池的
范围
Byte,Short,Integer,Long : [-128~127]
Character : [0~127]
Boolean : [True, False]
意味着这个区间内的数据,都采用同样的数据对象。通过==判断得到的结果为true。
运行时数据区
堆(线程共享)
所有线程共享,几乎所有对象和数组都被分配到了堆内存中
堆内存分配(基于垃圾收集器分代理论)
新生代(young区)
eden
一般情况新创建的对象会放到eden区,如果大对象(比如大于eden区),会直接放到old区,针对某些垃圾收集器可以设置参数,如果对象大于多少兆直接放到old区
Survivor(s0,s1)
为了解决内存的不连续性,younggc之后会将少数存活的对象放到s0,下次会将eden存活对象和s0区的对象放到s1(两个s区相互切换放,反之一样)
为了存活对象长时间占用s区(满了不动),s区会将相同分带年龄内存大小的总和大于s0区的一半,那么大于等于这个年龄的对象就直接进入老年代,这也是为什么老年代会有年龄较小对象的原因
Survivor的预筛选保 证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
老年代(old区)
方法区(线程共享)
方法区是一个逻辑分区,包含元空间、运行时常量池、字符串常量池
元空间
直接内存
运行时常量池
字符串常量池
方法区主要是用来存放已被虚拟机加载的类型信息、常量、静态变量等数据
虚拟机栈(线程私有)
以栈帧为单位的压栈或出栈
每个方法在执行的同时都会创建一个栈帧,每个方法从调用开始到结束,就对应着一个栈帧在线程栈中压栈和出栈的过程
栈帧构成
局部变量表
数组结构,主要存放对应方法的参数和局部变量
如果是实例方法,局部变量表第一个参数是一个 reference 引用类型,存放的是当前对象本身 this。
局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使 用。
操作数栈
操作数栈也是一个数组结构,但并不是通过索引来访问的,而是栈的压栈和出栈操作。
动态链接
每个栈帧内部都包含一个指向当前方法所在类型的运行时常量池的引用,以便对当前方法的代码实现动态链接。
在class文件里面,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用来表示,动态链接的作用就是将这些以符号引用所表示的方法转换为对实际方法的直接引用。
方法返回值
方法执行后,有两种方式退出该方法:正常调用完成,执行返回指令。异常调用完成,遇到未捕获异常,不会有方法返回值给调用者。
本地方法栈(线程私有)
程序计数器(线程共享)
程序计数器来记录某个线程的字节码执行位置
举例子比如多线程引起的CPU时间片切换,线程挂起重新获得cpu执行权限时,需要知道上次执行到什么位置
执行java方法时有值,执行native方法时为undefined
GC
YoungGC
eden区满时,只清理young区
OldGC
old区满时,只清理old区
FullGC
发生Young GC之前进行检查,如果“老年代可用的连续内存空间” < “新生代历次Young GC后升入老年代的对象总和的平均大小”,说明本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,此时会触发FullGC
当老年代没有足够空间存放对象时,会触发一次FullGC
如果元空间区域的内存达到了所设定的阈值-XX:MetaspaceSize=,也会触发FullGC。
怎么判断对象是否可以被回收?
引用计数法(摒弃)
有一次引用计数就加1
回收引用为0的对象
无法解决对象之间循环引用,可能会引起内存泄漏,最终会引起内存溢出
可达性分析算法
GC Root链可达的对象是存活对象
对象的引用
如果 reference (引用)类型的数据中存储的数值代表的是另外一块内存
的起始地址,就称这块内存代表着一个引用。 这种定义很纯粹,但是太过狭隘,一个对象在这种定义下
只有被引用或者没有被引用两种状态,对于如何描述一些处于判刑中又或者我们想扔又舍不得的对象就
显得无能为力。
强引用
把一个对象赋给一个引用变量,这个引用变量就是一个强引 用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能
被垃圾回收机制回收的,即 使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java
内存泄漏的主要原因之 一。
被垃圾回收机制回收的,即 使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java
内存泄漏的主要原因之 一。
软引用
SoftReference类实现,在内存不足时才会回收
弱引用
WeakReference 类来实现,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主
要作用是跟踪对象被垃圾回收的状态。
要作用是跟踪对象被垃圾回收的状态。
对象的生命周期
创建阶段
- 为对象分配存储空间 开始构造对象 从超类到子类对static成员进行初始化 超类成员变量按顺序初始化,递归调用超类的构造方法 子类成员变量按顺序初始化,子类构造方法调用 一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段
应用阶段
对象至少被一个强引用持有着。
不可见阶段( Invisible )
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存
在着的。
简单说就是程序的执行已经超出了该对象的作用域了。
不可达阶段( Unreachable )
对象处于不可达阶段是指该对象不再被任何强引用所持有。
收集阶段( Collected )
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好
准备时,则对象进入了“收集阶段”。如果该对象已经重写了 finalize() 方法,则会去执行该方法的终
端操作。
finalize()方法
会先去检查是否有必要调用finalize()方法,如果重写了或者没被jvm调用过就有必要,
如果有必要调用,对象会被放入F-Queue一个队列中,然后虚拟机会创建一个低优先度
的flinazer的线程去执行finalize,如果在finalize重新和根对象建立连接,对象将会活的一枚复活币,
然后从F-Queue中删除(finalize方法只会调用一次,复活币只会有一枚)
终结阶段
当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回
收器对该对象空间进行回收。
对象空间重新分配阶段
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了
垃圾收集算法
标记-清除(Mark-Sweep)
- 标记-找出内存中需要回收的对象,并且把它们标记出来
- 此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时
- 清除-清除掉被标记需要回收的对象,释放出对应的内存空间
缺点:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程 序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1)标记和清除两个过程都比较耗时,效率不高
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无 法找到足够的连续内存而不得不提前触发另一次垃圾收集动。
标记-复制(Mark-Copying)
- 将内存划分为两块相等的区域,每次只使用其中一块,
- 当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
缺点: 空间利用率降低。
标记-整理(Mark-Compact)
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活
的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)
class文件
class常量池
字面量:字面量主要是文本字符串、final 常量值、类名和方法名的常量等。
文本字符串是public String s = "abc";中的"abc"
用final修饰的成员变量,包括静态变量、实例变量和局部变量
符号引用:类和接口的全限定名、字段名称和描述符、方法名称和描述符
类加载机制
装载
类加载器通过类全限定名获取2进制流,将class文件的静态存储结构转换为方法区的运行时数据结构,然后在堆中生成一个java.lang.Class对象作为对方法区中数据访问入口
方法区:类信息,静态变量,常量
堆:代表被加载类的java.lang.Class对象
堆:代表被加载类的java.lang.Class对象
链接
验证(-Xverify:none 取消验证)
文件格式验证
比如是否符合class文件格式,是否能正确存入方法区运行时数据格式等。
元数据验证
java语法校验,比如是否有父类,是否继承了final修饰的父类,一个非抽象类是否实现了所有的抽象方法等等
字节码验证
进行数据流和控制流分析,确定程序语义是合法的、符合逻辑的。对类的方法体进行校验分
析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。运行检查
栈数据类型和操作码操作参数吻合(比如栈空间只有4个字节,但是我们实际需要的远远大于
4个字节,那么这个时候这个字节码就是有问题的)
跳转指令指向合理的位置
析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。运行检查
栈数据类型和操作码操作参数吻合(比如栈空间只有4个字节,但是我们实际需要的远远大于
4个字节,那么这个时候这个字节码就是有问题的)
跳转指令指向合理的位置
符号引用验证
这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),
可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。符号引用
验证的目的是确保解析动作能正常执行。比如常量池中描述类是否存在
访问的方法或者字段是否存在且具有足够的权限
可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。符号引用
验证的目的是确保解析动作能正常执行。比如常量池中描述类是否存在
访问的方法或者字段是否存在且具有足够的权限
准备
为类变量(静态变量)分配内存并且设置该类变量的默认初始值。
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量是
会随着对象一起分配到Java堆中。
这里不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量是
会随着对象一起分配到Java堆中。
解析
把类中的符号引用转换为直接引用,如果有了直接引用,那引用的目标必定存在内存中
符号引用个人理解为编译后class文件上的标识符
直接引用跟jvm内存布局有关,个人理解为直接指向内存的指针
对解析结果进行缓存,一个符号引用可能会被多次解析,一次解析成功就一直成功,失败就一直失败
初始化
初始化阶段是执行类构造器()方法的过程。比如赋值
在Java中对类变量进行初始值设定有两种方式:
声明类变量是指定初始值
使用静态代码块为类变量指定初始值
声明类变量是指定初始值
使用静态代码块为类变量指定初始值
JVM初始化步骤:
1.假如这个类还没有被加载和连接,则程序先加载并连接该类
2.假如该类的直接父类还没有被初始化,则先初始化其直接父类
3.假如类中有初始化语句,则系统依次执行这些初始化语句
1.假如这个类还没有被加载和连接,则程序先加载并连接该类
2.假如该类的直接父类还没有被初始化,则先初始化其直接父类
3.假如类中有初始化语句,则系统依次执行这些初始化语句
使用
主动引用(只有当对类的主动使用的时候才会导致类的初始化)
创建类的实例,也就是new的方式
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(如 Class.forName(“com.carl.Test”) )
初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类( JvmCaseApplication ),直接使用 java.exe 命令来
运行某个主类
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(如 Class.forName(“com.carl.Test”) )
初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类( JvmCaseApplication ),直接使用 java.exe 命令来
运行某个主类
被动引用
引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
定义类数组,不会引起类的初始化。
引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化
的)。
定义类数组,不会引起类的初始化。
引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化
的)。
卸载
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
类加载器(ClassLoader)
负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例的代码模块。
类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。
类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。
有哪些类加载器
Bootstrap ClassLoader(启动类加载器)
负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由 C++实现,不是ClassLoader子类。
Extension ClassLoader(扩展类加载器)
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs 指定目录下的jar包。
App ClassLoader(应用类加载器)
负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。
Custom ClassLoader(自定义类加载器)
通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的 ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。
为什么分层?
为信任级别做区分,比如自己写的类路径下的类和java核心api下的类,不能被随意篡改,如String类
JVM类加载机制的三种方式
全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该
类加载器负责载入,除非显示使用另外一个类加载器来载入
双亲委派(父类委托)
“双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标
类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。
类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。
自底向上检查类是否加载,自顶向下尝试加载类
缓存机制
缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class
时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进
制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程
序的修改才会生效.对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不
会被重复调用。
实现自己的类加载器
继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应
该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。
打破双亲委派
SPI
OSGI
类加载扩展点
为什么静态方法不能调用非静态方法和变量
静态方法属于类,在类加载的时候就会分配内存,有了入口地址,可以通过“类名.方法名”直接调用。
非静态成员(变量和方法)属于类的对象,所以只有该对象初始化之后才会分配内存,然后通过类的对象去访问。
所以静态方法中调用非静态成员变量,该变量可能还未初始化。因此编译器会报错。
静态块是在什么时候执行?
类中的静态块会在整个类加载过程中的初始化阶段执行,而不是在类加载过程中的加载阶段执行。
初始化阶段是类加载过程中的最后一个阶段,该阶段就是执行类构造器方法的过程
类构造器?
clinit是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法。
clinit是class类构造器对静态变量,静态代码块进行初始化
一个类一旦进入初始化阶段,必然会执行静态语句块。
静态类和非静态类程序的初始化顺序
父类静态变量
父类静态代码块
子类静态变量
子类静态代码块
父类非静态变量
父类非静态代码块
父类构造函数
子类非静态变量
子类非静态代码块
子类构造函数
父类需要优先加载,然后在是子类,接着是父类的静态方法加载优先,其次是子类。
收藏
收藏
0 条评论
下一页