JVM内存模型深度剖析

JVM对象创建过程

1、类加载检查

当虚拟机遇到一条new指令的时候,首先会检查这条指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,那么必须执行相应的类加载过程。

2、分配内存

当类检查完成后,便需要为新生的对象分配内存了。不过在类加载完成的时候,对象所需的内存大小便可完全确定了。为对象分配内存的任务,实际上等同步把一块确定大小的空间从Java堆中划分出来。这个步骤有两个问题:

  1. 如何划分内存(和垃圾回收算法有关系)
  2. 在并发情况下,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

划分内存的方法有:

  • 指针碰撞法(Bump the Pointer)
    • 如果Java的堆内存是绝对规整的,所有使用过的内存放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,那么所分配的内存就仅仅是把那个指针想空闲的空间挪动一段与对象大小相等的距离。指针碰撞分配内存模型图如下:
  • 空闲列表(Free List)
    • 如果Java的堆内存是不规整的,已使用的内存和空闲的内存是相互交错的,这样就没办法进行指针碰撞了,虚拟机就必须维护一个列表,用来记录哪块内存是可用的,在分配的时候,从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

 

解决并发的方法:

  • CAS
    • 虚拟机采用CAS配上失败重试的方式保证更新操作的原子性,来对分配内存空间的动作进行同步处理。
  • 本地线程分配缓冲(Thread Local Allocation Buffer)
    • 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。哪个线程要分配内存,就在哪个线程本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓冲区时才需要同步锁定。可通过XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB,-XX:TLABSize 指定TLAB大小。TLAB分配流程如下图:

3、初始化

内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4、设置对象头

初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是那个实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashObject()方法时才开始计算)、对象的分带年龄等信息。这些信息存放在对象的对象头(ObjectHeader)之中。

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。HotSpot虚拟机中对象头包括两部分信息,第一部分是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有锁、偏线程ID、偏向时间戳等。另一部分是类型指针,即对象指向它的类元数据指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如下两图:

32位对象

 64位对象

 

对象头

  • Mark Word(标记字段,32位站4字节,64位占8字节):记录对象运行时信息,如hashCode、GC分代年龄、锁状态标志等。
  • Klass Pointer:指向所属类的信息
  • 数组长度(数组对象才有):4字节存储长度

实例数据:各种字段的值,按宽度分类紧邻存储

对齐填充:内存对齐为1个字长整数倍,减少CPU总线周期

5、执行方法

执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上来讲,就是为属性赋值(注意,这与上面的赋 零值不同,这是由程序员赋的值),和执行构造方法。

对象头和指针压缩

对象大小查看可以引入jol-core包:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
public class JOLSample {

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

        System.out.println();
        ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
        System.out.println(layout1.toPrintable());

        System.out.println();
        ClassLayout layout2 = ClassLayout.parseInstance(new Person());
        System.out.println(layout2.toPrintable());
    }

    // -XX:+UseCompressedOops           默认开启的压缩所有指针
    // -XX:+UseCompressedClassPointers  默认开启的压缩对象头里的类型指针Klass Pointer
    // Oops : Ordinary Object Pointers
    public static class Person {
        //8B mark word
        //4B Klass Pointer   如果关闭压缩-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,则占用8B
        int id;        //4B
        String name;   //4B  如果关闭压缩-XX:-UseCompressedOops,则占用8B
        byte b;        //1B
        Object o;      //4B  如果关闭压缩-XX:-UseCompressedOops,则占用8B
    }
}

