Java -- 虚拟机阅读记录

1、运行时数据区域

  • Java 堆
    在虚拟机启动时创建,存储对象实例和数组。是垃圾回收的主要区域,一般分为 OLD、YOUNG(Eden,S0,S1)区域,进一步的划分都是为了更好的回收内存。逻辑上是连续的,但是可以处在物理上不连续的内存空间中。对象的组成包括对象头、实例数据、对齐填充(不满8的整数倍字节进行填充)。

  • Java对象布局中有锁相关的信息,也就是说synchronized关键字实际上锁的是对象。对象头中运行时数据的大小为JVM的一个WORD大小,即32位JVM的为32位,64位JVM为64位,为了存储更多的信息,最后的lock两位为标志位,分别表示无锁、偏向锁、轻量级锁、重量级锁、GC标记。其中lock和biased_lock一起表示是否有锁。age字段为4位,最大值为16,也就是minor GC的时候,S0和S1循环累加的最大值,超过就会进入到老年代。
  • 无锁情况:存储25位的对象hashcode码,调用了hashcode方法进行计算之后才会存储进去;偏向锁情况:thread为持有偏向锁的线程ID,epoch为偏向时间戳;轻量级锁情况:指向栈中锁记录的指针;重量级锁:指向管程Monitor的指针,也就是常用的synchronized锁。
  • synchronized加锁的方式:同步实例方法,锁的是对象实例;同步类方法,锁的是class实;同步代码块,锁的是括号里面的对象,锁住的是所有以该对象为锁的代码块;相比JUC包中的代码,区别在于synchronized控制的代码在没有多线程的情况下也无法解除,造成性能低下,所以后期synchronized进行了升级,也就是锁膨胀。
  • 1.5之前使用操作系统进行加锁,synchronized核心组件:
1、waitSet
调用wait方法后被阻塞的线程存放的位置

2、contentionList
竞争队列,所有请求锁的线程首先被放置到这里

3、EntryList
contentionList中有资格成为候选者的线程被放置到这里

4、OnDeck
任意时刻,最多只有一个线程正在竞争锁资源,该线程被设置为OnDeck

5、Owner
当前已经获取到锁资源的线程

6、!Owner
当前释放锁的线程


//执行过程
JVM每次从队列的尾部,取出一个线程作为OnDeck,

过程:线程访问代码块的时候,如果当前锁处于重量级锁,直接挂起线程;如果处于轻量级锁,CAS尝试修改锁记录的指针,失败的话自旋几次,如果自旋几次都没有成功,那么当前锁升级为重量级锁,当前线程挂起;如果处于偏向锁,而且不是自己,说明多个线程加入了竞争,那么开始撤销偏向锁,等待原持有偏向锁的线程到达安全点,然后暂停原持有偏向锁的线程,如果此时原线程没有退出,则升级为轻量级锁,唤醒原有暂停的线程从安全点执行。

说明:偏向锁主要是针对一个线程的,表示不需要进行同步操作,只需要简单的判断偏向锁的标志。此时如果有两个线程来竞争,就会升级为轻量级锁了。

1、依赖
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>


2、类测试
package com.vim.modules.web.controller;

import org.openjdk.jol.info.ClassLayout;

public class Test {

    private boolean flag = false;

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new Test()).toPrintable());
    }
}

3、测试结果
com.vim.modules.web.controller.Test object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           9f 20 ed 27 (10011111 00100000 11101101 00100111) (669851807)
     12     1   boolean Test.flag                                 false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

4、分析结果
8个字节的对象头+4个字节的类元空间指针+1个字节的实例数据+3个字节的8倍填充

 

public Test{
    public static void main(String[] args){
        //test变量存储在main栈帧的局部变量表中,指向的对象存储在堆中
        Test test = new Test(); 
        //和test一样,指向的是同一个类元信息    
        Test test2 = new Test();
    }   
}
  • 方法区,也叫元空间
    存储已被虚拟机加载的类信息、常量、静态变量,也常被称为永久代,也可以理解为class文件在内存中的存放位置。是在类加载的时候,通过类的全限定名称,去读取类的二进制字节流(各种加载方式),将其转化为方法区的运行时数据结构存储起来。
    1)Class文件常量池,属于非运行常量池,在编译阶段就已经确定。主要有两种:字面量和符号引用量,如class类全限定名、方法名。在执行的时候会解析这些符号,转化为符号对应的直接引用,也就是指令码,而运行时常量池,是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
          
System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false
System.out.println(s4 == s5);  // false
System.out.println(s1 == s6);  // true

解析:
1、s1 == s2,是由于在编译期间,Hello字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。
2、s1 == s3,在编译期间进行了优化合并,所以也成立。
3、s1 == s4,在运行期间才能确定
4、s1 == s9,由于s9是两个变量拼接而成,但是编译期间无法确定,不能做优化。
5、s4 == s5,两者都在堆中
6、s1 == s6,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等
  • 本地方法栈
    为虚拟机使用到的 Native 方法服务

  • 程序计数器 (PC register)
    根据这个计数器的值来选取下一条需要执行的字节码的指令,由于在任何一个确定的时刻,一个处理器都只有一个线程在工作,所以为了在线程切换后能够恢复到确定的位置。

  • 虚拟机栈(Stacks)
    每一个线程对应一个虚拟机栈,生命周期和线程相同,描述的是Java方法执行的内存模型。每一个方法执行时,会创建一个栈帧,压入到栈中,执行完成后会出栈。
    栈帧在执行时,会存在局部变量表、操作数栈、动态链接、方法出口等信息,其中局部变量表中存放了编译器内可知的各种数据类型,包括对象引用的内存地址。局部变量表所需要的空间,在编译期间就已经分配确定完毕,在执行方法的时候,已经将方法下面的一条指令地址存储到方法出口中,以便执行返回,动态链接中放置的是对应的方法在类元信息中的地址。以下实例演示: javap -c Test.class 反编译

