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

Java开发虚拟机八足随笔分享

时间:2023-04-02 00:25:01 Java

简述JVM内存模型线程私有运行时数据区:程序计数器、Java虚拟机栈、本地方法栈。线程共享的运行时数据区:Java堆、方法区。简要说明程序计数器程序计数器表示当前线程正在执行的字节码的行号指示符。程序计数器不会产生StackOverflowError和OutOfMemoryError。虚拟机栈的简单描述Java虚拟机栈用来描述Java方法执行的内存模型。线程创建时分配栈空间,线程结束后回收栈空间。栈中的元素用于支持虚拟机进行方法调用。每个java训练方法执行时,都会创建一个栈帧,用于存放方法的局部变量表、操作栈、动态链接、返回地址等信息。虚拟机栈会产生两种异常:StackOverflowError:线程请求的栈深度大于虚拟机允许的深度。OutOfMemoryError:如果JVM栈容量可以动态扩展,超出虚拟机栈占用内存,抛出。本地方法栈的简单描述本地方法栈类似于虚拟机栈,不同的是虚拟机栈是为虚拟机的Java方法执行服务的,而本地方法栈是为本地方法服务的。虚拟机栈可以看作是普通java函数对应的内存模型,本地方法栈可以看作是native关键字修饰的函数对应的内存模型。本地方法栈会产生两种异常:StackOverflowError:线程请求的栈深度大于虚拟机允许的深度。OutOfMemoryError:如果JVM栈容量可以动态扩展,超出虚拟机栈占用内存,抛出。JVM中堆的主要作用是存放对象实例。Java中几乎所有的对象实例都在堆上分配内存,而堆也是内存管理的最大部分。Java的垃圾回收主要是在堆区进行的。可以使用-Xms和-Xmx设置堆的最小和最大大小。堆抛出OutOfMemoryError异常。方法区简介方法区用于存放虚拟机加载的类信息、常量、静态变量等数据。在JDK6之前,使用永久代来实现方法区,容易出现内存溢出。JDK7去掉了放在永久代的字符串常量池和静态变量。JDK8舍弃了永久代,使用在本地内存中实现的元空间来实现方法区。JDK7中永久代的内容被移到了元空间中。方法区会抛出OutOfMemoryError异常。运行时常量池简介运行时常量池存储常量池表,用于存放编译器产生的各种字面量和符号引用。通常,除了保存Class文件中描述的符号引用外,运行时常量池中还会保存符号引用翻译后的直接引用。此外,还将存储基本类型的字符串。JDK8之前是放在方法区的,大小受方法区限制。JDK8将运行时常量池存储在堆中。直接内存简介直接内存也叫堆外内存,就是在JVM堆外的内存区域分配内存对象。这部分内存不由虚拟机管理,而是由操作系统管理。Java通过DirectByteBuffer进行操作,避免了Java堆和Native堆之间来回拷贝数据。简述java中创建对象的过程。检查指令的参数是否可以在常量池中定位到某个类的符号引用,并检查该引用所代表的类是否已经被加载、解析和初始化。如果没有,先进行类加载。检查通过后,虚拟机会为新对象分配内存。内存分配完成后,虚拟机将成员变量置零,并设置对象头,包括哈希码、GC信息、锁信息、对象所属类的类元信息等。执行init方法,初始化成员变量,执行实例化代码块,调用类的构造函数,将对象在堆中的首地址赋值给引用变量。简述JVM为对象分配内存的策略。指针碰撞:这种方法在内存中放置一个指针作为边界指示符,将已使用的内存放在一边,空闲的内存放在另一边,通过移动指针完成分配。空闲列表:对于Java堆内存不规则的情况,虚拟机必须维护一个列表记录哪些内存可用,从列表中找到足够大的空间分配给对象并在分配时更新列表记录。java对象内存分配如何保证线程安全?内存空间的分配采用CAS机制,通过失败重试的方式保证更新操作的原子性。这种方法效率低下。每个线程在Java堆中预先分配一小块内存,然后在为对象分配内存时直接在自己的“私有”内存中分配。这种策略被普遍采用。简述对象的内存布局对象在堆内存中的存储布局可以分为对象头、实例数据和对齐填充。对象头主要包含两部分数据:MarkWord和类型指针。MarkWord用于存储哈希码(HashCode)、GC世代年龄、锁状态标志、线程持有的锁、偏向线程ID等信息。类型指针是指向其类元数据指针的对象。如果对象是Java数组,就会有一段数据用来记录数组的长度,实例数据存储代码中定义的各种类型的字段信息。对齐填充充当占位符。HotSpot虚拟机要求对象的起始地址必须是8的整数倍,所以需要alignmentpadding。判断一个对象是否为垃圾的引用计数方法:设置一个引用计数器,对象被引用计数器加1,引用失效时计数器减1。如果计数器为0,则它被标记为垃圾。对象之间的循环引用会出现问题,一般不用这种方法。可达性分析:以GCRoots的根对象为起始节点,从这些节点开始,按照引用关系向下查找,如果找不到对象,则将其标记为垃圾。可以作为GCRoots的对象包括虚拟机栈和本地方法栈中引用的对象,类静态属性引用的对象,常量引用的对象。简述Java的引用类型强引用:强引用关联的对象不会被回收。一般使用new方法创建强引用。软引用:与软引用关联的对象只有在内存不足时才会被回收。通常,SoftReference类用于创建软引用。弱引用:垃圾回收器遇到它就回收,也就是说它只能存活到下一次垃圾回收发生。弱引用通常使用Wea??kReference类创建。幻象引用:无法通过该引用获取对象。唯一的目的是在回收对象时能够收到系统通知。幻象引用必须与引用队列结合使用。简述标记清除算法、标记清除算法和标记复制算法标记清除算法:先对要清除的对象进行标记,然后统一回收。这种方法效率低下并且会产生大量不连续的片段。标记排序算法:先标记存活对象,然后将所有存活对象移到一端,然后清理超出该端边界的内存。复制算法:将可用内存按容量分成大小相等的两块,每次只使用其中一块。当已用空间用完后,将存活的对象复制到另一块,然后一次性清理已用内存空间。分代收集算法简介分代收集算法是根据对象的生命周期将内存分成若干块,针对不同的块使用合适的收集算法。通常,堆分为新生代和老年代,这两部分使用不同的算法。新生代的使用:标记复制算法老年代的使用:标记清除或标记整理算法串行垃圾收集器简介单线程串行收集器。在垃圾回收期间,所有其他线程都必须挂起。新生代使用标记复制算法,老年代使用标记整理算法。简单高效。简单地说,ParNew垃圾收集器可以看作是Serial垃圾收集器的多线程版本。新生代使用标记复制算法,老年代使用标记整理算法。简单来说,ParallelScavenge垃圾回收器关注的是吞吐量,即cpu运行代码时间/cpu总耗时(cpu运行代码时间+垃圾回收时间)。新生代使用标记复制算法,老年代使用标记整理算法。简而言之,CMS垃圾收集器专注于最短的暂停时间。CMS垃圾收集器是最早的并发收集器,垃圾收集线程和用户线程同时工作。使用标记和清除算法。收集器分为几个步骤:初始标记、并发标记、并发预清理、并发清除、并发重置。初始标记:暂停其他线程(stoptheworld)并标记与GC根直接关联的对象。并发标记:可达性分析的过程(程序不会停顿)。并发预清理:在并发标记阶段找到从年轻代晋升到老年代的对象,重新标记,挂起虚拟机(stoptheworld)扫描CMS堆中剩余的对象。并发清理:清理垃圾对象,(程序不会暂停)。并发重置,重置CMS收集器的数据结构。简而言之,G1垃圾收集器与以前的收集器不同。垃圾收集器将堆分成多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离的。通过引入Region的概念,将原来整个内存空间划分为多个小空间,这样每个小空间都可以单独进行垃圾回收。初始标记:标记与GC根直接关联的对象。并发标记:可达性分析。在最后的标记中,在并发标记过程中再次标记用户线程修改的对象。筛选回收:对每个Region的回收值和成本进行排序,然后根据用户预期的GC停顿时间制定回收计划进行回收。BriefMinorGCMinorGC是指发生在新生代的垃圾回收。因为大多数Java对象的存活时间都很短,所以MinorGC非常频繁,回收速度一般也比较快。简要说明FullGCFullGC是清理整个堆空间——包括年轻代和永久代。调用System.gc(),老年代空间不足,空间分配保证失败,永久代空间不足,无法产生fullgc。常见的内存分配策略大多数情况下,对象都分配在新生代的Eden区。当Eden空间不足时,就会发起MinorGC。大对象需要大量连续的内存空间,直接分配到老年代区域。如果第一次MinorGC还活着,可以被Survivor容纳,则将对象移至Survivor并将age设置为1,每经过一次MinorGCage就会加1,当增加到一定级别(默认15)会被提升到老年代。如果Survivor中所有同龄对象的大小之和大于Survivor的一半,则年龄不小于这个年龄的对象可以直接进入老年代。空间分配保证。在MinorGC之前,虚拟机必须检查老年代中的最大可用连续空间是否大于新生代中对象的总空间。如果满足,则说明本次MinorGC确定是安全的。如果不是,JVM将检查HandlePromotionFailure参数是否允许保证失败。如果允许,它会继续检查老年代最大可用连续空间是否大于老年代中已经提升的对象的平均大小。如果满足则为MinorGC,否则改为FullGC。简述JVM类加载过程Loading:通过完整的类名获取类的二进制字节流。将类的静态存储结构转换为方法区的运行时数据结构。在内存中生成该类的Class对象作为方法区数据的入口。验证:验证文件格式、元数据、字节码、符号引用等的正确性。准备:为方法区中的类变量分配内存,并设置为0值。解析:将符号引用转换为直接引用。初始化:执行类构造函数的clinit方法进行真正的初始化。简述JVM中的类加载器BootstrapClassLoader启动类加载器:加载/lib下的jar包和类。用C++编写。ExtensionClassLoader扩展类加载器:/lib/ext目录下的jar包和类。用java写的。AppClassLoader是一个应用类加载器,加载当前classPath下的jar包和类。用java写的。简述双亲委托机制类加载器收到类加载请求后,首先判断当前类是否已经加载。已经加载的类会直接返回。如果没有加载,类加载请求会先转发给父类加载器,再转发给启动类加载器。只有当父类加载器无法完成时,它才会尝试自己加载。加载类顺序:BootstrapClassLoader->ExtensionClassLoader->AppClassLoader->CustomClassLoader检查类是否加载顺序:CustomClassLoader->AppClassLoader->ExtensionClassLoader->BootstrapClassLoader双亲委派机制的优点是避免了类的重复加载。同一个类被不同的类加载器加载会生成不同的类,双亲委托保证了java程序的稳定运行。保证核心API不被修改。如何打破双亲委托机制重载loadClass()方法,即自定义类加载器。如何构建自定义类加载器创建自定义类继承自java.lang.ClassLoader重写findClass、loadClass、defineClass方法常用JVM调优参数-Xms初始堆大小-Xmx最大堆大小-XX:NewSize年轻代大小?-XX:MaxNewSize新生代的最大值?-XX:PermSize永久代的初始值?-XX:MaxPermSize永久代的最大值?-XX:NewRatio新生代与老年代调用系统的比例。gc()一定会产生垃圾收集?为什么?调用System.gc()时,不会立即进行垃圾回收,只会记录gc请求。需要配合System.runFinalization()回收静态变量存放位置。在1.8之前,静态成员变量存在于方法区。1.8之后,因为JDK8取消了永久代,静态变量存放在堆中。内存溢出和内存泄漏内存溢出:程序在申请内存的时候,此时已经使用了过多的内存,没有足够的剩余内存空间供其使用。内存泄漏:程序申请内存后,无法完全释放已分配的内存空间。垃圾收集器的种类:SerialCollectors:Serial、SerialOldParallelCollectors:ParallelScavenge、ParallelOldConcurrentCollectors:CMS、G1作者:BackendTechnology小牛说