运行结果:
"C:\Program Files\Java\jdk1.8.0_271\bin\java.exe" -Dvisualvm.id=4607453614000 "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2020.3.2\lib\idea_rt.jar=53958:D:\Program Files\JetBrains\IntelliJ IDEA 2020.3.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_271\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_271\jre\lib\rt.jar;D:\work\IdeaProjects\WorkDemo\target\classes;D:\work\JARRepositry\MavenRepositry\com\github\dozermapper\dozer-core\6.5.0\dozer-core-6.5.0.jar;D:\work\JARRepositry\MavenRepositry\commons-beanutils\commons-beanutils\1.9.3\commons-beanutils-1.9.3.jar;D:\work\JARRepositry\MavenRepositry\commons-collections\commons-collections\3.2.2\commons-collections-3.2.2.jar;D:\work\JARRepositry\MavenRepositry\commons-io\commons-io\2.5\commons-io-2.5.jar;D:\work\JARRepositry\MavenRepositry\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\work\JARRepositry\MavenRepositry\org\slf4j\jcl-over-slf4j\1.7.25\jcl-over-slf4j-1.7.25.jar;D:\work\JARRepositry\MavenRepositry\org\objenesis\objenesis\2.6\objenesis-2.6.jar;D:\work\JARRepositry\MavenRepositry\org\glassfish\jaxb\jaxb-runtime\2.4.0-b180830.0438\jaxb-runtime-2.4.0-b180830.0438.jar;D:\work\JARRepositry\MavenRepositry\javax\xml\bind\jaxb-api\2.4.0-b180830.0359\jaxb-api-2.4.0-b180830.0359.jar;D:\work\JARRepositry\MavenRepositry\org\glassfish\jaxb\txw2\2.4.0-b180830.0438\txw2-2.4.0-b180830.0438.jar;D:\work\JARRepositry\MavenRepositry\com\sun\istack\istack-commons-runtime\3.0.7\istack-commons-runtime-3.0.7.jar;D:\work\JARRepositry\MavenRepositry\org\jvnet\staxex\stax-ex\1.8\stax-ex-1.8.jar;D:\work\JARRepositry\MavenRepositry\com\sun\xml\fastinfoset\FastInfoset\1.2.15\FastInfoset-1.2.15.jar;D:\work\JARRepositry\MavenRepositry\javax\activation\javax.activation-api\1.2.0\javax.activation-api-1.2.0.jar;D:\work\JARRepositry\MavenRepositry\org\openjdk\jmh\jmh-core\1.19\jmh-core-1.19.jar;D:\work\JARRepositry\MavenRepositry\net\sf\jopt-simple\jopt-simple\4.6\jopt-simple-4.6.jar;D:\work\JARRepositry\MavenRepositry\org\apache\commons\commons-math3\3.2\commons-math3-3.2.jar;D:\work\JARRepositry\MavenRepositry\io\netty\netty-all\4.1.35.Final\netty-all-4.1.35.Final.jar;D:\work\JARRepositry\MavenRepositry\com\dyuproject\protostuff\protostuff-api\1.0.10\protostuff-api-1.0.10.jar;D:\work\JARRepositry\MavenRepositry\com\dyuproject\protostuff\protostuff-core\1.0.10\protostuff-core-1.0.10.jar;D:\work\JARRepositry\MavenRepositry\com\dyuproject\protostuff\protostuff-runtime\1.0.10\protostuff-runtime-1.0.10.jar;D:\work\JARRepositry\MavenRepositry\com\dyuproject\protostuff\protostuff-collectionschema\1.0.10\protostuff-collectionschema-1.0.10.jar;D:\work\JARRepositry\MavenRepositry\junit\junit\4.12\junit-4.12.jar;D:\work\JARRepositry\MavenRepositry\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar;D:\work\JARRepositry\MavenRepositry\com\google\code\gson\gson\2.8.6\gson-2.8.6.jar;D:\work\JARRepositry\MavenRepositry\com\google\guava\guava\30.1-jre\guava-30.1-jre.jar;D:\work\JARRepositry\MavenRepositry\com\google\guava\failureaccess\1.0.1\failureaccess-1.0.1.jar;D:\work\JARRepositry\MavenRepositry\com\google\guava\listenablefuture\9999.0-empty-to-avoid-conflict-with-guava\listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar;D:\work\JARRepositry\MavenRepositry\com\google\code\findbugs\jsr305\3.0.2\jsr305-3.0.2.jar;D:\work\JARRepositry\MavenRepositry\org\checkerframework\checker-qual\3.5.0\checker-qual-3.5.0.jar;D:\work\JARRepositry\MavenRepositry\com\google\errorprone\error_prone_annotations\2.3.4\error_prone_annotations-2.3.4.jar;D:\work\JARRepositry\MavenRepositry\com\google\j2objc\j2objc-annotations\1.3\j2objc-annotations-1.3.jar;D:\work\JARRepositry\MavenRepositry\org\apache\commons\commons-lang3\3.11\commons-lang3-3.11.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-engine\6.6.0\flowable-engine-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-bpmn-converter\6.6.0\flowable-bpmn-converter-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-bpmn-model\6.6.0\flowable-bpmn-model-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-process-validation\6.6.0\flowable-process-validation-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-image-generator\6.6.0\flowable-image-generator-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-engine-common-api\6.6.0\flowable-engine-common-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-engine-common\6.6.0\flowable-engine-common-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-variable-service-api\6.6.0\flowable-variable-service-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-event-registry-api\6.6.0\flowable-event-registry-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\com\fasterxml\jackson\core\jackson-core\2.11.2\jackson-core-2.11.2.jar;D:\work\JARRepositry\MavenRepositry\com\fasterxml\jackson\core\jackson-databind\2.11.2\jackson-databind-2.11.2.jar;D:\work\JARRepositry\MavenRepositry\com\fasterxml\jackson\core\jackson-annotations\2.11.2\jackson-annotations-2.11.2.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-variable-service\6.6.0\flowable-variable-service-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-identitylink-service\6.6.0\flowable-identitylink-service-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-identitylink-service-api\6.6.0\flowable-identitylink-service-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-entitylink-service\6.6.0\flowable-entitylink-service-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-entitylink-service-api\6.6.0\flowable-entitylink-service-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-event-registry\6.6.0\flowable-event-registry-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-event-registry-model\6.6.0\flowable-event-registry-model-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-event-registry-json-converter\6.6.0\flowable-event-registry-json-converter-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-eventsubscription-service-api\6.6.0\flowable-eventsubscription-service-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\com\fasterxml\uuid\java-uuid-generator\3.3.0\java-uuid-generator-3.3.0.jar;D:\work\JARRepositry\MavenRepositry\org\liquibase\liquibase-core\3.8.0\liquibase-core-3.8.0.jar;D:\work\JARRepositry\MavenRepositry\org\yaml\snakeyaml\1.23\snakeyaml-1.23.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-event-registry-configurator\6.6.0\flowable-event-registry-configurator-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-eventsubscription-service\6.6.0\flowable-eventsubscription-service-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-task-service\6.6.0\flowable-task-service-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-task-service-api\6.6.0\flowable-task-service-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-job-service\6.6.0\flowable-job-service-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-job-service-api\6.6.0\flowable-job-service-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-batch-service\6.6.0\flowable-batch-service-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-batch-service-api\6.6.0\flowable-batch-service-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-idm-api\6.6.0\flowable-idm-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-idm-engine\6.6.0\flowable-idm-engine-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;D:\work\JARRepositry\MavenRepositry\commons-codec\commons-codec\1.15\commons-codec-1.15.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-idm-engine-configurator\6.6.0\flowable-idm-engine-configurator-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-cmmn-api\6.6.0\flowable-cmmn-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-cmmn-model\6.6.0\flowable-cmmn-model-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-dmn-api\6.6.0\flowable-dmn-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-dmn-model\6.6.0\flowable-dmn-model-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-form-model\6.6.0\flowable-form-model-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-form-api\6.6.0\flowable-form-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-content-api\6.6.0\flowable-content-api-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-http-common\6.6.0\flowable-http-common-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\apache\commons\commons-email\1.5\commons-email-1.5.jar;D:\work\JARRepositry\MavenRepositry\com\sun\mail\javax.mail\1.5.6\javax.mail-1.5.6.jar;D:\work\JARRepositry\MavenRepositry\javax\activation\activation\1.1\activation-1.1.jar;D:\work\JARRepositry\MavenRepositry\org\mybatis\mybatis\3.5.5\mybatis-3.5.5.jar;D:\work\JARRepositry\MavenRepositry\org\springframework\spring-beans\5.2.9.RELEASE\spring-beans-5.2.9.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\org\springframework\spring-core\5.2.9.RELEASE\spring-core-5.2.9.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\org\springframework\spring-jcl\5.2.9.RELEASE\spring-jcl-5.2.9.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\joda-time\joda-time\2.10.6\joda-time-2.10.6.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-spring\6.6.0\flowable-spring-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-spring-common\6.6.0\flowable-spring-common-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-job-spring-service\6.6.0\flowable-job-spring-service-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-event-registry-spring-configurator\6.6.0\flowable-event-registry-spring-configurator-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\flowable\flowable-event-registry-spring\6.6.0\flowable-event-registry-spring-6.6.0.jar;D:\work\JARRepositry\MavenRepositry\org\springframework\spring-context\5.2.9.RELEASE\spring-context-5.2.9.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\org\springframework\spring-aop\5.2.9.RELEASE\spring-aop-5.2.9.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\org\springframework\spring-expression\5.2.9.RELEASE\spring-expression-5.2.9.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\org\springframework\spring-jdbc\5.2.9.RELEASE\spring-jdbc-5.2.9.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\org\springframework\spring-tx\5.2.9.RELEASE\spring-tx-5.2.9.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\org\springframework\spring-orm\5.2.9.RELEASE\spring-orm-5.2.9.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\org\projectlombok\lombok\1.18.4\lombok-1.18.4.jar;D:\work\JARRepositry\MavenRepositry\com\google\protobuf\protobuf-java\3.7.1\protobuf-java-3.7.1.jar;D:\work\JARRepositry\MavenRepositry\org\junit\jupiter\junit-jupiter\5.8.2\junit-jupiter-5.8.2.jar;D:\work\JARRepositry\MavenRepositry\org\junit\jupiter\junit-jupiter-api\5.8.2\junit-jupiter-api-5.8.2.jar;D:\work\JARRepositry\MavenRepositry\org\opentest4j\opentest4j\1.2.0\opentest4j-1.2.0.jar;D:\work\JARRepositry\MavenRepositry\org\junit\platform\junit-platform-commons\1.8.2\junit-platform-commons-1.8.2.jar;D:\work\JARRepositry\MavenRepositry\org\apiguardian\apiguardian-api\1.1.2\apiguardian-api-1.1.2.jar;D:\work\JARRepositry\MavenRepositry\org\junit\jupiter\junit-jupiter-params\5.8.2\junit-jupiter-params-5.8.2.jar;D:\work\JARRepositry\MavenRepositry\org\junit\jupiter\junit-jupiter-engine\5.8.2\junit-jupiter-engine-5.8.2.jar;D:\work\JARRepositry\MavenRepositry\org\junit\platform\junit-platform-engine\1.8.2\junit-platform-engine-1.8.2.jar;D:\work\JARRepositry\MavenRepositry\io\projectreactor\reactor-core\3.3.5.RELEASE\reactor-core-3.3.5.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\org\reactivestreams\reactive-streams\1.0.3\reactive-streams-1.0.3.jar;D:\work\JARRepositry\MavenRepositry\io\projectreactor\reactor-test\3.3.5.RELEASE\reactor-test-3.3.5.RELEASE.jar;D:\work\JARRepositry\MavenRepositry\io\vavr\vavr\0.9.0\vavr-0.9.0.jar;D:\work\JARRepositry\MavenRepositry\io\vavr\vavr-match\0.9.0\vavr-match-0.9.0.jar;D:\work\JARRepositry\MavenRepositry\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar" com.alisha.java.base.jvm.JOLSample
java.lang.Object 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)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


