JVM 是Java平台的基石,JVM是一个抽象的计算机,符合计算机的体系模型。
JVM中对象的创建过程
本章节主要是学习JVM是如何创建对象?
如下代码:
public class ObjectCreate {
private int age;
private boolean isKing;
public static void main(String[] args) {
//JVM 遇到 new 指令
// 检查加载
// 分配内存
//1. 内存空间连续 指针碰撞
//2. 内存空间不连续 jvm维护空闲列表 根据空闲列表分配
//3. 并发安全问题:CAS加失败重试,本地线程分配缓冲(TLAB Thread Local Allocation Buffer) 每个线程在Eden区
//单独的把某一块内存区域划分给该线程,所以效率会更高一些
ObjectCreate objectCreate = new ObjectCreate();
System.out.println(objectCreate.age);
System.out.println(objectCreate.isKing);
}
}
一个对象的创建过程如下:
- 检查加载:首先要检查
ObjectCreate类对应的符号引用,检查这个类是否被加载过。 - 分配内存:在堆空间划分内存,解决并发安全问题。
- 划分内存的方式
- 指针碰撞:堆内存是连续规整的,直接按照顺序分配内存即可
- 空闲列表:堆内存是不连续的不规整的,这时候JVM会有一个空闲列表,进行分配空间,例如下面打叉的是空闲的,打对勾的是占用的,
- 指针碰撞:堆内存是连续规整的,直接按照顺序分配内存即可
- 并发安全问题:多个线程在分配内存安全性问题的解决
- CAS 加失败重试:当线程1在分配内存时,会先读取当前值的old,然后经过预处理,CSA会通过实时值与old进行比较,如果相等则分配内存,如果不相等,则说明已经被其他线程访问过,那么线程1就会再来一次。
- 分配缓冲:本地线程分配缓冲(TLAB Thread Local Allocation Buffer) 每个线程在Eden区,单独的把某一块内存区域划分给该线程,所以效率会更高一些,但是会受制于Eden区的大小。
XX:+UseTLAB 默认是开启的
- CAS 加失败重试:当线程1在分配内存时,会先读取当前值的old,然后经过预处理,CSA会通过实时值与old进行比较,如果相等则分配内存,如果不相等,则说明已经被其他线程访问过,那么线程1就会再来一次。
- 划分内存的方式
- 内存空间初始化:(注意:不是对象的构造方法) “零”值 如:int值为0 Boolean为false age=0 isKing=false
- 设置:设置对象头
- 对象初始化:构造方法
上述,就是创建一个对象的过程,当然这个过程还是比较复杂的,需要一些时间去了解和掌握
对象的内存布局及访问
对象占据的内存空间应该是8字节的整数,如果不是8字节的整数,则需要有对齐填充,添加剩余的字节,保证对象的空间是8字节的整数。这样有利于分配内存和垃圾回收。
### 对象是如何访问的呢?
在Java中我们通常是在方法中访问的,而方法中的调用对象都是使用对象的引用进行调用,对象的调用有两种方式。
句柄方式访问对象
句柄的方式,就是在Java堆中多了一个中间句柄池,通过操作句柄池来指向对象的实例
句柄方式的好处:稳定性好,为什么呢?例如实例对象销毁了没有了,那么直接修改句柄池,到对象实例数据的指针为null即可,而不用再去修改当前线程正在执行的引用重置reference
而句柄方式的坏处:损耗的性能,由于中间句柄池,而多了一层访问的性能开销。
直接指针访问对象
Hotspot的采用的就是直接指针。也是我们常用的一种方式,节省了中间句柄池的开销,减少了性能的损耗。
直接指针和句柄的区别:
- 句柄:例如实例对象销毁了没有了,那么直接修改句柄池,到对象实例数据的指针为null即可,而不用再去修改当前线程正在执行的引用重置
reference - 直接引用:如果对象实例销毁了,那么就需要修改当前线程正在执行的引用
reference去进行重置。 - 句柄的稳定性好,直接指针性能高
判断对象的存活?
在JVM中是如何判断对象是活的还是死的呢?
C、C++是手动回收内存,Java是自动回收内存
手动释放内存容易出现的问题:
- 忘记回收-内存泄漏
- 多次回收 - malloc free free
没有任何引用指向的一个对象或者多个对象(循环引用)就是垃圾。
JVM虚拟机的中的判断有两种方式:
- 引用计数法
A对象引用B对象,那么B对象的计数+1. 但是引用计数法无法解决循环引用的问题。
- 可达性分析(根可达)
可达性分析又叫根可达,只要对象的引用有根,也就是根可达那么就不能回收,如果对象没有根就可以进行回收。这里可能比较难以理解,看下图所示:
如上图所示:可达性分析,很简单的解决了循环引用的问题。循环引用没有根,那么就是垃圾回收的对象。
那么GC roots哪些可以作为根呢?
作为GC roots 的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
- JVM的内部引用(class对象、异常对象NullPointException、OutofMemoryError,系统类加载器)
- 所有被同步锁(synchronized 关键字)持有的对象
- JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等
- JVM实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代的对象)
public class Isalive {
public Object instance = null;
//占据内存,便于判断分析GC
private byte[] bigSize = new byte[10 * 1024 * 1024];
public static void main(String[] args) {
Isalive objectA=new Isalive();//objectA 局部变量表 GCRoots
Isalive objectB = new Isalive();
//互相引用
objectA.instance = objectB;
objectB.instance = objectA;
//切断可达
objectA = null;
objectB = null;
//强制垃圾回收
System.gc();
}
}
如下: 两个对象大约是20多M,回收之后就是536K,也就是说两个对象都被回收掉了
[GC (System.gc()) 24412K->536K(251392K), 0.0008534 secs]
[Full GC (System.gc()) 536K->416K(251392K), 0.0129759 secs]
Class回收条件(方法区)
Class的回收条件比较苛刻,必须同时满足以下的条件,这也是为什么JVM要把方法区单独出来。是和堆区的对象的回收是有很大的区别的。
- 该类所有的实例都已经 被回收,也就是堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 参数控制:
-
- Xnoclassgc : 禁用类的垃圾收集(GC),从而缩短了应用程序运行期间的中断时间。
-
Finalize (不推荐使用,忘掉)
如果对象覆盖了,finalize方法,那么在垃圾收集的时候对象会根据finalize方法进行自我拯救。
/**
* 对象的自我拯救
*/
public class FinalizeGC {
public static FinalizeGC instance = null;
public void isAlive(){
System.out.println("I am still alive!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed");
FinalizeGC.instance = this;
}
public static void main(String[] args) throws InterruptedException {
instance = new FinalizeGC();
//对象进行第一次GC
instance = null;
System.gc();
Thread.sleep(1000); //Finalize 方法优先级很低,需要等待
if (instance!=null){
instance.isAlive();
}else {
System.out.println("I am dead");
}
//对象进行第二次GC
instance = null;
System.gc();
Thread.sleep(1000);
if (instance != null){
instance.isAlive();
}else {
System.out.println("I am dead");
}
}
}
运行结果如下:
finalize method executed
I am still alive!
I am dead -- 第二次GC不能在自我拯救了 finalize 不能执行第二次
Java中比如try-finally里可以更好的代替finalize
各种引用
- 强引用 强引用就是=的关系
- 软引用 SoftReference :
当系统即将发生OOM之前会对软引用进行回收,如果空间足够了就不会抛出OOM异常,如果空间还是不够则还会抛出OOM异常。
如下代码:
/**
* -Xms20m -Xmx20m
*/
public class TestSoftRef {
public static class User{
public int id = 0;
public String name = "";
public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public static void main(String[] args) {
User u = new User(1,"jake");//new 是强引用
SoftReference<User> userSoft = new SoftReference<>(u);//软引用
u = null;//干掉强引用,确保这个实例只有userSoft软引用
System.gc();//进行一次GC垃圾回收,
System.out.println("After GC");
System.out.println(userSoft.get());//对象还存活 User{id=1, name='jake'}
//往堆中填充数据,导致OOM
List<byte[]> list = new LinkedList<>();
try {
for (int i = 0; i < 100; i++) {
list.add(new byte[1024*1024*1]);
}
} catch (Throwable e) {
System.out.println("抛出异常时,打印软引用对象:"+userSoft.get());//抛出异常时,打印软引用对象:null
}
}
}
那么什么情况下会用到软引用呢?一般是用到缓存中,但是这部分的内存会占用很大的空间,一旦系统即将OOM则回收软引用,可以预防内存溢出。
- 弱引用 WeakReference
弱引用只要发生垃圾回收,那么弱引用就会干掉。弱引用也是用在数据缓存
User u = new User(1,"jake");//new 是强引用
WeakReference<User> userSoft = new WeakReference<>(u);//弱引用
u = null;//干掉强引用,确保这个实例只有userSoft弱引用
System.out.println(userSoft.get());//User{id=1, name='jake'}
System.gc();//进行一次GC垃圾回收,
System.out.println("After GC");
System.out.println(userSoft.get());//null
- 虚引用 PhantomReference
虚引用了解即可,在真实项目中几乎不会用到。虚引用会随时被垃圾回收器回收,主要就是监听垃圾回收器的工作是否正常。
对象的分配策略
- 对象的分配原则
- 对象优先在Eden分配(几乎所有的对象都在堆空间分配)
- 空间分配担保:先回收新生代MinorGC, 判断是不是老年代能够放下,如果不能放下就触发MajorGC/FullGC->MinorGC
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态对象年龄判定 : 相同年龄所有对象的大小,大于from区/to区的一半,则全部进入老年代
新生代GC:MinorGC
老年代GC:MajorGC
全部GC:FullGC
注意:永久代是方法区。垃圾回收是在哪一个区域满了就自动进行垃圾回收
如下图所示,是对象的一个完整的分配策略
虚拟机的优化技术:
- 逃逸分析:如果存在逃逸则将对象分配在栈中。
逃逸分析 (不会逃逸出方法) 分配在栈上面 以避免没有必要的垃圾回收
运行如下代码:添加VM options -XX:-DoEscapeAnalysis -XX:+PrintGC
/**
* 逃逸分析
* -XX:-DoEscapeAnalysis : 关闭逃逸分析
* -XX:-DoEscapeAnalysis -XX:+PrintGC - 执行速度:374 ms
* 没有关闭逃逸分析:执行速度:5ms
*/
public class EscapeAnalysisTest {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < 50000000; i++) {//5000万次 5000万个对象
allocate();
}
System.out.println((System.currentTimeMillis()-start)+" ms");
Thread.sleep(600000);
}
static void allocate(){//逃逸分析 (不会逃逸出方法) 分配在栈上面 以避免没有必要的垃圾回收
//这个myObject引用没有出去,也没有其他方法使用
MyObject myObject = new MyObject(2020,2020.6);
}
static class MyObject{
int a;
double b;
public MyObject(int a, double b) {
this.a = a;
this.b = b;
}
}
}
我们关闭逃逸分析,运行结果:
[GC (Allocation Failure) 65536K->520K(251392K), 0.0015100 secs]
[GC (Allocation Failure) 66056K->456K(251392K), 0.0020627 secs]
[GC (Allocation Failure) 65992K->440K(251392K), 0.0034059 secs]
[GC (Allocation Failure) 65976K->472K(316928K), 0.0008799 secs]
[GC (Allocation Failure) 131544K->456K(316928K), 0.0016654 secs]
[GC (Allocation Failure) 131528K->456K(437760K), 0.0092627 secs]
[GC (Allocation Failure) 262600K->425K(437760K), 0.0017700 secs]
[GC (Allocation Failure) 262569K->425K(700416K), 0.0004432 secs]
374 ms
下面我们再来看打开逃逸分析执行速度:
6 ms
从上面看两种结果,在逃逸分析开启的时候,运行速度是6ms 可以推测出逃逸分析,将没有引用出去的对象分配在栈中。
- 本地线程分配缓冲