JVM
2021-01-12 14:49:57 0 举报
AI智能生成
JVM
作者其他创作
大纲/内容
基础认知
特点
一次编译,到处运行!(write once,run anywhere! )
自动内存分配
自动垃圾回收
产品实现
hotspot(Oracle)
JRocket(之前是sun,后来被Oracle整合)
J9(IBM)
区分
JDK:java开发工具包,包含JRE+各类开发调试的工具
JRE:java运行时环境,包含JVM+java各种核心的api类库
JVM:java虚拟机
jdk下载网址
https://www.oracle.com/java/technologies/oracle-java-archive-downloads.html
类加载过程
加载Loading
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
链接Linking
1.验证(Verify):验证生成的Class文件是否符合java虚拟的规范,(CA FE BA BE)文件格式验证,元数据验证,字节码验证,符号引用验证
2.准备(Prepare)
为类变量(静态变量)分配内存并设置该类变量的默认初始值,即零值
注:这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中
static final修饰的常量,则在此阶段就被赋值为定义的初始值
3.解析(Resolve):是Java虚拟机将常量池内的符号引用替换为直接引用的过程
初始化Initialization
1.初始化阶段就是执行类构造器方法<clinit>( )的过程,为类变量进行自定义初始化
2.该方法不需要显示的进行定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并后生成的
3.虚拟机必须要保证一个类的<clinit>在多线程下同步加锁
类加载器
1.类加载器分为:引导类加载器,自定义类加载器两大类
2.引导类加载器(BootStrap ClassLoader)
是使用c/c++编写的,是不可达的
java中的核心类库都是使用引导类加载器进行加载的:rt.jar、resource.jar、扩展类加载器、系统类加载器、等
3.扩展类加载器(ExtClassLoader)
使用java编写,继承自ClassLoader,是Launcher的内部类,属于自定义加载器
用于加载java类库中的ext.jar中的类信息
4.应用/系统类加载器(AppClassLoader)
使用java编写,继承自ClassLoader,是Launcher的内部类,属于自定义加载器
加载我们在程序中自定义的类
注:自定义类加载器的使用情况
隔离加载类,修改类加载方式,扩展加载源,防止源码泄露
自定义类加载器只需要继承自ClassLoader加载器并重写findClass()即可,也可以直接继承自URLClassLoader即可
双亲委派机制
1.如果一个类加载器收到了被加载的请求时,并不会直接去加载,而是先委派给其父类的加载器去执行
2.如果父类的加载器还存在父类加载器,则继续向上委派,直到请求到顶级的引导类加载器
3.如果父类的加载器可以完成该类的加载请求,则交给父类加载该类;如果父类不能完成此类的加载,则交给子加载器去加载
eg:自定义一个String类进行加载,在加载自定义String类前会先去请求父类加载器,一直请求到BootStrap加载器,发现该加载器可以完成String类的加载,则就由BootStrap ClassLoader加载器完成加载过程,因此并不会加载到我们自定义的String类,而是加载的系统自带的String类
优点:避免类被重复加载,保护程序的安全,防止核心程序被篡改
因为JDK9中引入了模块化系统,将lib/ext目录移除了,扩展类加载器变为了平台类加载器(Platform ClassLoader),双亲委派机制也发生了变化:当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载
常用调优参数
官方参数文档
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
栈
-Xss128K,设置jvm中栈的大小(每个线程私有的空间)
堆
-Xms20m,设置堆的初始化大小(-XX:InitialHeapSize)
-X,jvm的运行参数;ms,memory size
默认堆初始化大小是电脑物理内存大小的1/64
cmd命令查看堆内存使用情况
jps,查看运行的进程号
jstat -gc 进程id
参数查看堆内存使用情况
-XX:+PrintGCDetails
-XX:+PrintFlagsInitial,查看所有的参数的默认初始值
-XX:+PrintFlagsFinal,查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xmx64m,设置堆的最大大小(-XX:MaxHeapSize)
默认堆的最大大小是电脑物理内存的1/4
如果设置堆的初始大小与最大大小相同,则在jvm运行期间堆不会再进行扩展
开发中建议将堆的初始大小与最大大小设置为相同,避免jvm在运行中进行扩容,造成系统资源的占用
-XX:NewRatio=2,设置老年代与新生代的比例
老年代与新生代的默认值比例是2:1
cmd命令查看参数
jps 查看进程号
jinfo -flag NewRatio/SurvivorRatio 进程id
-Xmn50m,设置新生代的空间大小,当设置新生代与老年代的比例,并设置有该参数时,则以该参数为准
-XX:SurvivorRatio=8,设置伊甸园区与幸存者区的比例
默认伊甸园与幸存者区的比例是8:1:1,但可能会有一定的波动范围
存在自适应机制:-XX:-UseAdaptiveSizePolicy,关闭自适应内存分配策略,但是不会起作用
想要比例是8:1:1,要显示的在参数中进行设置
-XX:MaxTenuringThreshold=15,设置对象进入老年代的阈值
系统默认的阈值是15,达到该阈值的对象将晋升到老年代中
某些特大的对象新生代无法存放,也会直接在老年代进行分配
YGC/Minor GC只在Eden空间满的时候触发,幸存区满的时候不会触发GC,YGC发生时会顺便进行幸存区的清理
频繁收集新生代,较少收集老年代,几乎不动永久代/元空间
-XX:HandlePromotionFailure=true,设置空间分配担保
在jdk7开始,该参数由系统进行管理,直接默认是true,不受我们外界管理设置
-XX:+UseTLAB,设置为每个线程分配独立的运行内存区域
该空间分配在Eden内,占Eden空间的1%,系统默认是开启使用该分配策略的
-XX:+DoEscapeAnalysis,使用逃逸分析分配对象
栈上分配:一个对象经过逃逸分析后,没有发生逃逸(只在方法内部进行使用),则该对象可以在栈上进行分配
好处:不用进行垃圾收集处理,节省堆内空间,分配速度较快
在jdk7以及之后,系统默认是开启使用逃逸分析的
-XX:+HeapDumpOnOutOfMemoryError 程序运行中当堆空间发生OOM时会生成dump文件
-XX:+PrintCommandLineFlags 查看命令行相关的参数(包含垃圾回收器)
方法区(永久代/元空间)
-XX:PermSize=20.75m,设置永久代初始化大小(jdk7系统默认是20.75M)
-XX:MaxPermSize=82m,设置永久代最大使用空间大小(jdk7系统默认是82M)
-XX:MetaspaceSize,设置元空间的初始化大小(jdk8系统默认是20.79M)
-XX:MaxMetaspaceSize,设置元空间的最大使用空间大小(jdk8中元空间分配在本地内存,所以由系统的内存来决定)
对象实例化
创建对象的方式
new
调用构造方法创建对象
Class的newInstance( )
反射的方式,只能调用空参的构造器,权限必须是public(在jdk9中标识为已过时)
Constructor的newInstance(xxx)
反射的方式,可以调用空参的或者带参的构造器,权限没有要求
实现Cloneable接口,使用clone( )
使用反序列化
从文件、网络中获取对象的二进制流信息,再反序列化为对象
使用第三方库Objenesis
创建对象的步骤
1.判断对象对应的类是否进行加载、链接、初始化
加载要创建对象的所属的类信息
2.位对象分配内存空间
如果内存空间是规整的
使用(指针碰撞)来进行空间分配,相应的内存指针移动该对象大小的空间
对应的垃圾收集算法是(标记整理算法)Serial 、Parallel
如果内存空间不是规整的
虚拟机内部维护一个列表(空闲列表),使用空闲列表的方式来进行分配
对应的垃圾收集算法可以是(标记清除算法)CMS垃圾收集器
3.处理并发安全问题
使用cas配上失败重试保证原子性
每个线程预先分配一块内存TLAB
4.进行对象属性的默认初始化操作(赋零值)
5.设置对象的对象头信息
6.执行init方法进行显示初始化
为属性设置自定义的属性值
对象内存布局
对象头(Header)
运行时元数据(Mark Word)
哈希值(HashCode)
GC分代年龄计数器
锁状态标志
线程持有的锁
偏向线程id
偏向时间戳
类型指针
指向元空间/方法区中的对象所属的类型,并不是所有的对象都包含有类型指针
说明:如果创建的对象是数组,还需要记录数组的长度
实例数据(Instance Data)
说明
它是对象真正存储的有效信息,包括定义的各种类型字段(包括从父类继承下来的和自己的)
规则
相同宽度的字段总是被分配到一起
父类中定义的变量会出现在子类之前
如果CompactFields参数为true(默认为true):子类的窄变量可能插入到父类变量的空隙
对齐填充(Padding)
不是必须的,也没有特殊的含义,仅仅起到占位符的作用
对象访问定位
句柄池访问
栈中对象的引用指向堆中的句柄池中的地址,句柄池中包含对象实例数据的指针(指向对象实例)与对象类型数据的指针(指向对象在方法区/元空间的对象的类型信息)
优点:实例对象的修改只需要在句柄池中进行修改即可,比较方便
缺点:需要单独的划分句柄池,占用堆中的内存资源
直接指针访问
栈中的引用直接指向堆中的对象实体,对象实体中直接包含有对象实例数据信息,另外包含到对象类型数据的指针(指向方法区/元空间)
优点:指向明确,比较简单
Hotspot虚拟机采用的就是直接指针的方式来进行对象的访问定位的
StringTable
内存分配位置
jdk6以及之前,是存放在方法区中的(字符串常量池),系统默认参数存放是 1009个
jdk7以及之后,是存放在堆中的(字符串常量池),系统默认参数存放是 60013个
jdk8以及之后,可以设置的该参数的最小值是1009个
为什么要调整?
1.永久代的空间较小,默认所存储的字符串较少
2.永久代中垃圾回收的频率较低,回收效益也较小
字符串拼接
1.字面量之间的拼接
String s="a"+"b";
编译优化,在编译为字节码时优化为:String s="ab";存放在字符串常量池中
2.带有变量的拼接
String str="a"; String s=str+"b";
编译优化,具体步骤为
1.新建一个匿名对象StringBuilder,new StringBuilder( );
2.调用append方法进行拼接,append("a").append("b");
3.调用toString( )方法进行字符串转化,也会在堆中创建一个String对象
是在堆中进行对象的存放的
3.带final常量的拼接
等价于字面量之间的拼接,存放在字符串常量池中
创建了几个对象?
1. String s=new String("ab");
创建了2个对象
堆中创建对象s,对应字节码中的 new
字符串常量池中存放常量"ab",对应字节码中的 ldc
2. String s=new String("a")+new String("b");
创建了6个对象
1.在堆中创建StringBuilder对象,new StringBuilder( )
2.在堆中创建String对象,new String("a")
3.在字符串常量池中添加常量a
4.在堆中创建String对象,new String("b")
5.在字符串常量池中添加常量b
6.调用StringBuilder的toString方法,在堆中创建对象s
注:StringBuilder的toString方法并没有在常量池中添加常量
class文件结构
官网:https://docs.oracle.com/javase/specs/index.html
1.模数
class文件开头的四个字节(U4)的内容用来保存魔数值(CAFEBABE),用于表示是否是java虚拟机所能识别的文件
2.class文件版本
主版本+副版本,各占用两个字节的长度,java1.0版本对应的是45,java8.0对应52,高版本的虚拟机可以执行低版本的class文件
3.常量池
常量池计数器:占用两个字节的内容,用来记录常量池表的容量
常量池表:存放字面量和符号引用,存放‘常量池计数器-1’个,索引下标从:1~常量池计数器-1,0索引位置用于存放无引用的特殊情况
4.访问标志
访问标志(access_flags),2个字节;用于识别一些类或者接口层次的访问信息
包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract 类型;如果是类的话,是否被声明为final;等等
5.类索引、父类索引、接口索引集合
6.字段表集合
7.方法表集合
8.属性表集合
0 条评论
下一页