JVM随笔

本文深入解析JVM架构,包括类装载系统、运行时数据区、执行引擎等关键组件。详细介绍了类装载的过程、对象内存分配机制及常量池的动态特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

jvm随笔一

  • Jvm 基础
  • jvm Class装载系统
  • jvm 垃圾回收器
  • jvm 调优

jvm基础

Java程序,都是通过JRE(java运行时环境)分析,解析字节码并执行。JRE其实就是由JAVA虚拟机(JVM)实现。JVM内部抽象体系结构主要有3大部分组成,即类装载器子系统,运行时数据管理区和执行引擎。

引入一个图片,我个人觉得这是非常好的一副阐述虚拟机JVM核心架构的图形。这个图形来自于 http://geek.csdn.net/news/detail/131976
JVM核心架构图

介绍JVM运行时数据管理区


jvm内存空间分布图

  • 方法区(持久区):类加载子系统负责从文件系统或者网络中加载class信息,加载的类信息即存放在方法区的内存空间,除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量。

  • Java堆:(老年代+新生代) 在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java的对象都存放于java堆,堆空间是所有线程共享的。并且java堆是完成自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显示释放。

  • Java栈: 是一块线程私有的内存空间,如果说java堆和程序数据密切相关,那么java栈就和线程执行密切相关。线程执行的基本行为是函数调用,每次函数条用的数据都是通过java栈传递的。

  • 本地方法栈: 和java栈类似,最大的不同在于本地方法栈用于本地方法调用,java通过JNI,调用本地方法。(通常是c编写)

  • PC寄存器: 每个线程的私有空间,java虚拟机会为每一个java线程创建PC寄存器。

  • 直接内存: Java NIO库,允许java程序使用直接内存,直接内存是在java堆外的,直接向系统申请的内存区间。

jdk1.8之前JVM 内存一直如图所示,jdk1.8 之后,method Area 变为Matespace(元空间),并且从堆区转移本地内存(native memory)中,这样永久内存就不再占用堆内存,它可以通过自动增长来避免JDK7以及前期版本中常见的永久内存错误(Java.lang.OutOfMemoryError: PermGen)。
JDK8也提供了一个新的设置Matespace内存大小的参数:-XX:MaxMetaspaceSize=128m,如果不设置JVM将会根据一定的策略自动增加本地元内存空间。如果你设置的元内存空间过小,你的应用程序可能得到以下错误:”’ java.lang.OutOfMemoryError: Metadata space ”’

java Class装载系统

Class类型通常以文件的形式存在,只有被Java虚拟机装载的Class类型才能在程序中使用。
jvm装载Class类型,可以分为加载、连接和初始化3个步骤,其中连接又可以分为验证、准备和解析3步。

Class文件装载过程

Class加载

在这个阶段,JVM主要完成三件事:
1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、文件生成等方式。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。

Class连接

类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接大致分三个阶段。
1、验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。
2、准备:为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。如static int a = 100;
静态变量a就会在准备阶段被赋默认值0。
对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。
另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int a = 666; 静态常量a就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。
3、解析:将类的二进制数据中的符号引用换为直接引用。

Class初始化

类初始化是类加载的最后一步,除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行Java代码。
类的初始化的主要工作是为静态变量赋程序设定的初值。
如static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100。

类装载的条件

Class只有在必须要使用的时候才会被装载,java虚拟机不会无条件地装载Class类型。Java虚拟机规定,一个类或接口在初次使用时,必须要进行初始化。这里的”使用“,是指主动使用,主动使用有下列几种情况:

  • 当创建一个类的实例是,比如使用new关键字,或者通过反射、克隆、反序列化。
  • 当调用类的静态方法时。
  • 当使用类或接口的静态字段时(final常量除外)。
  • 当使用java.lang.reflect包中的方法反射类的方法时。
  • 当初始化子类,要求先初始化父类。
  • 作为启动虚拟机,含有main()方法的那个类。
    以上情况属于主动,其他属于被动使用,被动使用不会引起类的初始化。

java 对象内存分配机制

Java对象的内存布局包括:对象头(Header),实例数据(Instance Data)和补齐填充(Padding)。

对象头

HotSpot虚拟机的对象头包括两部分信息:运行时数据和类型指针。

运行时数据

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

类型指针

即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。
(并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身,可参考对象的访问定位)

实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类中继承下来的,还是在子类中定义的,都需要记录下来。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oop,从分配策略中可以看出,相同宽度的字段总是分配到一起。

对齐填充

HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

下图从首行main函数开始,一行行的分析Java运行时内存分配

Java内存分配

java 方法区类型信息主要包含一下内容:(常量池 jdk1.7 已经从方法区移动到堆区)
- 这个类型完整的有效名,以及直接父类的完成有效名(除非这个类型是interface或是java.lang.Object这两种没有父类)
- 这个类型的修饰符
- 这个类型直接接口的一个有序列表
- 类型的常量池,域信息,方法信息
- 除了常量外的所有静态变量

运行时常量池和Class常量池(JDK1.7/1.8)

  • JVM对Class文件中每一部分的格式都有严格的要求,每一个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行;但运行时常量池没有这些限制,除了保存Class文件中描述的符号引用,还会把翻译出来的直接引用也存储在运行时常量区。
  • 相较于Class文件常量池,运行时常量池更具动态性,在运行期间也可以将新的变量放入常量池中,而不是一定要在编译时确定的常量才能放入。最主要的运用便是String类的intern()方法。
package com.company;

public class Main {

    public static void main(String[] args) {

    String firstName = new String("test") + new String("01") ;

    //String lastName = "test" + "01";  //#1

    firstName.intern();
    String name = "test01";

    System.out.println(name == firstName); //#2

    //System.out.println("== intern name  " + (name == lastName));

    }
}

以上代码#1 注释的时候, #2 打印为true(注释的时候,firstName.intern()会去判断常量池是否存在,不存在,则在常量池中生产一个对堆对象的引用), #1 不注释的时候,#2 打印false。(主要去掉注释的时候,name引用的字符串对象就在常量池中,而firstName在堆中。)
jdk1.7后intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用,jdk1.6之前都是false.

[Class文件中常量池详解]http://m.blog.csdn.net/wangtaomtk/article/details/52267548
[通过反编译深入理解java String及intern]http://www.importnew.com/21024.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值