Java进阶---JVM
JVM概述
JVM作用:
负责将字节码翻译为机器码,管理运行时内存
JVM整体组成部分:
类加载系统(ClasLoader):负责将硬盘上的字节码文件加载到内存中
运行时数据区(RuntimeData Area):负责存储运行时各种数据
执行引擎(Execution Engine):负责将字节码转为机器码
本地方法接口(Native Interface):负责调用本地方法(非Java的方法)
垃圾回收(重点)
类加载系统
作用
负责将硬盘上的字节码文件加载到内存中(运行时数据区中)
类什么时候被加载
1.在一个类写一个main方法,运行main方法
2.new某个类对象时
3.使用类中的静态成员
4.使用反射机制时
public class Hello {final static int num = 10;static int num1= 10;/*静态代码块在类被加载时自动执行,目前可以看做一个类只被加载一次*/static {System.out.println("类被加载了");}public static void main(String[] args) {System.out.println("111111111");}
}public class TestHello {public static void main(String[] args) throws ClassNotFoundException {//new Hello();//System.out.println(Hello.num);// Class.forName("com.ffyc.javapro.jvm.classloader.Hello");/*创建的是数组对象,数组是Hello类型*/Hello [] hellos = new Hello[10];//只是访问类中的静态常量,类是不加载的,直接返回静态常量值即可System.out.println(Hello.num);//System.out.println(Hello.num1);}
}
类加载的过程(了解)
加载阶段:以字节流形式读取文件
连接阶段:验证 准备 解析
初始化阶段:主要为静态成员变量初始化赋值
类加载器
类加载器就是负责加载类的实践者
不同的类,是由不同的类加载器加载的
类加载器的分类:
启动类加载器(引导类加载器),不是用Java语言写的,而是用C/C++写的,负责加载虚拟机核心的类库
扩展类加载器,是用Java语言写的,负责加载jre/lib/ext目录下的类
应用程序类加载器,是用Java语言写的,负责加载程序员写的项目中的类(target/class)
双亲委派机制
当收到类加载任务时,首先委派给上级的类加载器加载,如果上级类加载器还有父级,依次递归,直到最顶级的启动类加载器,当父级类加载器找到类时,成功返回。
如果找不到,就要委派给子类加载器,如果子类加载器找到后,成功返回。
如果均未找到,那么就输出ClassNotFoundException
为什么设计双亲委派机制
为了安全,避免了自己定义的类,替换了系统中的核心类
例如:自己创建java.lang.String,结果还是加载的系统中的String类
如何打破双亲委派机制
可以自定义类加载器
写一个类 继承ClassLoader类,
重写findClass();
自己用流将字节码读入
Class<?> clazz = defineClass(null, bytes, 0, bytes.length);
Object o = clazz.newInstance();//反射机制创建对象//com.ffyc.javapro.jvm.classloader.MyClassLoader@1b6d3586
System.out.println(clazz.getClassLoader());
运行时数据区
当类加载系统把类信息加载到内存后,存储到运行时数据区
运行时数据区,根据不同的功能可以分为5个部分:
程序计数器
作用:程序计数器用来记录线程执行的指令集的位置,因为线程在执行时cpu要进行切换执行,需要记录线程执行的位置
特点:
1.是运行时数据区中空间最小的,运行速度最快的区域
2.每个线程都有一个属于自己的程序计数器,是线程私有的,程序计数器生命周期与线程生命周期相同
3.程序计数器是运行时数据区中唯一一个不会有内存异常情况的区域
虚拟机栈
虚拟栈是运行单位,管理程序如何执行,调用一个方法,方法入栈执行,运行结束后,出栈.
虚拟机栈主要用来运行java语言写的方法.
特点:
线程私有的,每个线程中调用的方法都在线程对应的虚拟机栈中执行.
栈中存储局部变量
虚拟栈中不存在垃圾回收
虚拟机栈中会存在内存溢出问题(递归调用太深)
Exception in thread "main" java.lang.StackOverflowError 栈溢出错误
A线程中的方法不能调用B线程中的方法
先进后出
public void test(){int a = 10;//局部变量int b= 20;String s = new String(); //s是引用类型,保存的是对象地址
}
当一个方法被调用后,被压入到虚拟机栈中称为一个栈帧,
栈帧内部结构:
局部变量表(存储局部变量的区域)
操作数栈(操作数栈就是用来计算的区域)
例如
int a= 10,int b=20; //a和b存储在局部变量表中
int c = a+b ;//计算时,把a和b从局部变量表加载到操作数栈运算,把 运算结果赋给c,把c写回到局部变量表
方法返回地址: 记录方法调用的位置,方法执行完成后要回到自己开的位置
本地方法栈
本地方法: 在java程序中,不是用java语言实现的方法, 由底层操作系统提供
使用 native关键修饰的方法,没有方法体
因为java语言属于上层语言(开发上层应用程序),没有权限与底层硬件进行交互(如读取内存数据,读取硬盘数据),
本地方法栈用来执行本地方法的,当程序中调用了本地方法,那么被加载到本地方法栈中运行.
特点:
线程私有的,每个线程都有属于自己的本地方法栈
本地方法栈也会出现内存溢出情况
本地方法栈中不会出现垃圾回收
堆
概述
作用: 堆空间是用来存储java中创建的对象的
特点: 堆空间是运行时数据区中最大的一块内存空间,
还可以根据需要通过参数设置大小: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内大小
堆空间是所有线程共享的.
堆空间会出现内存溢出情况的.
堆空间是垃圾回收的重点区域.
堆内存区域划分
新生代(区):
伊甸园区
幸存者0(from)
幸存者1(to)
老年代(区):
为什么要分区(代)
根据对象的存活周期,对象的大小放在不同的区域,不同的区域可以采用不同的垃圾回收算法.
会频繁的回收新生代, 相对较少回收老年代.
可以对回收算法扬长避短.
对象创建内存分配过程
1.新创建的对象都存储在伊甸园区(比较大的对象,可以直接分配到老年代)
2.当下次垃圾回收到来时,把伊甸园区存活的对象移动到幸存者0区,清空伊甸园区
3.当下次垃圾回收时,把伊甸园区中存活的对象和幸存者0区的存活对象移动到幸存者1区,清空伊甸园区和幸存者0区.
4.当一个对象经历过最大上限15次垃圾回收后,依然存活,那么将此对象移动到老年代
堆空间参数
涉及JVM调优面试题
根据实际的需要。来调整JVM中原有的一些参数,如堆的初始化大小,分代年龄
官网地址: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial查看所有参数的默认初始值
-Xms:初始堆空间内存
-Xmx:最大堆空间内存
-Xmn:设置新生代的大小
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC处理日志
方法区
概述
方法区主要存放类信息(属性,方法,静态常量...)和编译器编译后的代码,
也是线程共享的区域,
方法的大小也是可以设置的,方法区的大小决定可以加载多少的类,
方法区也是有可能出现内存溢出的。
方法区大小设置
Java 方法区的大小不必是固定的,JVM可以根据应用的需要动态调整.
元数据区大小可以使用参数-XX:MetaspaceSize指定
方法区的垃圾回收
方法区也是有垃圾回收的,方法区的垃圾回收主要回收的是类信息。
类信息回收条件是比较苛刻的:
1.该类所创建的对象已经不再使用,并且被回收了
2.该类的Class对象也不再被使用了
3.加载该类的类加载器也被回收了
线程共享:堆,方法
线程私有的:程序计数器,虚拟机栈,本地方法栈
会出现内存溢出:堆,方法,虚拟机栈,本地方法栈
会出现垃圾回收:堆,方法区
本地方法接口
虚拟机中负责调用本地方法的入口,本地方法运行在本地方法栈中。
什么是本地方法
被native修饰的方法,没有方法体,是操作系统提供的方法
为什么Java中要调用本地方法
Java属于上层应用开发语言,没有权限直接访问计算机硬件(硬盘,内存,外设(喇叭)),需要调用本地操作系统提供的方法。
执行引擎(黑盒)
执行引擎在虚拟机中主要负责将加载到虚拟机中的字节码 解释/翻译 为机器码
.java-----jdk编译--->.class 在开发阶段(前端编译)
.class-----执行引擎编译--->机器码 在运行阶段(后端编译)
什么是解释器?什么是JIT编译器?
解释器/解释执行--->sql,html,css,js,python解释执行 不需要整体编译,由解释器一行一行执行,
解释执行特点:速度慢,不需要花费时间编译
编译执行,先把代码整体进行编译,生成另一种文件格式,
编译执行特点:编译后执行快,但是编译需要花费一定的时间
jvm中的执行引擎在将字节码编译为机器码时,采用半解释,半编译机制。
开始时,可以先采用解释执行,立即投入到翻译工作中,
等到编译器编译完成后,采用编译执行
垃圾回收
Java语言特点
开源,跨平台,面向对象,自动垃圾回收,线程,网络...
概述
什么是垃圾?
一个对象没有被任何引用指向,这个对象就可以被回收,就称为垃圾对象,
垃圾对象如果不及时清理,导致新对象肯可能没有空间存储,进而导致内存溢出(内存不够用了)
早期垃圾回收
早期是手动的回收 C和C++
malloc()
free()
给程序员带来不便,如果忘记释放,造成内存泄漏(对象不再使用,但是还占用着内存)
现在的语言多数采取了自动垃圾回收,例如java
程序员只需要new对象申请内存,不需要自己去释放空间,解放了开发人员
应该关心哪些区域的回收?
重点是堆:频繁回收新生代,较少回收老年代
方法区
内存溢出与内存泄漏
内存溢出:内存空间不足以运行程序,会报出内存错误(out of memory OOM)Error
内存泄漏:一些对象已经不再被使用l,但是垃圾回收器却不能回收的对象,一直悄悄的占用着内存资源。
举例:提供close()方法关闭资源的对象
数据库连接对象,
网络Socket
IO读取文件的对象
垃圾回收相关算法
垃圾标记阶段算法
标记阶段的目的:将堆内存中的对象进行检查,检查对象有没有被引用指向。
标记阶段涉及两个算法:引用计数算法(现在的虚拟机已经不再使用了)
可达性分析算法
引用计数算法:
思想:在每个对象中设置一个字段用来记录引用的数量,有一个引用指向对象,计数器加一,一旦有引用断开,计数器减一;
优缺点:实现思路简单
计数器字段占用空间,加一,减一是需要开销的,
重点是不能解决循环引用问题,A.B.C三个对象之间相互关联引用,此时计数器都是1,但是可能与外界没有联系,外界不可能使用A.B.C这三个对象,垃圾回收器不能回收他们,造成内存泄漏问题。
Hello h1 = new Hello(); 引用计数器0
Hello h2 = h1;
h1 = null;
h2 = null;
可达性分析算法(根搜索算法,追踪性分析算法)
思想:从一些活跃对象开始进行搜索,只要跟根对象有联系的对象,就不是垃圾对象,
与根对象没有任何联系的,即使对象之间存在引用关系,也可以判定为垃圾对象。
解决了引用计数算法中的循环引用问题。
哪些对象可以作为根对象:
1.虚拟机中引用的对象 运行中的方法中引用的对象
2.类中的一些静态成员变量
3.与同步锁有关的对象
4.虚拟机内部的一些类Class类,异常类,类加载器
final finally{ } finalize()
Object类中finalize()
public class Demo{protected void finalize() throws Throwable { Demo a = this;}
}
finalize()在对象被判定为垃圾后,在对象被真正回收之前由虚拟机自动调用,
在finalize()中执行一些最后要执行的功能,
finalize()只被执行一次
由于finalize()存在,对象可以分为3种状态:
可触及的:有引用指向的对象
可复活的:已经被判定为垃圾对象,但是fianlize()方法还没执行过,有可能在finalize()中复活
不可触及的:fianlize()已经被执行过了,并且又判定为垃圾了
垃圾回收阶段算法
标记-复制算法
将内存分为两块,把正在使用中的内存块的存活对象,复制到另一块内存中,从内存块的开始位置摆放,然后清除正在使用的内存块.
对象会被移动, 适合存活对象少,垃圾对象多的场景, 适合新生代的回收.
标记-清除算法
不移动存活对象的,将垃圾对象中的地址记录在一个空闲列表,有新对象到来时,可以把新对象分配到空闲列表中的内存地址上,覆盖垃圾对象.
适合老年代回收,因为存活多且大,不需要移动对象.
标记-压缩算法
将存活对象会压缩到内存的一端,重新排列,将边界外的空间进行清理,以减少内存碎片.
也是适用于老年代
垃圾回收器
标记阶段算法和回收阶段的算法都是方法论,垃圾回收器是真正回收的实践者.
不同的jdk版本中提供不同的垃圾回收器, 不同的版本的jdk中可以由不同开发商实现垃圾回收器.
垃圾收集器分类
从线程数量分: 单线程垃圾收集器,只有一个线程进行垃圾回收
多线程垃圾收集器,有多个线程同时进行垃圾回收
从工作模式分: 独占式垃圾收集器, 当垃圾收集线程执行时,其他程序线程会暂停执行
并发式垃圾收集器, 当垃圾收集线程执行时,可以允许其他程序线程同时执行
从回收的内存空间分: 新生代垃圾收集器
老年代垃圾收集器
jdk8中内置的垃圾回收器
Serial,Serial Old,
ParNew,Parallel Scavenge,Parallel Old,
CMS,
G1
重点了解2款垃圾收集器
CMS(Concurrent Mark Sweep,并发标记清除),这款垃圾收集器首创了垃圾回收线程和其他程序线程同时执行.
初始标记: 垃圾回收线程独占的
并发标记: 用户程序线程和垃圾回收线程同时执行的
重新标记: 垃圾回收线程独占的
并发清理: 用户程序线程和垃圾回收线程同时执行的
重置线程: 用户程序线程和垃圾回收线程同时执行的