JVM
1. Java内存区域详解
线程私有:程序计数器,虚拟机栈,本地方法栈
线程共享的:堆,方法区,直接内存
1.1 各个区域详解
程序计数器
每个线程需要一个计数器记录自己执行到哪一行了。线程之间切换需要保存自己的执行位置,因此必须每个线程有自己的计数器。
生命周期随着线程创建而创建,随着线程结束而死亡。
java虚拟机栈
由一个个栈帧组成,每个栈帧都拥有局部变量表,操作数栈,动态链接,方法出口等信息。
局部变量表存放了编译期可知的各种数据类型,对象引用。
可能出现两种错误,StackOverFlowError
,OutofMemoryError
前者如果不允许虚拟机栈内存动态扩展,就可能因为递归导致栈深度超过当前java虚拟机最大深度的时候抛出异常。
后者是虚拟机栈内存大小可以动态扩展,如果虚拟机栈扩展着扩展着,没空间了,就会报错。
本地方法栈
很类似,但是执行的是本地方法。
堆
存放对象实例,之前是所有对象都存放在堆中,现在有些新技术的产生,栈上分配,导致不是所有对象都在堆中了。
如果某些方法中的对象引用没有被返回或者没有被外面使用,没有逃逸出去,那就直接在栈上分配内存。
垃圾回收主要在堆区域。可细分为老年代和新生代。Eden,FromSurvivor,ToSurvivor。
对象一般在Eden中分配,经历一次GC之后,eden和S0中存活的,放到是s1种,下次就是,eden和s1中存活的,放到s0中,并且每次的GC存活,年龄+1。超过阈值就放入老年代中。
方法区
(现在已经不在永久代,而是在元空间)存储已经被虚拟机加载的类信息,常量,静态变量等等。
常用参数:
-XX:MetaspaceSize=N//元空间的初始大小
-XX:MaxMetaspaceSize=M//最大大小
2 虚拟机对象
2.1 对象的创建
- 类加载检查:遇到一条new指令,首先检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析、初始化过。如果没有就要先执行相应的类加载过程。
加载过程如下图,下一章节详细说。
- 分配内存:类加载通过以后,虚拟机为新生对象分配内存,对象所需要的内存大小在类加载完之后就确定了。
- 初始化零值:将分配的内存空间,都初始化为0值
- 设置对象头:对象头存放着对象的hashcode,元数据信息,GC分代年龄之类的。
- 执行init方法:此时从jvm的角度看,一个新的对象已经产生了,但是从java的角度看,所有字段都还是0.所以接着执行init方法,把对象按照程序员的意愿初始化,这样真正的对象才算产生出来。
2.2 类加载过程
加载
- 通过全类名获取类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口
验证
准备
正式为类变量分配内存并设置类变量初始值。这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。而类变量在方法区中。
初始值通常为默认的0,0L,null,false等。public static int value = 11;实际上此时为0,final的话为11.
解析
虚拟机将常量池中的符号引用替换为直接引用的过程。解析动作主要针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符七类符号引用进行。
符号引用;一组符号描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量、句柄等等。
举例:jvm给每个类准备了一个方法表,存放类的所有方法。不同的方法肯定在表中偏移量不同,然后调用一个方法的时候,只要知道这个方法在表中的偏移量就可以直接调用。所以解析过程就是把原本的字面量的符号引用,替换为了直接引用。得到了类、字段、方法在内存中的指针或者偏移量。
初始化
类加载的最后一步,执行初始化方法clinit()
的过程。
只有主动使用类才初始化类
1.遇到new、getstatic、putstatic、invokestatic这四条字节码指令时。
即程序创建一个类的实例对象。
程序访问类的静态变量。
给类的静态变量赋值。
调用类的静态方法。
这些时候初始化,上文的public static int value = 11;会从0变成11
2.使用java.lang.reflect包的方法对类进行反射调用时,如Class.forName("...").newInstance()
如果此时没有初始化,则需要初始化。
3.初始化一个类,如果父类还没初始化,则需要先初始化父类。如果实现了某个接口,这个接口有默认方法。接口要先初始化。
4.jvm启动时,用户需要定义一个要执行的主类,也就是包含main的那个类,虚拟机要先初始化。
5.轻量级反射啥啥的,看不懂。这个直接copy:
MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类。
卸载
该类的class对象被GC
三个要求:
1.该类的所有实例对象都已经被GC,堆中不存在该类的实例对象。
2.该类没有在其他任何地方被引用。
3.该类的类加载器的实例已经被GC。
在JVM生命周期类,jdk自带的BootstrapClassLoader,ExtClassLoader,AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收,它们加载的类也不会被卸载。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
2.3 对象的内存布局
对象在内存中有三块区域:对象头,实例数据,对齐填充。
对象头:
- 用于存储对象自身的运行时数据(哈希码,GC分代年龄,锁状态标志等等)
- 类型指针,对象指向类的元数据的指针,通过这个指针确定这个对象是哪个类的实例。
实例数据:
程序中定义的各种类型的字段内容。
对其填充:
不必然存在,只是为了凑齐8字节的整数倍
3.JVM垃圾回收
MinorGC一直重复这个过程:eden+from区->to区。
to区填满之后,所有对象移步老年代。
3.1 GC分类
部分收集
新生代收集:MinorGC/YoungGC
老年代收集:MajorGC/OldGC
混合收集:MinorGC+部分老年带回收
整堆收集
FullGC
3.2 怎么判断对象已经死亡?
引用计数法
有地方引用它,就+1,引用失效就减1.但是很难解决循环引用的问题。一般不使用这种。
可达性分析
GCRoot对象
- 比如虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区静态属性、静态常量引用的对象
- 所有被同步锁持有的对象
3.3 垃圾收集算法
- 标记清除
- 复制算法
- 标记整理
- 分代收集
标记清除 产生大量碎片
标记复制 空间只能使用一半
标记整理 需要大量移动
分代收集
新生代:主要采用复制算法,但是不是分为两块。而是eden+from->to
老年代:标记清除/标记整理。
3.4 类文件结构
- 访问标志:是不是public的?是不是abstract的?是不是final的?是不是enum的?是不是接口?
- 索引信息:当前类、父类、接口数量、接口信息
- 字段表集合:描述类中声明的变量,包括静态变量和实例变量,不包括方法的局部变量。
- 方法表集合:描述类声明的方法。
- 属性表集合:上述表运用到的信息。
3.5 类加载方式
所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存。
JVM内置三个classLoader:
- BootstrapClassLoader:加载javahome/lib目录下的jar包
- ExtentionClassLoader: 加载jrehome/lib/ext 下的jar包,或者被java.ext.dirs系统变量指定的路径下的jar包
- AppClassloader:面向用户的加载器,加载当前应用classpath下的jar包和类
双亲委派模型
好处:保证了java程序的稳定运行。避免类的重复加载,也保证了java的核心API不被篡改。
3.6 JVM常用参数设置
Xms2G -Xmx5G //最小2G最大5GB的堆内存大小。
-XX:NewSize=256m //为新生代分配最小256m的内存
-XX:MaxNewSize=1024m//最大1024M的新生代内存
-Xmn256m//新生代分配256m的内存,最小最大一致的讲话写法
-XX:NewRatio=1 // 新生代与老年代1:1
-XX:MetaspaceSize=N //元空间的初始大小
-XX:MaxMetaspaceSize=M //元空间的最大大小
-XX:=UseSerialGC //使用SerialGC
-XX:+UseParallelGC
-XX:+USeParNewGC
-XX:+UseG1GC