public class Test {

	public static int test(){
		int a=3;
		int b=4;
		int c = a + b;
		return c;
	}

	public static void main(String[] args){
		test();
	}
}
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":
()V
       4: return

  public static int test();
    Code:
       0: iconst_3    //将常量2压入栈中
       1: istore_0    //将栈中的常量2取出,放入到局部变量表0位置
       2: iconst_4    //将常量4压入栈中
       3: istore_1    //将栈中的常量4取出,放入到局部变量表1位置
       4: iload_0     //将局部变量表中0位置取出,放入栈中
       5: iload_1     //将局部变量表中1位置取出,放入栈中
       6: iadd        //将栈顶两int型数值相加并将结果压入栈顶
       7: istore_2    //将栈中的结果取出,放入到局部变量表2位置
       8: iload_2     //将局部变量表中2位置取出,放入栈中
       9: ireturn     //返回结果

//备注,istore 放到局部变量表 和 iload 从局部变量表取出

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #2                  // Method test:()I
       3: pop
       4: return
}

2、对象创建过程

  • 创建的几种方式

package com.vim.modules.web.controller;

import com.vim.modules.web.model.User;

public class Test {

    public static void main(String[] args) throws Exception{
        //使用new关键字
        User user1 = new User();

        //使用Class类的newInstance方法
        User user2 = (User)Class.forName("com.vim.modules.web.model.User").newInstance();
        User user3 = User.class.newInstance();

        //使用Constructor类的newInstance方法
        User user4 = User.class.getConstructor().newInstance();
    }
}
  • 初始化的触发条件
    new 实例化对象、读取或设置一个类的静态字段、调用类的静态方法、反射,这几种情况,如果类没有进行初始化,会出发初始化的操作;当父类没有初始化时,先初始化父类;

  • 非触发条件情况
    读取类的静态字段,该字段被final修饰;通过数组定义来引用类;

3、垃圾回收机制

  • 垃圾回收的区域主要是在堆,分为老年代、新生代(Eden、S0、S1),S0和S1属于Survivor区域,新创建的对象都会放置到Eden区域,但是该区域最终会满,会触发minor GC,会将该区域无效的对象进行垃圾收集。会将Eden部分存活的对象放置到S0区域,S0满了,会将存活的放置到S1中,S0和S1之间循环的放置会不断的加年龄,到达了一定的次数,就会被移动到老年代。当老年代放置满了,就会触发full GC,通过jvisualvm命令可以看到动态的分配过程。

  • 对象无用的条件是没有指针指向这些对象。可达性分析算法:通过一系列“GC root”的对象作为起点,从这些节点开始向下搜索,节点走过的路径称为引用链,当一个对象到GC root没有任何引用链的话,则证明此对象是不可用的。还有一种方式是引用计数法,为每一个对象创建一个引用计数,引用时加1,释放引用时减1,到达0时可以被回收,但是会存在循环引用的问题。
  • 垃圾回收算法:标记清除,标记无用对象,然后进行清除回收,效率不高,无法解决碎片问题;标记整理,标记无用对象,让所有的存活对象往一边移动,然后清除边界外的内存;复制算法,划分两个相同大小的区域,当一块用完的时候,将获得对象复制到另一块上,然后把已使用的空间一次性的清理掉。

标记清除

标记整理

  • 回收算法

4、双亲委派模型

  • 如果一个类加载器收到了类加载的请求,它自己并不会去尝试加载这个类,而是交由自己的父类加载器去加载,一直递归到顶层。只有父类无法加载到该类时,自己才会去尝试加载这个类。
package com.vim.modules.web.controller;

import java.net.URL;
import java.net.URLClassLoader;

public class Test {

    public static void main(String[] args) throws Exception{
        //启动类加载器,sun.boot.class.path
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();

        //扩展类加载器,java.ext.dirs
        urls = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();

        //应用类加载器
        urls = ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs();
        for(URL url : urls){
            System.out.println(url);
        }
    }
}
  • 类加载阶段分为加载、连接、初始化过程,而加载阶段,需要通过类的全限定名去获取类的二进制字节流。对于任何一个类,都需要由它的类加载器和这个类本身一同确立在JVM中的唯一性。每一个类加载器都有一个独立的类名称空间。
  • 双亲委派的好处是,使类有了层次划分,父类已经加载的类,子类就不需要加载了,同时java核心库中定义的class不会被任意替换,保证了安全性。
  • 自定义类加载器
package com.vim.modules.web.controller;

public class CustomClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) {
        //加载class字节流
        return null;
    }
}
  • 破坏双亲委派模式,原因是有时候需要使用当前类加载器的子类加载器去加载,但是双亲委派模式单向的,可以使用Thread类的setContextClassLoader方法去设置上下文类加载器,没有设置的话默认是系统类加载器。

 

 

 

发布了100 篇原创文章 · 获赞 20 · 访问量 1万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章