java对象内存布局和对象定位

发布于:2022-11-09 ⋅ 阅读:(7) ⋅ 点赞:(0) ⋅ 评论:(0)

观察对象在内存中的存储布局

利用java agent

(class文件到内存之间的代理,这个代理得自己去实现)的机制。

import java.lang.instrument.Instrumentation;

/**
 * @author zhousong
 * @ClassName 打印对象内存布局类
 * @description:
 * 1.在src目录下创建
     * Manifest-Version: 1.0
     * Created-By: mashibing.com
     * Premain-Class: java.lang.instrument.Instrumentation.打印对象内存布局类
 * 2.打jar包,在需要使用该Agent Jar的项目中引入该Jar包
 * 3.运行时候加上虚拟机参数-javaagent:d:\toker\打印对象内存布局类.jar
 * 4.使用的时候-打印对象内存布局类.sizeOf(new Object()));即可
 * @datetime 2022年 11月 07日 15:31
 * @version: 1.0
 */
public class 打印对象内存布局类 {
    private static Instrumentation inst;
    static void premain(String agentArgs,Instrumentation _inst){
        inst=_inst;
    }
    static long sizeOf(Object o){
        return inst.getObjectSize(o);
    }
}

利用JOL工具

       /**
         *# Running 64-bit HotSpot VM.
         * # Using compressed oop with 3-bit shift.
         * # Using compressed klass with 3-bit shift.
         * # Objects are 8 bytes aligned.: 8字节对齐,不足就补充字节数
         * # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
         * # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
         */
        System.out.println(VM.current().details());

        /**
         *com.toker.cloud.concurrent.jvm.testcase.ObjectJVMCase1 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)                           81 22 01 f8 (10000001 00100010 00000001 11111000) (-134143359)
         *      12     4        (loss due to the next object alignment)
         * Instance size: 16 bytes
         * Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
         *
         * 通过第一个print的结果可以看到vm的对象是有8字节对齐的限制,在这里空对象是4+4=4=12个字节,没有向8个字节对齐
         * 因此最后补充了4个字节。因此可以看到Space losses空间损失了4个字节。  对象总大小为12+4=16个字节大小
         *<!-- JOL 工具开始:研究对象的内存结构-->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>${jol.version}</version>
</dependency>

<!-- JOL 工具结束:研究对象的内存结构 -->

package com.toker.cloud.concurrent.jvm;
import com.toker.cloud.concurrent.jvm.testcase.对象内存布局测试类1;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

/**
 * @author zhousong
 * @ClassName 对象内存布局 JOL: java object layout,用来观察java对象
 *       <!-- JOL 工具开始:研究对象的内存结构-->
 *             <dependency>
 *                 <groupId>org.openjdk.jol</groupId>
 *                 <artifactId>jol-core</artifactId>
 *                 <version>${jol.version}</version>
 *             </dependency>
 *     ClassLayout.parseInstance(t).toPrintable();
 * @description: 对象在内存中的基本组成
 * @datetime 2022年 11月 02日 17:21
 * @version: 1.0
 */
public class 对象内存布局 {
    public static void main(String[] args) {
         */
        对象内存布局测试类1 objectJVMCase1 = new 对象内存布局测试类1();
        System.out.println(ClassLayout.parseInstance(objectJVMCase1).toPrintable());

    }
}

Java对象定位

《Java虚拟机规范》中只定义了reference用来指向一个对象引用,并没有规定这个引用应该通过什么方式定位,如何去实现。reference数据存储在栈中引用堆中的对象,主流的实现方式有两种,一种是直接寻址,一种是间接寻址(句柄池).

直接指针寻址

直接寻址就是reference中直接存储的就是堆中对象的地址,如果不止访问对象本身,还需要访问对象类型Class数据就再加一次指针定位的开销,直接寻址的好处,它的速度快,相比于句柄访问(间接寻址)节省了还要在堆中通过句柄吃寻址的这一次寻址的开销,而且java程序运行过程中,访问对象是一件极其频繁的操作,省去的这一次寻址也是一项非常可观的执行成本。HotSpot虚拟机就是采用这种方式进行对象的访问。

间接寻址