[I 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)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     0    int [I.<elements>                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


com.alisha.java.base.jvm.JOLSample$Person 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)                           65 cc 00 f8 (01100101 11001100 00000000 11111000) (-134165403)
     12     4                int Person.id                                 0
     16     1               byte Person.b                                  0
     17     3                    (alignment/padding gap)                  
     20     4   java.lang.String Person.name                               null
     24     4   java.lang.Object Person.o                                  null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

什么是Java对象的指针压缩

  • jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
  • JVM配置参数:UseCompressedOops,compressed--压缩、oop(ordinary object pointer)--对象指针
  • 启用指针压缩:-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops

为什么要进行指针压缩?

  • 在64位平台的Hotspot中使用32位(实际上存储用64位),内存使用会多出1.5倍,使用较大指针在主内存和缓存之间移动数据,占宽带较大,同事GC也会承受较大压力
  • 为了减少64位平台下的内存消耗,启用指针压缩功能
  • 在JVM中32位地址最大支持4G内存(2的32次方),可以通过对象指针的存入堆内存时压缩编码,取出CPU寄存器后解码方式进行优化(对象指针在堆中32位,在寄存器中是35位,2的35次方=32G),使得32位地址就可以支持更大的内存配置(小于等于32G)
  • 堆内存小于4G时,不需要开启指针压缩,JVM会直接去除高32位地址,即使用低虚拟地址空间
  • 堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对Java对象寻址,这就会出现第一条中说的问题,因此堆内存最好不要大于32G

