在windows中,虚拟一个运行环境。
Java技术的核心就是java虚拟机,因为所有的java程序都运行在java虚拟机内部。
jdk、jre、jvm区别:
jvm的组成部分
程序在执行之前先要把java代码转换成字节码(class 文件),jvm首先通过类加载器把字节码加载到内存中的运行时数据区 ,然后通过执行引擎将字节码翻译成机器码,再交由CPU去执行,而这个过程中需要调用其他语言的接口本地库接口来实现整个程序的功能,这就是这 4 个主要组成部分的职责与功能。
将硬盘上的class文件(字节码),加载到内存中运行时数据区。至于它是否可以运行,由加载引擎决定。
User.class通过类加载成User的Class对象(Class类的对象来表示这个类的信息),然后去创建对象
使用IO读取字节码文件,转换为方法区中运行时结构,为每个类创建一个Class类的对象,存储在方法区中
验证:
准备:为静态的变量进行内存分配,并设置默认初始值;不包含用final修饰的static常量(final修饰的静态常量在编译时进行初始化)
public static int value = 123;//value 在准备阶段后的默认值是0,而不是123
解析:将编译后的字节码中的符号引用转成直接引用
(符号引用:Class文件的逻辑符号。直接引用:方法区中某一个地址)
类的初始化,为类中的静态变量进行赋值
public static int value = 123;//value在初始化阶段后值是123.
类被初始化后,才认为类的加载完成了
JVM 规定,每个类或者接口被首次主动使用时才对其进行初始化。以下5种情况,类加载的过程是完整进行的
以下两种情况类不会被初始化(不会被完整加载):
- static final int b = 20; //编译期间赋值的静态常量
- System.out.println(User.b);
- User[] users = new User[10];//作为数组类型
1.引导类加载器(启动类加载器)。
用c/c++语言开发的,负责加载java核心类库。与java语言无关的。
2.扩展类加载器。
java 语言编写的,由sun.misc.Launcher$ExtClassLoader实现,继承ClassLoader类。从JDK安装目录的 jre/lib/ext子目录(扩展目录)下加载类库
3.应用程序类加载器。
Java语言编写的,由sun.misc.Launcher$AppClassLoader实现,派生于ClassLoader类。加载程序中自己开发的类。
应用程序类加载器是由扩展类加载器加载,扩展类加载器由引导类加载器加载的
加载一个类时,它并不会自己先去加载,而是先委托给父类加载器加载。如果父类加载器还存在其父类加载器,继续向上级委托,直到最顶级的引导类加载器。父级找到就返回,如果没有找到就委派给子级加载器。最终所有的类加载器没有找到,报ClassNotFoundException。
为什么要这么做?
保证安全,为保证先加载系统中的类,避免我们写的类把系统里面的类覆盖掉,防止类被重复加载。
思考:自己创建一个名为java.lang的包,再创建一个名为String的类,当我们new String()时,会将加载创建核心类库中的String对象还是创建我们自己创建的String类对象?
先确保加载系统类,避免我们写的类把系统里面的类覆盖掉
- public static void main(String[] args) {
- new java.lang.String();//逐级向上委托,最终引导类加载器找到了系统中真正的String
- }
- public class String{
- static {
- System.out.printIn("自定义的String");
- }
- }
双亲委派优点?
双亲委派机制,是java提供的类加载的规范,但不是强制不能改变的。我们可以通过自定义的类加载器,改变加载方式。
可以通过继承ClassLoader类,重写findClass方法,去自己指定要加载的类。
典型的tomcat中,加载部署在tomcat中的项目时,就使用的是自己的类加载器
每个线程都有一个程序计数器,用来记录每个线程所运行到的位置(用来存储下一条指令的地址,由执行引擎读取下一条指令),是线程私有的,生命周期与线程一致。
内存空间小,运行速度快,不会出现内存溢出的情况和垃圾回收。
java虚拟机栈运行java方法的区域,而本地方法栈用来运行本地方法;(被Native修饰的方法//private native void start0();)
是线程私有的;空间大小可以调整,可能会出现内存溢出错误(栈溢出),不会有垃圾回收。
栈是运行单位(堆是存储单位),是用来运行java中的方法,可能会出现栈溢出,不会有垃圾回收。每个方法在执行的同时都会创建一个线帧(用于存储局部变量表、操作数栈、动态链接、方法出口等信息),每个方法从调用到执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。
是线程私有的,每一个线程对应一个栈。不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法)。
栈的操作有两个,调用方法(入栈),运行结束(出栈)
栈的特点
栈中会出现异常,当线程请求的栈深度大于虚拟机所允许的深度时 , 会出现StackOverflowError。(内存不够)
在一条活动的线程中,一个时间点上,只有一个活动栈,只有当前在执行的方法的栈帧(栈顶)是有效的,被称为当前栈帧,与当前栈帧对应的方法为当前方法,定义这个方法的类为当前类。
栈帧结构:
(1)局部变量表(存储方法中的变量)
(2)操作数栈(数据计算的过程 所在的空间)
(3)动态链接 。A调用B,会在A方法中存储B方法的地址
- void A(){
- B();//B方法的地址
- }
(4)方法返回地址。方法执行完毕之后,要返回之前调用它的地方。
是存储空间,用来存储java对象,是Java虚拟机中最大的一块内存,是所有线程共享的。
在jvm启动时就被创建,大小可以调整(jvm调优)。例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小),一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除,堆是垃圾回收的重点区域。
Java8及之后堆内存分为:新生区(新生代)+老年区(老年代)
新生区分为伊甸园区和两个幸存者区
伊甸园区:存放刚刚创建的对象
两个幸存者区:始终有一个区域是空的,减少内存中的碎片
老年区:存放生命周期较长的对象,垃圾回收的频率会降低;年轻代垃圾回收的频率高
可以根据对象存活的时间长短,把对象放在不同的区域,不同区域的垃圾回收策略不同,可以提高垃圾回收效率。
频繁回收年轻代,较少回收老年代。
- public static void main(String[] args) {
- List
list = new ArrayList(); - while(true){
- list.add(new Random().nextInt());
- }
- }
配置新生代与老年代在堆结构的占比
在整个项目中,生命周期长的对象偏多,就可以把老年代设置更大,来进行调优。
对象垃圾回收的年龄 -XX:MaxTenuringThreshold=
JVM在进行GC时,大部分时候回收的都是指新生区,针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大类型:部分收集、整堆收集。
部分收集:
对年轻代进行垃圾回收称为Minor GC/yong GC,是频繁进行的回收
对老年代进行垃圾回收称为Major GC/old gc,回收的次数较少
整堆收集:在内存实在不够用时,会触发Full GC(整堆收集),对整个java堆和方法区进行垃圾收集。
以下情况会触发Full GC:
开发期间尽量避免整堆收集,因为垃圾回收时,会造成其他现成的暂停。
JVM调优:根据实际的场景,来对内存空间的大小、比例、垃圾回收器进行设置。
官方文档 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
在jdk7之前,字符串常量池在方法区中,由于方法区的垃圾回收在整堆收集时发生,回收频率低。在jdk7之后,将字符串常量池的位置移到了堆空间中,回收频率高。
俗称为方法区,在jdk1.7之前称为永久代,在1.7之后称为元空间。
主要是存储类信息。在jvm启动时创建,大小可以调整,是线程共享,也会出现内存溢出,会发生垃圾回收(Full GC/整堆收集)。
Java虚拟机规范中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开。
方法区、堆、栈的交互关系:
Java方法区的大小不是固定的,JVM可以根据应用的需要动态调整。
元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的两个参数。
默认值依赖于平台,windows下,-XXMetaspaceSize是21MB,也称为高水位线,一旦触及就会触发Full GC,尽量将方法区的初始值设置较大一点,因为方法区一旦空间不足,就会触发FULL GC。
-XX:MaxMetaspaceSize 的值是-1,即没有限制,就可以使用计算机内存。
运行时常量池:与字符串常量池不同,运行时常量池中,主要用于存放编译期生成的各种字面量与符号引用
在FULL GC时,方法区发生垃圾回收。
主要是回收类信息,类信息回收条件比较苛刻,满足以下3点即可:
一般可以认为类一旦被加载就不会被卸载(回收)。
程序计数器、java栈、本地栈是线程私有的;堆、方法区是线程共享的,是会出现垃圾回收的
程序计数器不会出现内存溢出;java栈,本地栈、堆、方法区可能会出现内存溢出
java栈、本地栈、堆、方法区大小是可以调整的
本地方法:用native关键字修饰的方法,没有方法体。不是java语言实现的(操作系统底层方法)例如hashCode();
关键字native可以与其他所有的java标识符连用,但是abstract除外。
java语言需要与外部的环境进行交互(例如需要访问内存,硬盘,其他的硬件设备),直接访问操作系统的接口即可。
java的jvm本身开发也是在底层使用到了C语言
作用:将加载到内存中的字节码(不是直接运行的机器码),解释/编译为不同平台的机器码
.java文件 ---编译器(javac)-->.class(字节码文件),在开发期间,由jdk提供的编译器(javac)进行源码编译 (称为前端编译,编译期)
.class(字节码)----解释/编译成--->机器码(后端编译,在运行时由执行引擎完成的, 运行时)
解释器:将字节码逐行解释执行,效率速度慢,一般刚开始的时候使用解释执行
编译器(JIT just in time 即时编译器):将一些代码进行整体编译,后期执行效率快,但是编译是需要时间的。在程序运行过程中,将一些频繁执行的热点代码进行编译,并缓存到方法区中,以后执行效率提高了。
是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升java程序的执行性能。
热点代码:一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体。
目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
采用的解释器和编译器结合的方案。
java是支持自动垃圾回收,有些语言不支持需要手动。自动垃圾回收不是java语言首创的。
垃圾回收关心的问题:
在运行过程中,当一个对象不再被任何引用指向时,被称为垃圾对象。
- String s1 = "abc";
- s1 = null;
如果不及时清理内存中的垃圾对象,内存会被耗尽,严重时会出现内存溢出,新的对象没有空间存储。
垃圾回收也可以清除内存里的碎片。碎片整理 将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。(数组必须是连续空间的)
在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new进行内存申请,并使用delete进行内存释放。比如以下代码:
- MibBridge *pBridge= new cmBaseGroupBridge();
- //如果注册失败,使用 Delete 释放该对象所占内存区域
- if(pBridge->Register(kDestroy)!=NO ERROR)
- delete pBridge;
这种方式控制更加精确,但是给程序员带来了更多的工作量,万一忘记删除,那么会占用内存不释放(内存泄漏)
现在除了Java以外,C#、Python、Ruby等语言都使用了自动垃圾回收的思想。
好处:对内存管理更合理、自动化,降低内存泄漏和内存溢出的风险;将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发。
坏处:对程序员管理内存的能力降低了,解决问题能力变弱了,不能调整垃圾回收的机制。
哪些区域的回收?
垃圾回收主要针对堆和方法区进行,方法区只有在full gc时触发。
内存溢出:对象不断被创建,经过垃圾回收后,仍然不能满足新对象的存储,就会导致程序崩溃。OutofMemoryError
内存泄漏:一些对象不再被使用,但是垃圾回收器不能判定为垃圾,这些对象就默默的占用着内存资源,称为内存泄漏。大量的此类对象存在,也是导致内存溢出的原因。
内存泄漏的场景:
一些提供close()的资源未关闭导致内存泄漏:IO流、jdbc连接,Socket用完后没有关闭
单例对象,一个程序中,只存在一个对象,生命周期长
作用:判断对象是否为垃圾对象。
当一个对象不再被任何引用指向时,就为垃圾对象。相关的标记算法:引用计数算法和可达性分析算法。
引用计数算法(在现代的jvm中并没有被使用)。
实现原理:对每个对象保存一个整型的引用计数器属性,计数器来记录对象被引用的情况。
对于一个对象 A,只要有任何一个引用指向了对象A,则对象A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
- String s1 = new String("aaa");//1
- String s2 = s1; //2,有两个引用变量指向aaa对象
- s2 = null; -1
- s1 = null; -1
缺点:
实现思路:从一些根对象(GCRoots)出发去查找,与根对象直接或间接连接的对象就是存活对象,否则判定为垃圾对象。
GC Roots可以是哪些元素?
总的来说是活跃的,正在使用的或者系统中的
final,finally,finalize()区别?
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
自己不要在程序中调用finalize(),留给垃圾回收器调用。理由包括下面三点:
生存还是死亡?
由于finalization机制的存在,在虚拟机中的对象状态分为3种:
1.System.gc()会触发Full GC,程序员不要主动去调用。
.gc()方法被调用后,不会保证立即执行,因为垃圾回收也是一个线程,需要操作系统调度之后才可以执行。
2.内存溢出和内存泄漏
内存溢出:对象不断被创建,经过垃圾回收后,仍然不能满足新对象的存储,就会导致程序崩溃。OutofMemoryError
内存泄漏:一些对象不再被使用,但是垃圾回收器不能判定为垃圾,这些对象就默默的占用着内存资源,称为内存泄漏。大量的此类对象存在,也是导致内存溢出的原因。
内存泄漏的场景:
一些提供close()的资源未关闭导致内存泄漏:IO流、jdbc连接,Socket用完后没有关闭
单例对象,一个程序中,只存在一个对象,生命周期长。
3.Stop the World(STW)
在当垃圾回收的线程工作时,其他的线程会暂停一下。
为什么要暂停其他线程?确保是在某一个时间点上,否则运行中的分析是不准确的。
垃圾线程执行完后,其他线程恢复执行。越优秀的回收器,暂停时间比较短.
区分出内存中存活对象和死亡对象后,GC接下来就是垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是:
有两个大小相等的内存,一个是正在使用的,一个是空闲的;把正在使用的内存中存活的对象复制到空闲内存中,将正在使用的空间中的所有对象清除。
优点:减少内存的碎片,运行效率高。
缺点:需要两块内存空间,其中一块是空闲的。
使用场景:垃圾对象多、存活对象少,适合于新生代的垃圾回收。
清除不是真正的把垃圾对象清除掉,也没有移动存活的对象。
将垃圾对象地址保存到一个空闲列表中。后面有新对象到来时,从空闲列表中找到一个能够放的下新对象的地址,覆盖掉垃圾对象。
优点:运行简单
缺点:回收效率低;清理后的内存是不连续的,会产生碎片
将所有的存活对象移动到内存的一端,将其他区域的垃圾对象进行清理。
压缩算法是需要移动对象的
优点:不像复制算法有两块内存空间,不像清除算法有内存碎片。
缺点:回收速度慢
复制算法适合新生区,其余适合老年区
复制 | 清除 | 压缩 | |
---|---|---|---|
速率 | 最快 | 中等 | 最慢 |
空间开销 | 通常需要2倍空间(不堆积碎片) | 少(会堆积碎片) | 少(不堆积碎片) |
移动对象 | 是 | 否 | 是 |
前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点,分代收集应运而生。
不同的对象的生命周期是不一样的,可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
年轻代特点:区域较小、对象生命周期短、存活率低、回收频繁。这种情况复制算法的回收整理,速度是最快的
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。一般是由标记-清除或标记-清除与标记-压缩的混合实现
垃圾回收器是内存回收的实践者,垃圾回收算法是方法论。jvm中提供了许多不同类型的垃圾回收器,可以根据实际情况进行选择,这也是jvm调优的一部分。
(1)按线程数量:单线程垃圾回收器、多线程垃圾回收器
(2)按工作模式:独占式、并发式
(3)按回收区间:年轻代垃圾回收器、老年代垃圾回收器
图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
Concurrent Mark Sweep并发标记清除,支持垃圾回收线程与用户线程并发(同时)执行(只是在某个阶段做到了并发,还存在独占的)
初始标记:独占式的,暂停用户线程
并发标记:垃圾回收线程与用户线程并发(同时)执行
重新标记:独占式的,多垃圾线程执行
并发清除:垃圾回收线程与用户线程并发(同时)执行 进行垃圾对象的清除
从CMS垃圾回收器开始,首创了并发并发垃圾收集
优点:可以作到并发收集
弊端:使用标记清除算法,会产生内存碎片;并发执行影响到用户线程;无法处理浮动垃圾
引入并发标记后,用户线程和垃圾回收线程可以同时执行,会带来一个问题:对象是否为垃圾不好确定。
所以在标记时,将对象分为3种颜色(3种状态)
三色标记的过程:
这个过程正确执行的前提是:没有其他线程改变对象间的引用关系。然而,并发标记的过程中,用户线程仍在运行,因此就会产生漏标和错标的情况。
(1)漏标:
灰色对象下面还有未扫描的白色对象,但是灰色与黑色断开,本来灰色和白色就与跟对象没有关系了,可以被回收,但是灰色的还会再进行一次扫描,这样本次垃圾回收就不能回收灰色和白色。
(2)错标
B.D=null;//B到D的引用被切断
A.xx=D;//A到D的引用被建立
进行一次扫描,这样本次垃圾回收就不能回收灰色和白色
灰色对象和白色对象断开联系,白色被判定为垃圾,但是某个黑色与白色关联,黑色对象不再被扫描了,这样错将与黑色关联的白色 认定为垃圾。
错标的结果比漏表严重的多,会把不该清除的对象清除掉。
出现错标的条件:
只要打破任一条件,就可以解决错标的问题。
解决错标:
将堆内存划分成较小的多个区域,对这些个区域进行跟踪,优先回收 垃圾数量大的区域。
G1也是支持并发收集的,而且吞吐量优于CMS,可以对整个堆进行管理。