java堆中划分出一块区域作为句柄池,reference中存储的就是句柄池的地址,句柄池中存储了对象的实例数据与类型数据的地址信息。使用间接定位对象的方式也有优点,堆是垃圾收集器工作的主要区域,在这里会进行频繁的垃圾收集操作,在进行垃圾收集时会涉及大量的对象移动,在频繁的移动对象时,只需要修改句柄的指向实例数据的指针即可,不用频繁的更改栈中的reference变量的值。

JIT(Just In-Time Compiler)

JVM是采用默认混合模式编译代码的,所以说java不能单纯说是解释型语言还是编译型语言

/**
 * @author zhousong
 * @ClassName JVM混合模式验证
 * @description: 默认混合模式,所以说java不能单纯说是解释型语言还是编译型语言
 * -Xmixed:默认为混合模式,启动速度较快,对热点代码实行检测和编译
 * -Xint:使用解释模式,通过bytecode intepreter解释器执行,启动很快,执行稍慢
 * -Xcomp:使用纯编译模式,通过JIT(Just In-Time Compiler)编译器,执行很快,启动很慢(很多类库的时候) * 
 * @datetime 2022年 11月 02日 16:34
 * @version: 1.0
 */
public class JVM混合模式验证 {

    /**
     *1、什么时候会用JIT编译成本地代码呢?
     *      刚刚开始是用解释器执行,执行过程中有某一段代码执行的频率特别高(1s中执行几十万次),
     *      JVM就会把这段代码编译成本地代码(类似用C语言编译本地*.exe的文件),再执行该段代码时就不会用解释器解释来执行了,提升效率。
     *2、为什么不直接编译成本地代码,提高执行效率呢
     *      1)java解释器的执行效率其实也很高了,在某些代码的执行效率上不一定输于执行本地代码
     *      2)如果执行的代码引用类库特别多,在执行启动时时间会非常长
     */
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        //这是Java7引入的新特性。分割数字增强可读性
        for(int i=0; i<10_0000; i++) {
            for (int j = 0; j <10_0000 ; j++) {
                long m=j%3;
            }
        }

        long end =System.currentTimeMillis();
        /**
         *当jvm的配置是-Xint, 总计花费时间:104757(ms)
         *当jvm的配置是-Xcomp, 总计花费时间:2(ms)
         * 默认,总计花费时间:3(ms)
         */
        System.out.println("总计花费时间:"+(end-start)+"(ms)");
    }
}

c1、c2 编译线程

什么是Hotspot JIT编译器?

你的应用程序可能有数百万行的代码。然而,只有一小部分代码会被反复执行。这个小的代码子集(也被称为 "热点")负责你的应用程序的性能。在运行时,JVM使用这个JIT(Just in time)编译器来优化这个热点代码。大多数时候,由应用程序开发人员编写的代码并不是最佳的。因此,JVM的JIT编译器对开发者的代码进行优化,以提高性能。为了进行这种优化,JIT编译器使用C1、C2编译器线程。

c1和c2编译器线程之间的区别是什么?

在Java的早期,有两种类型的JIT编译器。

a. 客户端

b. 服务器

根据你想使用的JIT编译器的类型,必须下载和安装适当的JDK。例如,如果你正在构建一个桌面应用程序,那么需要下载具有 "客户端 "JIT编译器的JDK。如果你要构建一个服务器应用程序,那么就需要下载具有 "服务器 "JIT编译器的JDK。

客户端 JIT 编译器会在应用程序启动后立即开始编译代码。服务器JIT编译器将观察代码的执行情况相当一段时间。基于它获得的执行知识,它将开始进行JIT编译。尽管服务器JIT编译速度很慢,但它产生的代码将比客户端JIT编译器产生的代码更优秀、更有性能。

今天,现代的JDK同时带有客户端和服务器JIT编译器。这两种编译器都试图优化应用程序的代码。在应用程序启动时,代码是用客户端JIT编译器编译的。后来,随着知识的增加,代码就用服务器JIT编译器进行编译。这被称为JVM中的分层编译。

JDK的开发者称他们为客户端和服务器JIT编译器,内部称为c1和c2编译器。因此,客户端JIT编译器使用的线程被称为c1编译器线程。服务器JIT编译器使用的线程被称为c2编译器线程。

c1、c2编译器线程的默认大小

c1、c2编译器线程的默认数量是根据运行应用程序的容器/设备上可用的CPU数量决定的。下面的表格总结了c1、c2编译器线程的默认数量。