关于对齐填充:对于大部分处理器,对象以8字节整数倍来对齐填充,都是高效的存取方式。

JVM对象内存分配

对象内存分配流程图

逃逸分析

逃逸分析的基本原理就是分析对象动态作用域,当一个对象在方法内被定义后,它可能会被外部方法所引用,例如作为调用参数被传到其他方法中。甚至还有可能被外部线程访问到,譬如赋值给其他线程中访问的实例变量,这种则称之为线程逃逸,从不逃逸、方法逃逸到线程逃逸,成为对象由低到高的不同逃逸程度。

public User test1() {
    User user = new User();
    user.setId(1);
    user.setName("pat");
    return user;
}

public void test2() {
    User user = new User();
    user.setId(1);
    user.setName("pat");
}

从上面的代码中,可以看到test1方法中的user对象被返回了,但这个对象的作用域范围不确定,test2方法中的对象的在方法结束时,该对象就可以认为是无效对象了。像这样的对象就可以将其分配在栈内存中,让其他方法结束时跟随栈内存一起被回收掉。

在JVM中可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象的分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭逃逸分析了,使用参数(-XX:-DoEscapeAnalysis)。

栈上分配

通过JVM内存分配,我们知道Java中的对象都是在堆上分配的,当对象没有被引用的时候,需要依靠GC进行内存回收,如果对象较多的时候,会给GC带来较大的压力,这样也间接的影响到应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象会不会被外部访问。如果不会逃逸,那么可以将该对象在栈上分配内存,这样该对象所占用的空间就可以随着栈帧出栈而销毁,就减轻了垃圾回收的压力。栈上分配可以支持方法逃逸,但不能支持线程逃逸。

