当前位置: 首页 > 后端技术 > Java

一篇文章深入了解JVM,面试轻松拿offer

时间:2023-04-02 02:14:44 Java

大家好,我是奇喵小屋,一个分享技术和生活的博主。以下是我的主页。关注掘金主页,知乎主页,Segmentfault主页,开源中国主页。更多MySQL、Redis、并发、JVM、分布式等面试热点知识,以及Java学习路线、面试重点、职业规划、面签经验等相关博客,后续会陆续发布。转载请注明出处!1.JVM内存结构1.1JDK6Runtime数据区下的内存结构Heap(线程共享的区域):存放对象实例,通过垃圾回收机制进行内存管理Method区(线程共享的区域):存放类对象(类的方法区各种数据的访问入口,各种数据包含以下信息)运行时常量池(包括字符串常量池)存放各种字面量常量,类文件中的符号引用,符号引用通过解析直接引用类型信息得到类型的全限定名,类型的父类的全限定名,类型实现的接口的全限定名,类型是类还是接口,以及该类型的访问修饰符类中声明的所有字段(包括静态变量和实例变量,不包括局部变量)描述(名称、类型、修饰符等)方法信息方法名、返回类型、参数表、字节码说明、修饰符、局部变量表和操作数栈大小、异常表静态变量指向类加载器引用指向Class类对象(Class.forName()Class)引用虚拟机栈(每个线程一个,线程私有)每次Java方法执行完后,会去Java虚拟机的栈中Push一个栈帧。一个Java方法执行后,从Java虚拟机栈中弹出对应的栈帧,编译出一个java文件。一个栈帧需要多大的局部变量表,操作数栈有多深,已经分析过了。并写在方公布的代码属性中,栈帧结构操作数栈局部变量表局部变量表存储了编译器已知的Java基本数据类型,reference,returnAddress类型,这些数据以Slot的形式存储在局部变量表中,除了Double和long占用2个槽,其余占用1个槽。JVM通过索引定位来访问局部变量表。索引从0开始,锁定记录。在运行时常量池中动态连接对栈帧所属方法的引用。方法调用时的动态连接(每次运行时,常量池中的一部分符号引用会转化为直接引用——动态连接)方法返回本地的地址Methodstack(oneperthread,threadprivate):类似于Java虚拟机栈,但是执行native方法programcounter(oneperthread,threadprivate):程序控制流的指示器程序计数器存储下一个的地址字节码指令。如果执行是本地方法,程序计数器是未定义的。直接内存直接内存不属于运行时数据区。JDK1.4引入了NIO类,引入了一种基于Channel和Buffer的I/O方式,可以使用Native函数库直接分配堆外内存(在直接内存中分配空间),然后使用一个DirectByteBuffer堆中存储的对象作为对这块内存的引用来执行操作,执行引擎原生库接口原生方法库1.2JDK7和JDK8内存结构的变化JDK1.7**字符串常量池,静态变量从方法区移到堆方法区:移出字符串常量池和静态变量堆:实例对象,字符串常量池,静态变量JDK1.8去掉方法区,把JDK1.7方法区剩下的东西移到元空间(元空间属于本地内存)。JDK1.8的内存结构如图2所示。对象2.1使用new创建对象——使用Class对象的newInstance()调用构造函数——使用Constructor类的newInstance方法调用构造函数——调用使用克隆方法的构造函数——调用了构造函数并使用反序列化——调用了构造函数2.2通过new创建对象运行时常量池,检查这个符号引用代表的类是否已经被加载、解析和初始化Pass如果没有,则执行相应的类加载,确保类已经加载完成,执行②②为新对象分配内存(类加载完成后可确定内存大小),保证线程安全分配内存。如果堆内存是规则的——指针如果堆内存是规则的,把使用过的内存放在一边,没用过的放在另一边,中间放一个指针作为指示。分配内存时,只需将指针移动到未使用的一侧,持续时间等于对象的大小。如果堆内存不规则——freelist虚拟机维护一个列表记录有哪些内存块在分配内存时,从列表中找到足够大的空间分配给对象实例,并更新列表记录分配的内存.采用哪种方式-->取决于堆内存是否正规-->取决于分配内存时使用的垃圾收集器保证线程安全的方法。修改,对象B同时也使用指针分配内存,同步分配内存空间的动作有以下两种方案-->JVM使用CAS+失败重试保证内存分配操作的原子性不同的线程在differentmemory空间中的内存分配在Java堆中,每个线程分配一小块内存(localthreadallocationbufferTLAB)。哪个线程需要分配内存,它在自己的TLAB中分配③将分配的内存空间初始化为0(不包括Object头),接下来就是填充对象头,存储对象是哪个类的实例等信息,如何把类的元数据信息,对象的hashcode,对象的GCgenerationage找到对象头。④执行init()方法初始化对象2.3对象内存布局一个实例对象占用的内存可以分为三部分——对象头、实例数据、对齐填充对象头(普通对象2个字,数组3个字)第一个字——>标记字(32or64位,内容取决于最后2个标志位)第二个字——>指针,指向实例对象所属类的Class对象第三个字——>数组长度实例data不需要对齐填充,主要是占空间,对象的大小保证是某个字节的整数倍。2.4对象的访问定位——定位对象的引用,通过句柄访问对象的方法。如果使用句柄访问,Java堆可能会分出一块作为句柄池。该引用存储句柄池的地址。对象引用通过直接指针存储实例对象的地址。实例对象的对象头存储了一个指向Class对象的指针。内存分配策略Java堆的内存模型对象先在Eden分配。如果Eden空间不够,则进行一次MinorGC,大对象直接进入老年代(比如很长的字符串或者元素数量很多的数组)长期存活。对象进入老年代JVM为每个对象定义了一个年龄计数器(Age),保存在对象头中。该对象通常出生在伊甸园。如果经过一次MinorGC后对象还活着,如果对象不能被Survivor容纳,则直接进入老年代。如果对象可以被Survivor容纳,则进入Survivor,设置Age为1。对象在Survivor中每次MinorGC存活下来,Age达到一定值(默认15,可以设置),进入老年代,判断是否相同幸存者区的年龄。objects,如果它们的sizes之和占据了ToSurvivor区空间的一半以上,则比这个年龄大的objects会直接进入oldgenerationspaceallocationguarantee。在YGC发生之前,JVM首先检查老年代中最大可用连续空间是否>新生代,如果所有对象的总空间大于,那么如果这次YGC安全性没有建立,则说明YGC是不安全。JVM检查HandlePromotionFailure参数以查看是否允许保证失败。如果平均大小大于该值,则执行YGC;否则,执行FullGC;如果不允许,执行FGC一个文件被编译成.class文件(二进制流文件)来启动Java进程,并在内存中创建运行时数据区。main()所在的类被加载到内存中,JVM不会在程序启动的时候加载所有的类,只有当某个类需要用到的时候,才会加载该类,它只会被加载一次如果其父类的一个接口定义了一个默认方法3.3类加载过程3.3.1加载这个过程是由ClassLoader完成的,即将二进制流读入内存并为其创建一个java.lang.Class对象。通过类的全限定名获取类.class文件(可从磁盘、网络等获取)将class文件中的静态存储结构转换为方法区中的运行时数据结构,生成java.lang.Class对象在内存中表示这个类作为方法区所有对类数据的访问和使用都必须经过这个Class对象。3.3.2Verification验证文件格式验证主要验证字节流是否符合Class文件格式规范,是否可以被当前虚拟机加载处理如:主次版本号是否在当前的处理范围内虚拟机。常量池中是否存在不受支持的常量类型。指向该常量的索引值中是否存在不存在的常量或不符合类型的常量。元数据验证这个类是否有父类(Object除外)这个类是否继承了一个不允许被继承的类(final类)如果这个类不是抽象类,这个类是否实现了所有的方法类需要在父类和接口中实现字段和字段中的方法是否与父类冲突。字节码校验最重要的校验环节是分析数据流向和控制,判断语义是否合法、合乎逻辑。主要针对元数据校验后方法体的校验。保证类方法在运行时不会有害。符号引用验证主要针对将符号引用转化为直接引用,会延伸到第三个解析阶段,主要判断访问类型等涉及引用的情况,主要是保证引用会被访问,不会出现类等不可访问问题。3.3.3PreparePreparation为类变量分配内存并设置初始值static变量-设置为零值finalstatic变量-设置为它的声明值3.3.4分析解析替换.class的常量池中的符号引用file直接引用的符号引用:用一组符号描述引用的目标直接引用:可以指向目标的指针,相对偏移量,或者可以间接定位目标的句柄3.3.5初始化初始化其实就是一次执行classclinit()方法的进程子类在初始化前必须完成父类的初始化。JVM保证一个类的clinit()方法在多线程环境下只会被一个线程执行一次——保证一个类只会被加载一次。Class检查父类是否已经加载——如果没有加载,则先加载父类检查类的接口是否已经加载——如果没有加载,则先加载接口Javac编译器会自动收集静态属性赋值语句,静态代码块生成clinit()方法(集合顺序按照代码中的顺序)——然后执行类构造函数clinit()方法。只有当界面使用父界面时,才会加载父界面,否则不加载父界面。3.4ClassLoader在JVM中,一个类在JVM中的唯一性是由加载它的类加载器和类本身决定的。3.4.1类加载器类型引导类加载器(BootstrapClassLoader)是用C++实现的,其他都是用Java实现的。它负责加载JVM基础核心类库。Java程序不能直接使用的类必须存放在${JAVA_HOME}/lib目录下,或者存放在-Xbootclassp下的-Xbootcalsspath参数指定的路径下。ExtensionClassLoader)负责加载一些类到JVM内存中。可以加载的类在以下条件下可以在Java程序中使用:${JAVA_HOME}/lib/ext目录中存储的类库由java.ext.dirs系统变量指定。应用类加载器(ApplicationClassLoader)中的所有类库是ClassLoader.getSystemClassLoader()的返回值,负责加载用户类路径(ClassPath)上的所有类库,在Java程序中可以使用。一般这是程序默认的类加载器3.4.2双亲委托机制的工作过程当一个类加载器接受当一个类加载请求到来时,它不会先自己加载类,而是将请求传递给它的父类加载器,父类加载器再将请求传递给父类加载器的父类加载器...直到通过tothestartupclassloader(upwardpass)启动类加载器在它的搜索范围内寻找要加载的类-如果找到则找不到加载类-传给子类加载器...直到某个类加载器可以在其搜索范围内找到要加载的类(向下传递)双亲委托机制的好处使得Java基础类库中的类(如Object)保证由特定的类加载器加载创建