CPUs c1 threads c2 threads

1 1 1

2 1 1

4 1 2

8 1 2

16 2 6

32 3 7

64 4 8

128 4 10

你可以通过向你的应用程序传递'-XX:CICompilerCount=N'JVM参数来改变编译器线程数。你在'-XX:CICompilerCount'中指定的数量的三分之一将被分配给c1编译器线程。剩余的线程数将被分配给c2编译器线程。假设你要用6个线程(即'-XX:CICompilerCount=6'),那么2个线程将被分配给c1编译器线程,4个线程将被分配给c2编译器线程。

c1, c2编译器线程高CPU消耗 - 潜在的解决方案

意义不大,不用讲了。无非就是几个参数稍微了解下就行了:-XX:-TieredCompilation、-XX:TieredStopAtLevel=N、-XX:ReservedCodeCacheSize=N、-XX:CICompilerCount

使用字节码和汇编语言同步分析volatile,synchronized的底层实现

要查看JIT生成的汇编代码,要先装一个反汇编器:hsdis。从名字来看,即HotSpot disassembler。

地址如下:profiling/bin at master · jkubrynski/profiling · GitHub

hsdis(Hotspot Disassembly)

C:\Users\Administrator>java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -version Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output Could not load hsdis-amd64.dll; library not loadable; PrintAssembly is disabled java version "1.8.0_73" Java(TM) SE Runtime Environment (build 1.8.0_73-b02) Java HotSpot(TM) 64-Bit Server VM (build 25.73-b02, mixed mode)

1、安装cgwin,Cygwin

2、下载binutils:简书

如果不想编译这么麻烦,直接去下个hsdis-amd64.dll放到jdk1.8.0_144\jre\bin\server,然后执行下面语句测试是否成功

C:\Users\Administrator>java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -version Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output Loaded disassembler from C:\Program Files\Java\jre1.8.0_73\bin\server\hsdis-amd64.dll Decoding compiled method 0x0000000003350a10:

JITWatch(Just In Time Compiler)

好像不能编译注释,我去掉了注释.代码想要用肉眼去看实在是太难找到自己想要看的代码最后翻译成的汇编在哪里。实在是太多了。所以需要借助一些工具的帮助。这里推荐使用的是JITWatch这个工具

  • 点击sandbox

Assembly not found. Was -XX:+PrintAssembly option used?

一般先排查日志文件,可以在该软件看到日志文件输出位置如sandbox.log:

1、我本地发现报如下错误:Could not load hsdis-amd64.dll; library not loadable; PrintAssembly is disabled

此时检查想到可能是hsdis-amd64.dll没有加载,发现是hsdis-amd64.dl放错了位置,copy到整正的jre环境即可

{我之前放的位置是:C:\Program Files\Java\jre1.8.0_73\bin\server,而我们的jre环境是C:\Program Files\Java\jdk1.8.0_73\jre\bin\server}

2、Sandbox Configuation中vm的参数没有加上-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

Not JIT-compiled

这个是因为没有可以重复的代码块可以优化的,所以JIT没有启作用,而只是jvm编译器起的作用,加个for循环就行了

public class JITAssembly {
    public volatile long sum =0;
    public void calcSum(int x){
        sum+=x;
    }
    public static void main(String[] args) {
        JITAssembly jitAssembly = new JITAssembly();
        for(int i=0;i<100000;i++){
         jitAssembly.calcSum(2);
        }       
        System.out.println(jitAssembly.sum);     
    }
}

反编译和反汇编的结果如下:

从上面可以看到volatile的反汇编后putfield之前调用lock,intel处理器的lock指令如下:

JIT启动条件(循环是100的时候没有启动JIT,但是当循环1000的时候就启动JIT)

实际测试观察synchronized的汇编的时候发现有意思的事情,但循环是100的时候没有启动JIT,但是当循环1000的时候就启动了。所以JIT判断重复代码块是有重复率判断依据的,这个依据以后再仔细说;

循环条件是100的时候

循环条件是10000的时候

可以看到synchronized的底层锁用的是CMPXCHG

CMPXCHG

含义: 比较并交换指令

用法:目的操作数和累加操作数(AH、AL、EAX)进行比较,如果相等(ZF=1),则将源操作数复制到目的操作数中,否则将目的操作数复制到累加器中。