标量替换

通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解成若干个被这个方法使用的成员变量所替换,这些代替的成成员变量在栈帧或者寄存器上分配空间,这样就不会因为没有一块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JKD7之后默认开启。

标量与聚合量

标量就是不可被进一步分解的变量,而Java中的基本数据类型就是标量(如:int、long等基本数据类型以及reference类型等),标量的对立就是可以进行进一步分解的变量,而这种量称为聚合量。而在Java中对象就是可以被进一步分解的聚合量。

栈上分配实例:

public class AllotOnStack {

    /**
     * 栈上分配,标量替换
     * 代码调用了1亿次alloc(),如果是分配到堆上,大概需要1GB以上堆空间,如果堆空间小于该值,必然会触发GC。
     *
     * 使用如下参数不会发生GC
     * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
     * 使用如下参数都会发生大量GC
     * -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
     * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
     */
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            alloc();
        }
        long endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);
    }

    private static void alloc() {
        User user = new User();
        user.setId(1);
        user.setName("pat");
    }
}

通过以上的阐释和示例得出的结论就是:栈上分配依赖于逃逸分析和标量替换。

对象在Eden区分配

在大多数情况下,对象都是在Eden去中分配的,当Eden区没有足够的空间进行分配时,虚拟机将会发起一层Minor GC。后面将会对此进行简单的测试,在这之前我们先来交接一下Minor GC和Full GC的区别,如下:

  • Minor GC/Young GC:指发生在新生代的垃圾回收动作,Minor GC非常频繁,回收速度一般也很快。
  • Major GC/Full GC:一般回收老年代、年轻代、方法区的垃圾,Major GC 一般会比 Minor GC的速度慢10倍以上。

