概述
JVM的位置
Java一次编译,到处运行
JVM体系结构
1.方法区和堆区是所有线程共享的内存区域;而java栈,本地方法栈和程序计数器是运行在线程私有的内存区域
2.java栈又叫做Jvm虚拟机栈
3.jvm生命周期:
启动:通过类加载器创建一个初始类来完成.
执行:
- 一个运行的java虚拟机有一个清晰的任务:执行Java程序
- 程序开始执行的时候他才运行,程序结束时他就停止
- 执行一个所谓Java程序时候,真正 正在执行的是一个叫Java虚拟机的进程
退出:
- 程序正常执行结束,程序异常和错误
- 程序异常或错误而异常终止
- 操作系统错误导致终止
- 线程调用调用Runtime类的halt方法,System的exit方法
- JIN调用API来加载或卸载Java虚拟机,Java虚拟机退出
java中的线程和进程
1.每个线程:独立包括程序计数器,栈,本地栈
2.线程间共享:堆,堆外内存(方法区,永久代/元空间,代码缓存)
PC(程序计数器),VMS(虚拟机栈),NMS(本地方法栈)
JVM95%的优化发生才堆区,5%发生在方法区
类加载
类加载的过程:
类加载的过程总共五个阶段:加载,验证,准备,解析,初始化
为了支持运行时绑定,解析过程在某些情况下可在初始化之后再开始,其他过程一般按顺序进行,但他们是按顺序开始,而不是完成了再下一个.
加载:
虚拟机完成三件事
- 通过一个类的全限定名来获取其定义的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的进行时数据结构
- 在堆中产生一个代表这个类的Class对象,作为方法区中这些数据的访问入口
[魔数]:Class文件的头4个字节,它的唯一作用是确定这个文件是否是一个能被虚拟机接收的Class文件,用来识别文件的格式,不仅仅是class文件.
验证:
验证主要是保证加载的类的正确性.CA
(1)文件格式的验证:验证.class文件字节流是否符合class文件格式规范,并且能被虚拟机处理,是否以魔数 0xCAFEBABE 开头,主次版本号是否在当前虚拟机处理范围之内
(2)元数据验证:主要是堆字节码描述的信息进行语义分析,保证其符合java语言规范,比如验证这个类是否有父类,抽象类是否实现了其父类或接口之中要求实现的方法,字段是否与父类冲突.
(3)字节码验证:通过数据流和控制流分析,确定程序语义是否合法,符合逻辑.
(4)符合引用验证:在解析阶段发生,保证可以将符号引用转化为直接引用
整个加载机制,验证阶段是重要非必需的阶段,如果我们能保证代码没问题
想节省验证时间可以使用:-Xverfity:none来关闭大部分验证
准备:
为类变量分配内存并设置类变量初始化值,这个变量所使用的内存都将在方法区中进行分配
解析:
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程.
符合引用:以一组符合来描述所引用的目标,可以是任意形式的字面量,要能无歧义的定位到目标.比如可以用外号或者学号来代表一个学生(非物理连接方式)
直接引用:是可以指向目标的指针,相当偏移量或者是一个能直接/间接定位到目标的句柄,不同的虚拟机直接引用一般不同,具体和虚拟机实现的内存有关(物理连接方式).
解析动作主要针对:类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7类符合引用.
初始化:
在准备阶段已经为类变量赋过一次值,在初始化阶段,程序员可以根据自己的需求来赋值,这个阶段就是执行类构造器<clinit>()方法的过程.
在初始化阶段,主要为类的静态变量赋正确的初始值,jvm负责对类进行初始化,主要对类变量进行初始化,在java中对类变量进行初始值设定有两种方式:
- 声明类变量为指定初始值
- 使用静态代码块为类变量指定初始值
JVM初始化步骤:
- 类还没被加载或连接,则程序先加载并连接该类
- 类的直接父类还没初始化,则先初始化直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
类的初始化时机(主动使用):
- 创建类的过程,也就是new
- 访问某个类/接口的静态变量,或对该静态变量赋值
- 调用类的静态方法
- 反射
- 初始化某个类的子类,其父类也会被初始化
- java虚拟机启动时标明为启动类的类(JavaTest)
其他都为被动使用,类不会初始化
类加载器
负责动态加载java类到java虚拟机内存空间中
有3种默认类加载器:
1.启动类(根)加载器:C++实现的,加载java核心库,加载环境变量下的rt.jar,resources.jar,charsets.jar和类
2.扩展类加载器:统领加载,加载目录%JRE_HOME%libext目录下的jar包和class文件,还可以加载-D java.ext.dirs选项指定的目录
3.应用程序(系统类)加载器:也称为SystemAppClass。 加载当前应用的classpath的所有类
类加载的方式三种方式:
- 加载main方法的主类
- 通过Class.forName()方法动态加载,默认执行初始化块(static{}).可以通过Class.forName(name,initialize,loader)种的initialize来指定是否执行初始化块
- 通过ClassLoader.loadClass()方法动态加载,不会执行初始化块
双亲委派机制
加载流程:
先向上找,找到了就加载,然后停止,否则在依次向下找,找到了就加载然后停止.
好处:
- 可以避免重复加载,父类加载完了,子类就不需要再次加载
- 更加安全,如果不使用双亲委派机制,用户就是随意定义类加载器来加载核心api,产生隐患
package java.lang;
public class String {
//双亲委派机制:安全
//1.APP-->EXC->BOOT(最终执行)
//BOOT
//EXC
//APP
public String toString1(){
return "yyy";
}
public String toString(){
return "Hello";
}
public static void main(String[] args) {
String s=new String();
s.toString();
/*
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
实际在rt点jar包里找
*/
}
/*
1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器区完成,一直向上委托,直到启动类加载器
3.启动类加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通过加载器进行加载
4.重复步骤 3
null: java调用不到 ~ C,C++
Java=C++:去掉繁琐的东西,指针,内存管理
*/
}
这里还有个沙箱安全机制,它用到了双亲委派机制
我们这里定义了一个包在java.lang下的String,String本属于jdk,如果没有沙箱安全机制,我们这个类会污染所有的String,他向上bootstrap中找到string,就加载string.然后发现真正的String类里面没有main
沙箱安全机制还要3个组成部件:
- class文件检验器
- 内置于Java虚拟机(及语言)的安全特性
- 安全管理器及Java API
class文件校验器
有4次扫描,保证class文件正确
- 检查class文件结构是否正确,比如检查class文件是否以魔数OxCAFEBABE打头(过滤损坏的,或非class文件)
- 检查是否符合java语言编译规则
- 简直字节码能否被JVM安全的执行,不会导致JVM崩溃
- 符合引用验证,jvm检查这个符号链接的正确性,然后建立真正的物理引用(直接引用)
内置于Java虚拟机的安全特性
基础的java语言特性,降低java出现内存混乱,崩溃的几率
- 机构化内存访问(不使用指针,一定程度上让黑客无法篡改内存数据)
- 自动垃圾回收
- 数组边界检查
- 空引用检查
- 数据类型安全
java的安全机制api
大部分都在java.security包下,因为源码很多,就不贴出来了,大家感兴趣的话可以研究下。以下是一些常用的类的api介绍:
-
java.security.AccessControlContext:基于它所封装的上下文作出系统资源访问决定,该类最常用于将代码标记为享有“特权”。
-
java.security.AccessController:用于与访问控制相关的操作和决定。java.security.SecureClassLoader此类扩展了 ClassLoader,支持使用相关的代码源和权限定义类,这些代码源和权限默认情况下可根据系统策略获取到。
-
java.security.Provider:此类表示 Java 安全 API "provider",这里 provider 实现了 Java 安全性的一部分或者全部。
-
java.security.Permission:表示访问系统资源的抽象类。所有权限都有一个名称(对它们的解释依赖于子类),以及用来定义特定 Permission 子类的语义的抽象方法。
本地方法接口
什么是native方法?
native方法是java调用非java代码的接口,定义一个native方法不需要提供实体(类似定义接口)
为什么使用需要native?
与java环境外交互,是本地方法存在的主要原因.
java的不足之处体现在运行速度上,还要不能直接访问到操作系统底层,因此使用native(原生的)关键字来扩展java程序功能
标识符native可以与其他所有的java标识符连用,但是abstract除外
目前该方法的使用越来越少,除非与硬件有关的应用
public class Demo {
public static void main(String[] args) {
new Thread(()->{
},"aa").start();
}
//native: 凡是带native 关键字的,说明java的作用范围达不到了,回去调用底层c语言的
//会进入本地方法栈
//调用本地方法接口 JNI
//JNI作用: 扩展java的使用,融合不同的编程语言,融合不同的编译语言为java所用
//java诞生的时候,c和c++横行,想要立足,必须要有调用c和c++的程序
//它在内存区域中专门开辟了一块专门标记区域:Native Method Stack,等级 native 方法
//在最终执行的时候,加载本地方法库中的方法通过JNI
//Java程序驱动打印机,管理系统,掌握即可,在企业级应用中较为少见
private native void start0();
//调用其他接口: Socket..WebService~..http~
}
本地方法栈
-
java虚拟机用于管理java方法的调用,本地方法栈是用于管理本地方法(一般非java实现的方法)的调用
-
本地方法栈,也是线程私有的
-
允许被实现成固定/可扩展的内存大小
-
当某个线程调用一个本地方法时,它就进入一个全新的并且不再受虚拟机限制的世界.它拥有和虚拟机拥有同样的权限,本地方法可以通过本地接口来访问虚拟机内部运行时数据区,它甚至可以直接使用本地处理器中的寄存器,直接从本地内存的堆中分配任意数量的内存.
-
不是所有的方法都支持本地方法
程序计数器
JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,
CPU只是把数据装载到寄存器才能够运行.JVM中的PC寄存器是对物理PC寄存器的一种抽象模型.
一个线程对应一个JVM栈,JVM栈中包含以组栈帧,当JVM调用一个java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入栈中.
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与其向相关联的方法为当前方法
程序计数寄存器的作用
PC寄存器是用来存储指向下一条指令的地址,也即是将要执行的指令代码,由执行引擎读取下一条指令.
- 它在一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域
- 在jvm规范中,每个线程都是它自己的程序计数器,是线程私有的,生命周期与线程的生命周期一致
- 任何时候一个线程都只有一个方法在执行,也就是所谓的当前方法.程序计数器会存储当前线程正在执行的java方法的JVM指令地址.如果在执行native方法,则未指定值,因为程序计数器不负责本地方法栈
- 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一跳需要执行的字节码指令
- 它是唯一一个在java虚拟机规范中,没有规定任何OOM情况的区域,而且没有垃圾回收机制
面试常问:
1.为什么使用PC寄存器记录当前线程的执行地址
(1)多线程宏观上是并行(多个事件同时发生),但实际上是并发交替执行的
(2)因为CPU需要不停的切换各个线程,这时候切换回来以后,要知道从哪里开始继续执行
(3)JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
所以,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响
2.pc寄存器为什么会设定未线程私有
所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停滴做任务切换,这样必然会导致经常中断或恢复,为了准确记录各个线程正在执行的当前字节码指令地址,自然是给每个线程分配一个pc寄存器,各个线程之间进行独立计算,互不干扰
方法区
方法区(永久代)在jdk8中又叫做元空间Metaspace
方法区存储已被虚拟机加载的类信息,常量,静态变量,即使编译器(JIT编译器)编译后的代码,虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的为和Java堆区分
方法区是被所有的线程共享,所有字段和方法字节码,以及一些特殊的方法,如构造函数,接口代码也在此定义,
简单的说,所有定义的方法的信息都保存在该区域,此区域属于共享区间
static,final,静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是 实例变量存在堆内存中,和方法区无关
栈
由于跨平台的设计,java的指令都是根据栈来设计的,不停平台cpu架构不停,所有不能设计基于寄存器的.
根据栈设计的优点:跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令.
内存中的堆和栈
栈是运行时的单元,而堆是存储的单元
1.栈解决程序的运行问题,即程序如何执行,或者说如何处理数据.堆解决的是数据存储问题,即数据怎么放,放哪里
2.一般来讲,对象主要都是放堆空间里面试运行时数据区比较大的一块
3.栈空间存放 基本数据类型的局部变量,以及引用数据类型的对象的引用
Java虚拟机栈的特点
- java虚拟机栈,早期也叫java栈.每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应这个一次次的java方法调用,它是线程私有的
- 生命周期和线程是一致的
- 作用:主管java程序的运行,并返回参与方法的调用和返回
- JVM直接堆java栈的操作只有两个(1)每个方法的执行,伴随着进栈. (2)执行结束后的出栈工作
- 对于栈来说不存在垃圾回收问题,但存在OOM异常
局部变量表:
局部变量表是一组变量值存储空间,用来存放方法参数和方法内部定义的局部变量(基本数据类型,对象引用)
在Java编译成Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量
方法嵌套调用次数由栈的大小决定,一般来说,栈越大,方法嵌套调用次数越多
局部变量表的变量只在当前方法调用中有效,方法调用结束,局部变量表就销毁
基本单位为变量槽(slot)
-变量槽(slot)
- 局部变量表的单位是变量槽(slot),每个变量槽可以存储32位长度的内存空间
- 可以存储8种基本数据类型,引用类型
- byte,short,char,float存储前转化为int,boolean转化为int,0为false,非0为true
- long和都double会被分配2个连续的空间(需要两次32位读写),其他仅分配一个空间
- JVM会为每个局部变量表种的slot分配一个访问索引,通过索引可以访问局部变量表中指定的局部变量值
- 如果访问65bit的局部变量值,只需要1个索引
- 如果当前栈帧是由构造方法或者实例方法创建的,呢么该对象引用this将会存放在index为0的slot处,其他参数参照参数表顺序排列
- 静态方法不能引用this,因为静态方法栈中局部变量表没有this
- 局部变量表中的变量是垃圾回收的重点,被局部变量表中直接引用或间接引用的对象都不会被回收
操作数栈:
和局部变量表一样,以字长位单位的数组,不过局部变量表用的是索引,操作数栈是弹栈(pop),压栈(push)来访问.操作数栈可以理解位java虚拟机栈中的一个用于计算的临时数据存储区.
主要用于保存计算过程中间结果,同时作为计算过程中变量临时存储的空间
操作数栈中,在压栈前会将byte,short,char转化位int.32bit的类型占用一个栈单位深度,64bit的类型占用两个栈深度单位
数据运算的地方,大多数指令(执行复制,交换,求和)都发生在操作数栈,然后结果压栈
栈顶缓存技术TOS
将栈顶元素全部缓存在物理cpu的寄存器中,以此降低对内存读/写次数,提升执行引擎的执行效率
动态链接:
每个栈帧都是包含一个指向运行时常量池(jdk1.7前在方法区,1.8之后常量池和静态常量池存放在元空间中)中该栈帧所属方法的引用,持有这个引用就是支持方法调用过程的动态连接
作用是将符合引用转化为调用方法的直接引用
方法返回地址:
方法出口有两种方式:正常完成出口和异常完成出口
正常的函数返回,使用return指令,另外一种抛出异常,两种方式,都会导致栈帧被弹出
无论哪种方式退出,最终返回到该方法被调用的位置,需要恢复上层方法的局部变量表,操作数栈,将返回值给调用者的栈帧中,让调用方法继续执行
异常完成退出不会给调用者产生任何的返回值
栈道中还要附加信息
栈内存,主管程序的运行,生命周期和线程同步
线程结束,栈内存也就释放了,对于栈来说,不存在垃圾回事问题
一旦线程结束,栈就Over
栈:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧
Java栈的大小是动态的不是固定不变的,ava栈满了:StackOverflowError
可以动态扩展,如果尝试扩展后无法申请足够的空间会抛出 OutOfMemoryError
可以在VM options中输入 -Xss来指定栈的大小,例如 -Xss256k,指定大小位256k
public class Test {//爆栈
public static void main(String[] args) {
new Test().a();//会爆栈
}
public void a(){
b();
}
public void b(){
a();
}
}
栈+堆+方法区:交互关系
相关面试题:
1.举例栈溢出的情况?
抛出:stackOverFlowError
可以调用递归,或者用-Xss设置栈的大小
2.调整栈的大小,就能保证不会出现栈溢出么?
不能,调用递归无限次肯定会溢出,如果申请的空间太多,还好抛出内存溢出(Out of Memery Error)
3.分配的栈内存越大越好?
不是,会挤压其他线程的空间
4.虚拟机栈存在垃圾回收机制?
不存在垃圾回收机制
三种JVM
1.Sun公司 HotSpot
2.BEA JRockit
3.IMB J9VM
我们学习的都是:HotSpot
堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的.
类加载器读取类文件后,一般会把什么东西放到堆中呢?类,方法,常量,变量~,保存我们所有引用类型的真实对象
堆内存中还要细分为三个区域:
- 新生区(伊甸园区) Young/New
- 养老区 Old
- 永久区 Perm
真理:经过研究,99%都是临时对象
新生区:
- 诞生和成长的地方,甚至死亡
- 伊甸园区,所有的对象都是在伊甸园区new出的
- 幸存者区(0,1)
老年区
永久区
这个区域常驻内存的,用来存放一些jdk自身携带的Class对象,interface元数据,存储的是java运行时的环境或类信息
这个区域不存在垃圾回收,关闭VM虚拟机就会释放这个区域的内存~
一个启动类,加载了大量的第三方jar包,tomcat部署了太多的应用,大量动态生成的反射类.不断的被加载,直到内存满,就会出现OOM
- jdk1.6之前: 永久代,常量池在方法区
- jdk1.7: 永久代,但是慢慢的退化了 '去永久代' 常量池在堆中
- jdk1.8之后: 无永久代,常量池在元空间
long max=Runtime.getRuntime().maxMemory();//返回虚拟机试图使用的最大内存
long total=Runtime.getRuntime().totalMemory(); //返回jvm的总内存
设置虚拟机内存
我们设置堆内存为8m,然后让虚拟堆内存爆掉,打印信息
先分配新生区,先GC,然后再满几次就Full GC,最后重GC也清理不动,报OOM错误
元空间:逻辑上存在,物理不存在
再一个项目中,突然出现OOM故障,专门分析原因?
- 能看看到代码在第几行出错:内存快照分析工具,MAT,Jpro
- Dubug,自己分析代码
MAT,Jprofiler作用:
- 分析Dump内存文件
- 快速定位内存泄漏问题
- 获得堆中的数据,获得大的对象
GC:垃圾回收
JVM在进行GC时,并不是堆这三个区域统一回收.大部分时候,回收都在新生代
经过异常GC后,eden去和幸存from区被情况,from区和to区会交换角色,被清空from变成新的to区
- 新生区
- 幸村区(from ,to)
- 老年区
GC两种类:轻GC(普通gc),重GC(全局gc)
新生代GC(Minor GC):发生再新生代的收集动作,Minor GC非常频繁,回收速度一般也比较快
老年代GC(Major GC/Full GC):发生再老年代的GC,出现Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串,数组)
为什么要这样?
为了避免大对象分配内存时由于分配担保机制带来的复制而降低效率
长期存活的对象将进入老年代
虚拟机给每个对象一个对象年龄(Age)计数器,对象再eden区出生,经过Minor GC后仍然能存活
并且能被幸存区容纳的话,将被移动到幸存区,年龄设置为1.每熬过一个Minor GC后,对象年龄增加1岁,当达到15岁(默认)时,将会晋升到老年区中,对象晋升的到老年代的年龄阀值,可以通过-XX:MaxTenuringThreshold来设置
判断对象已经死亡
引用计数法
给对象添加一个引用计数器,每当一个地方引用它时,计数器加1,每当引用失效时,计数器减少1.当计数器的数值为0时,也就是对象无法被引用时,表明对象不可在使用,这种方法实现简单,效率较高,大部分情况下不失为一个有效的方法。但是主流的Java虚拟机如HotSpot并没有选取引用计数法来管理内存,主要的原因难以解决对象之间的相互循环引用的问题
public class GC01 {
Object instance = null;
public static void main(String[] args) {
GC01 objA = new GC01();
GC01 objB = new GC01();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
/**
* A和B互相引用,如果用引用计数法,呢么它们的计数器都不为0
* 于是引用计数器无法通知GC回收器回收他们
*/
可达性分析算法
这个算法的基本思想通过称之为"GC Roots"的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称之为引用链
当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的
引用
1.强引用:
我们使用的大部分引用实际上是强引用,这是使用最普遍的引用.如果对象具有强引用,那就类似不可少的生活用品,垃圾回收器绝不会回收它,当内存空间不足,Java虚拟机宁愿抛出 OutofMemoryError,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题
2.软引用:
对象只具有软引用,就类似可有可无的生活用品,如果内存空间足够,就不会被垃圾回收器回收,如果内存空间不足,就会回收这些对象的内存,只有没有被回收,对象就是可以被程序使用
3.弱引用:
如果对象只具有弱引用,就类似可有可无的生活用品,它比软引用生命周期更短,垃圾回收器线程扫描到它所管辖的内存区域过程中,不论内存是否足够,会直接回收它.垃圾回收器是一个很优先级很低的线程,因此不一定很快发现呢些弱引用对象
4.虚引用:
形同虚设的引用,虚引用不会决定对象的生命周期,如果一个对象只有虚引用,呢么和没有任何引用一样,任何时候都可能被垃圾回收器回收.
虚引用主要用来跟踪垃圾回收的获得.
程序设计很少用虚引用和弱引用,软引用用的情况比较多
因为软引用可以加速JVM对内存的回收速度,维护系统的运行安全,防止内存溢出(OutOfMemory)
不可达的对象并非"非死不可"
即使可达性分析中不可达的对象,也并非"非死不可",要真正的宣告一个对象的死亡,至少经历2个过程:
不可达的对象会被第一次标记并且进行一次筛选,筛选条件是该对象是否必要执行finalize方法,如果没有覆盖finalize,或finalize方法已经被虚拟机调用过时,虚拟机将两种情况视为没有必要执行.
判断为需要执行的对象将会被放到一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就要被回收.
任何判断一个常量是否是废弃常量
字符串"abc"如果没有被任何String对象引用,就定义为废弃常量,如果发生内存回收,就会被系统清理
如果判断一个类是无用的类
要满足3个条件才是算"无用的类":
- 该类的所有实例都已经被回收,也就是java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
要满足以上3个条件的无用类进行回收,这里说的仅仅是"可以",而并不是和对象一样不使用了就会必然被回收
垃圾回收算法
复制算法
幸存区to,幸存区from,谁空的谁是to
Eden区存活的对象复制到to区,from区的对象根据对象需要根据年龄决定去向
好处:没有内存的碎片
坏处:浪费幸存区空间(多了一半空间永远是空to)
复制算法最佳使用场景:对象存活度比较低的时候(新生区)
标记清除算法
缺点:两次扫描,严重浪费时间,会产生内存碎片
优点:不需要额外的空间
标记压缩算法
和标记清除类似,只是会将存活的对象向一端移动,然后直接清理掉死亡的对象
分代收集算法
没有什么新思想,只是根据对象存活度的不同,将内存分成不同的几块.java堆分为新生代和老生代,然后用不同的算法处理
1.新生代每次收集都有大量对象死亡,所以选择复制算法,只需要少量对象的复制成本就可以完成垃圾回收
2.老生代的对象存活度较高,且没有额外的空间对其进行分配担保,所以我们必须选择标记清除/标记压缩算法
GC题目:
- JVM的内存模型和分区~详细到每个区放上面?
- 堆里面的分区有哪些?Eden,form,to,老年区,说说他们的特点!
- GC算法有哪些?标记清除法,标记压缩,复制算法,引用计数器,怎么用?
- 轻gc和重gc分别在什么时候发生?
总结:
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法
新生代:存活率低
复制算法
老年代:区域大,存活率高
标记清除(内存碎片不是太多)+标记压缩混合实现
JMM
最后
以上就是酷酷乐曲为你收集整理的JVM学习笔记的全部内容,希望文章能够帮你解决JVM学习笔记所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复