前面说了大量对象一开始是被分配在Eden区中的,当Eden区满了之后会触发Minor GC,这种情况可能会有99%的对象会被回收掉,剩余存活的对象会被挪到那块为空的Survivor区,在下一次Eden区满了之后,会再一次触发Minor GC,这时会把Eden区和Survivor区的垃圾对象回收掉,把剩余存活的对象一次性挪到另一块为空的Survivor区,因为新生代的对象都是朝生夕死的,因此在JVM中Eden区和Survivor区的默认比例是8:1:1,这样的比例也是比较合适的,让Eden去够大,Survivor够用即可。在JVM默认有-XX:+UseAdapteiveSizePolicy(默认开启),但是这个参数会导致8:1:1的参数自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdapteiveSizePolicy。

JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy

public class GCTest {

    public static void main(String[] args) throws InterruptedException {
        byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;
        allocation1 = new byte[60000*1024];

        //allocation2 = new byte[8000*1024];

//        allocation3 = new byte[1000*1024];
//        allocation4 = new byte[1000*1024];
//        allocation5 = new byte[1000*1024];
//        allocation6 = new byte[1000*1024];
    }
}

运行结果:
Heap
 PSYoungGen      total 75776K, used 6510K [0x000000076bf80000, 0x0000000771400000, 0x00000007c0000000)
  eden space 65024K, 10% used [0x000000076bf80000,0x000000076c5dbbe8,0x000000076ff00000)
  from space 10752K, 0% used [0x0000000770980000,0x0000000770980000,0x0000000771400000)
  to   space 10752K, 0% used [0x000000076ff00000,0x000000076ff00000,0x0000000770980000)
 ParOldGen       total 173568K, used 60000K [0x00000006c3e00000, 0x00000006ce780000, 0x000000076bf80000)
  object space 173568K, 34% used [0x00000006c3e00000,0x00000006c7898010,0x00000006ce780000)
 Metaspace       used 3409K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 373K, capacity 388K, committed 512K, reserved 1048576K

大对象直接进入老年代

在JVM中大对象需要大量连续内存空间的对象(如:字符串、数组)。JVM参数-XX:PretenureSizeThreshold可以设置对象的大小,如果对象超过这个参数设置的大小,就会直接进入老年代。这个参数只在Serial和ParNew两个收集器下有效。比如设置JVM参数-XX:PretenureSizeThreshold=1000000(单位是字节) -XX:UseSerialGC,再次执行上面的程序代码,就会发现大对象直接进入了老年代。而这样做的原因是为了避免大对象在内存分配时在Eden和Survivor区中来回的复制,导致降低了垃圾回收的效率。

长期存活对象进入老年代

虚拟机采用的是分代收集的思想来管理对象内存,这样在内存回收时就必须识别哪些对象放在新生代,哪些对象放在老年代。为了做到这一点,虚拟机给每一个对象设置了一个年龄计数器。当对象在Eden区中经历过一次GC之后依然能存活,并且能被Survivor区容纳的话,并且被移到Survivor区,这样对象年龄将会加一。对象在Survivor区中没熬过一次GC,年龄就会加一。当年龄达到一定程度的时候(默认是15岁,CMS收集器默认6岁,不同的收集器会略微有点不同),对象就会被晋升到老年代中。对象晋升到老年代的阈值,可以通过参数-XX:MaxTenuringThreshold来设定。

对象年龄判断

当前对象在Survivor区域中(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代,例如Survivor区域里现有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象年龄判断机制一般是在Minor GC之后触发的。

老年代空间担保机制

年轻代每次Minor GC之前JVM都会计算下老年代剩余可用空间,如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象),就会参数“-XX:-HandlePromotionFailure”(jdk8默认设置的)是否设置了,如果有这个参数,就会看看老年代的可用大小,是否大于之前每次Minor GC后进入老年代的对象平均大小。如果说上一步的结果是小于或者参数没有设置,那么就会触发一次Full GC,对象老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够的空间来存放新的对象,那么就会发生OOM。当然Minor GC之后剩余存活需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发Full GC,Full GC完之后如果还是没有足够的空间存放Minor GC之后的存活对象,也会发生OOM。

 

对象内存回收机制

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

引用计数算法

给对象一个引用计数器,每当有一个地方引用它,计数器就加一;当引用失效计数器就减一,任何时候计数器为零的对象就可能不再被使用。引用计数器虽然使用了额外的空间来进行计数,但是它原理简单、效率高,目前主流的虚拟机并没有选择这个算法来管理内存,其主要原因是这个看似简单的算法有很多例外的情况需要考虑,必须要配合大量额外处理才能保证正确的工作,譬如它很难解决对象之间的相互循环引用的情况。所谓对象之间的相互引用问题,如以下代码所示:

 

public class ReferenceCountingGC {

    public Object instance = null;

    public static void main(String[] args) {

        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();

        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;
    }
}

可达性分析算法

将“GC Root”对象作为起点,从这些节点向下开始搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的都是垃圾对象。

GC Root根节点:

  • 执行上下文:JVM 栈中参数、局部变量、临时变量等引用的对象
  • 全局引用:方法区中类的静态引用、常量引用、所指向的对象

优点:无需对象维护GC信息,开销小;单次扫描即可批量识别、回收对象,吞吐高

缺点:多线程情况下对象的引用关系随时在变化,为保证GC Root标记的准确定,需在不变化的 snapshot中进行,会出现STW现象。

 常见引用类型

引用类型

类名称

回收时机

强引用

-

只要GC Root引用链存在,就不会被回收

软引用

SoftReferece

被软引用所引用的对象,当GC后内存依然不足,才被回收

弱引用

WeakReference

被弱引用引用的对象,无论内存是否足够,都会被回收

虚引用

PhantomReference

虚引用所引用的对象无感知,进行正常GC,仅在回收时通知虚引用

finalize() 方法

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候这些对象处于“缓刑”阶段,要真正宣告一个对象真正死亡,至少要经历再次标记过程。标记的前提是对象在进行可达性分析后发现没有与GC Root相连接的引用链。

1、第一次标记进行一次筛选

筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法时,对象会被直接回收。

2、第二次标记

如果这个对象覆盖了finalize()方法,finalize()方法是对象逃脱被回收的最后一次机会,如果一个对象要在finalize()方法中成功的拯救自己,只要重新与引用链上任何的一个对象建立关联即可。譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将会移除“即将回收”的集合。如果对象这个时候还没逃脱,那基本上就会被回收了。

注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值