JAVA面试汇总-4.JVM

汤太咸

1.什么情况下会触发类的初始化?

(1)首先是类未被初始化时,创建类的实例(new 的方式),访问某个类或接口的静态变量,或者对该静态变量赋值,调用类的静态方法。
(2)对类进行反射调用的时候,如果类没有进行,则需要先触发其初始化。
(3)如果在初始化某一个类时候,其父类没有被初始化时候,则会触发父类的初始化。
(4)当咱们打的jar包,在执行完java -jar命令后,用户需要指定一个要执行的主类(包含 main() 方法的那个类,例如SpringBoot的那个启动类的main方法,非SpringBoot的,咱们自己手动打包的一般在MANIFEST.MF文件中指定),虚拟机会先初始化这个主类。
(5)JDK 1.7新增了一种反射方式java.lang.invoke.MethodHandle,通过实MethodHandle同样是访问静态变量,对该静态变量赋值,调用类的静态方法,前提仍然是该类未被初始化。

2.谈谈你对解析与分派的认识。

(1)解析调用是将那些在编译期就完全确定,在类加载的解析阶段就将涉及的符号引用全部转变为可以确定的直接引用,不会延迟到运行期再去完成。
(2)分派又分为静态分派和动态分派
(3)静态分派:同样是将编译期确定的调用,重载(Oveload)就是这种类型,在编译期通过参数的静态类型(注意不是实际类型)作为判断依据,找到具体的调用的方法。

public class TestOverLoad {
    public static void main(String[] args) {
        //静态类型都是Parent,实际类型分别是Sun和Daughter
        Parent sun = new Sun();
        Parent daughter = new Daughter();
        TestOverLoad test = new TestOverLoad();
        //输出结果按照静态类型执行
        test.testMethod(sun);
        test.testMethod(daughter);
    }
    static abstract class Parent { }
    static class Sun extends Parent { }
    static class Daughter extends Parent { }
    public void testMethod(Parent parent) {
        System.out.println("hello, Parent");
    }
    public void testMethod(Sun sun) {
        System.out.println("hello, Sun");
    }
    public void testMethod(Daughter daughter) {
        System.out.println("hello, Daughter");
    }
}

//输出
hello, Parent
hello, Parent

(4)动态分派:运行期根据实际类型确定方法执行版本的分派过程称为动态分派。重写(Override),在运行时期,通过判断实体的真实类型,判断具体执行哪一个方法。

public class TestOverride {
    public static void main(String[] args) {
        //静态类型都是Parent,实际类型分别是Sun和Daughter
        Parent sun = new Sun();
        Parent daughter = new Daughter();
        //这时候输出结果按照实际类型找到方法
        sun.testMethod();
        daughter.testMethod();
    }
    static abstract class Parent {
        public void testMethod() {
            System.out.println("hello, Parent");
        }
    }
    static class Sun extends Parent {
        @Override
        public void testMethod() {
            System.out.println("hello, Sun");
        }
    }
    static class Daughter extends Parent {
        @Override
        public void testMethod() {
            System.out.println("hello, Daughter");
        }
    }
}
//输出
hello, Sun
hello, Daughter

3.Java类加载器包括⼏种?它们之间的⽗⼦关系是怎么样的?双亲委派机制是什么意思?有什么好处?

(1)启动类加载器(Bootstrap ClassLoader),由C语言编写的。负责把<JAVA_HOME>\lib目录中的类库加载到虚拟机内存中。
(2)扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
(3)应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
(4)自定义类加载器。下面是自定义类加载器的方式,这个有几点注意,TestDemo编译出来class,把class复制到idea或者eclipse生成target目录之外,因为需要删除掉TestDemo.java,这样target下的class可能也自动没有了,另外如果不删除TestDemo.java会导致一直输出默认的应用程序加载器,因为你运行环境里有,双亲委派的应用程序加载器能找TestDemo,所以默认用父类的了,所以必须删除掉。

package test;

public class TestDemo {
    private String name;
    public TestDemo()
    {
    }
    public TestDemo(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public String toString()
    {
        return "Demo name is " + name;
    }
}
package test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class TestMyClassLoader extends ClassLoader
{
    public TestMyClassLoader()
    {
    }
    public TestMyClassLoader(ClassLoader parent)
    {
        super(parent);
    }
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }
    private File getClassFile(String name)
    {
        //重点是这个路径,在本地编译好TestDemo后,把class放在一个其他路径下
        //不要默认用idea或者eclipse的target路径
        //注意运行这个类之前把代码的TestDemo.java删除掉或者注释掉
        //否则怎么运行都是默认的加载器AppClassLoader
        File file = new File("/Users/buxuesong/TestDemo.class");
        return file;
    }
    private byte[] getClassBytes(File file) throws Exception
    {
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
    public static void main(String[] args) throws Exception
    {
        TestMyClassLoader mcl = new TestMyClassLoader();
        Class<?> c1 = Class.forName("test.TestDemo", true, mcl);
        Object obj = c1.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}
//输出
Demo name is null
test.TestMyClassLoader@5cad8086
//如果没删除TestDemo.java输出
Demo name is null
sun.misc.Launcher$AppClassLoader@18b4aac2

(5)扩展类加载器的父类是启动类加载器,应用程序类加载器的父类是扩展类加载器,自定义类加载器的父类是应用程序类加载器。
(6)双亲委派机制:除了启动类加载器,其余加载器都应该有自己的父类加载器,当一个类加载器需要加载某个类时,默认把这个累交给自己的父类去加载,只有当父类无法加载这个类时候(它的搜索范围中没有找到所需的类),自己才去加载,按照这个规则,所有的累加载最终都会到启动类加载器过一遍。
(7)双亲委派实际上保障了Java程序的稳定运作,因为随着这种父类关系自带了一种层级关系,按照层级关系来分别加载,如果不按照顺序各个加载器自行加载,用户如果自己写了一个java. lang.Object的类,系统会出现多个Object类,导致整个java体系无法运转。

4.如何⾃定义⼀个类加载器?你使⽤过哪些或者你在什么场景下需要⼀个⾃定义的类加载器吗?

(1)这问题问了我上面的,我在写一遍,自定义类加载器,有几点注意,TestDemo编译出来class,把class复制到idea或者eclipse生成target目录之外,因为需要删除掉TestDemo.java,这样target下的class可能也自动没有了,另外如果不删除TestDemo.java会导致一直输出默认的应用程序加载器,因为你运行环境里有,双亲委派的应用程序加载器能找TestDemo,所以默认用父类的了,所以必须删除掉。

package test;

public class TestDemo {
    private String name;
    public TestDemo()
    {
    }
    public TestDemo(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public String toString()
    {
        return "Demo name is " + name;
    }
}
package test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class TestMyClassLoader extends ClassLoader
{
    public TestMyClassLoader()
    {
    }
    public TestMyClassLoader(ClassLoader parent)
    {
        super(parent);
    }
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }
    private File getClassFile(String name)
    {
        //重点是这个路径,在本地编译好TestDemo后,把class放在一个其他路径下
        //不要默认用idea或者eclipse的target路径
        //注意运行这个类之前把代码的TestDemo.java删除掉或者注释掉
        //否则怎么运行都是默认的加载器AppClassLoader
        File file = new File("/Users/buxuesong/TestDemo.class");
        return file;
    }
    private byte[] getClassBytes(File file) throws Exception
    {
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
    public static void main(String[] args) throws Exception
    {
        TestMyClassLoader mcl = new TestMyClassLoader();
        Class<?> c1 = Class.forName("test.TestDemo", true, mcl);
        Object obj = c1.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}
//输出
Demo name is null
test.TestMyClassLoader@5cad8086
//如果没删除TestDemo.java输出
Demo name is null
sun.misc.Launcher$AppClassLoader@18b4aac2

(2)我们之前写的获取数据库连接,通过class.forname去加载数据库驱动。以及热加载这种方式,咱们修改了java文件,但是tomcat没有手动重启,这个时候有一个能够监控到java有变化重新编译了的情况,通过线程出发tomcat重启,就达到了热加载的机制。还有就是apk加密的方式,打包时候,源码-》class-》加密-》打成jar包-》安装-》运行-》classLoader解密-》classLoader加载-》用户使用app,这样只有实现解密方法的classloader才能正常加载,其他的classLoader无法运行。

5.堆内存设置的参数是什么?

(1)-Xms初始堆内存大小
(2)-Xmx最大堆内存大小,生产环境中,JVM的Xms和Xmx建议设置成一样的,能够避免GC时还要调整堆大小。
(3)-XX:NewSize=n,设置年轻代大小-XX:NewRatio=n设置年轻代和年老代的比值。如:-XX:NewRatio=3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4,默认新生代和老年代的比例=1:2
(4)-XX:SurvivorRatio=n,设置年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个,默认是8,表示Eden:S0:S1=8:1:1如:-XX:SurvivorRatio=3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5。
(5)-XX:+HeapDumpOnOutOfMemoryError,-XX:HeapDumpPath,这两个是设置但程序OOM后,输出Dump文件以供分析原因的,但是对于目前的K8s的情况,这俩没什么用,OOM之后,K8s发现服务没响应,直接kill了,然后重启一个新的,OOM根本就来不及生成,因为生成文件耗时较多,K8s杀的很快。
(6)-Xss128k 设置每个线程的堆栈大小。
(7)-XX:+PrintGCDetails,输出GC日志。

6.在JVM中,如何判断一个对象是否死亡?

(1)通过引用计数算法,引用计数器,每当一个地方引用这个对象的时候,计数器就加1,当引用失效,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用了。实现简单,效率高,但是它很难解决对象的循环引用问题。
(2)可达性分析算法,这个算法的基本思路是通过一系列称为“GC Roots”(一组必须活跃的引用)作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时候,那么证明此对象是不可用的。
(3)但是上面两种算法都不能直接判定对象已经死亡,只有gc时,针对两中算法都已经失效的情况下,对两个对象回收后才真正算是死亡。

7.Perm Space中保存什么数据?会引起OutOfMemory吗?

(1)全称是Permanent Generation space,是指内存的永久保存区域。该块内存主要是被JVM用来存储类的元信息、类变量以及内部字符串(interned string)等内容。Perm Space使用的JVM内存。在JDK1.8被元空(Metaspace)替代。永久代是hotspot VM的实现特有。
(2)Metaspace没有存储字符串常量池,而在jdk7的时候已经被移动到了堆中,MetaSpace其他存储的东西,包括类文件,在JAVA虚拟机运行时的数据结构,以及class相关的内容,如Method,Field道理上都与永久代一样,只是划分上更趋于合理。metaspace使用的本地内存而不是JVM内存,因此理论上可以扩展到和本地剩余内存一样大。
(3)当Perm Space中加载的类过多时候,或者存储的内部字符串过多,空间不足时候都可能会导致java.lang.OutOfMemoryError: PermGen space。

8.分派:静态分派与动态分派。

(1)上面第二个问题,讲过了
(2)静态分派:同样是将编译期确定的调用,重载(Oveload)就是这种类型,在编译期通过参数的静态类型(注意不是实际类型)作为判断依据,找到具体的调用的方法。

public class TestOverLoad {
    public static void main(String[] args) {
        //静态类型都是Parent,实际类型分别是Sun和Daughter
        Parent sun = new Sun();
        Parent daughter = new Daughter();
        TestOverLoad test = new TestOverLoad();
        //输出结果按照静态类型执行
        test.testMethod(sun);
        test.testMethod(daughter);
    }
    static abstract class Parent { }
    static class Sun extends Parent { }
    static class Daughter extends Parent { }
    public void testMethod(Parent parent) {
        System.out.println("hello, Parent");
    }
    public void testMethod(Sun sun) {
        System.out.println("hello, Sun");
    }
    public void testMethod(Daughter daughter) {
        System.out.println("hello, Daughter");
    }
}

//输出
hello, Parent
hello, Parent

(3)动态分派:运行期根据实际类型确定方法执行版本的分派过程称为动态分派。重写(Override),在运行时期,通过判断实体的真实类型,判断具体执行哪一个方法。

public class TestOverride {
    public static void main(String[] args) {
        //静态类型都是Parent,实际类型分别是Sun和Daughter
        Parent sun = new Sun();
        Parent daughter = new Daughter();
        //这时候输出结果按照实际类型找到方法
        sun.testMethod();
        daughter.testMethod();
    }
    static abstract class Parent {
        public void testMethod() {
            System.out.println("hello, Parent");
        }
    }
    static class Sun extends Parent {
        @Override
        public void testMethod() {
            System.out.println("hello, Sun");
        }
    }
    static class Daughter extends Parent {
        @Override
        public void testMethod() {
            System.out.println("hello, Daughter");
        }
    }
}
//输出
hello, Sun
hello, Daughter

9.请解释StackOverflowError和OutOfMemeryError的区别?

(1)当一个线程启动的时候,jvm就会给这个线程分配一个栈,随着程序的执行,会不断执行方法,因此栈帧会不断入栈和出栈。然后,一个栈所能容纳的栈帧是有限的,当栈帧的数量超过了栈所允许的范围的时候(比如递归调用),就会抛出StackOverflowError异常。
(2)程序在执行的过程中,需要不断的在堆内存new对象,每new一个对象,就会占用一段内存,当对没有足够的内容分配给对象示例时,就会抛出OutOfMemeryError异常。

10.你有没有遇到过OutOfMemory问题?你是怎么来处理这个问题的?处理过程中有哪些收获?

(1)读取文件时,每条数据对应一个实体,创建大量的实体,实体放入集合中,达到一定程度后,又无法通过GC回收被占用的内存,最终超过配置的内存大小,则会抛出OutOfMemeryError。
(2)从数据库中取出大量数据,每条数据一个实体,实体放入集合中,达到一定量级,超过配置的内存大小,会抛出OutOfMemeryError。
(3)实际上就一个根本原因,集合使用的内存超过了分配的最大内存了。这个时候有两种方案,分别是扩展分配的最大内存,使程序的集合能够分配到足够的内存,这种情况不推荐,治标不治本,下次集合占用的更多,还是会抛出OutOfMemeryError;另一种方案,使针对集合拆分,例如读取文件,每次读取200条,处理完成后,原来的集合设置null,重新创建新的集合,重新加入200条,这样GC的时候就可以释放掉已经处理完的实体集合。数据库读取大量数据建议分页处理,每次处理几十条,几百条即可。

11.StackOverflow异常有没有遇到过?⼀般你猜测会在什么情况下被触发?如何指定⼀个线程的堆栈⼤⼩?⼀般你们写多少?

(1)每当java程序启动一个新的线程时,java虚拟机会为他分配一个栈,java栈以帧为单位保持线程运行状态;当线程调用一个方法时,jvm压入一个新的栈帧到这个线程的栈中,只要这个方法还没返回,这个栈帧就存在。
(2)StackOverflow的意思是栈内存溢出,往栈里存放的过多,导致内存溢出。出现在递归方法,参数个数过多,递归过深。方法的嵌套调用层次太多(如递归调用),随着java栈中的帧的增多,最终导致这个线程的栈中的所有栈帧的大小的总和大于-Xss设置的值,而产生StackOverflowError溢出异常。
(3)-Xss256k 可以设置每个线程的堆栈大小,jdk1.5以后默认是1M。个人感觉不用单独配置,除非这个服务会有大量递归调用循环操作,否则不需要单独配置。

12.内存模型以及分区,需要详细到每个区放什么。

(1)分为方法区,虚拟机栈,本地方法栈,堆,程序计数器
(2)方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。对于HotSpot虚拟机来说实际上这部分在以前叫做永久代。运行时常量池,也在方法区中存储,存放编译期生成的常量,运行期间的常量也可以存储器中,例如String的intern()方法产生的。
(3)虚拟机栈,线程私有的。每个方法在执行时,会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。虚拟机栈中有局部变量表部分,局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
(4)本地方法栈,为虚拟机使用到的Native方法服务。
(5)Java堆,Java虚拟机管理的内存中最大的一块,虚拟机启动创建的,大部分的对象实例存储在其中,也是GC操作的重点区域,许多分配的区域,新生代、老年代、Eden,Survivor之类都在其中。
(6)程序计数器,线程私有的内存,占用空间较小,如果现成正在执行的一个Java方法,程序计数器存储的是正在执行的虚拟机字节码指令的地址;如果是native方法,这个计数器值则为空(Undefined)。唯一不会抛出OutOfMemoryError的空间。
(7)直接内存,这个不是Java虚拟机中的内存区域。NIO可以使用Native函数库直接分配对外内存,Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,可以提高性能,因为避免了在Java堆和Native堆中来回复制数据。如果分配了jvm大量内存,如果直接内存无法分配更多的内存时,也会抛出OutOfMemoryError。

13.做GC时,⼀个对象在内存各个Space中被移动的顺序是什么?

(1)大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
(2)大对象会直接分配到老年代中。大对象是需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

byte[] a = new byte[4*1024*1024];

(3)虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认为15),就将会被晋升到老年代中。这个阈值可以通过-XX:MaxTenuringThreshold设置。

14.虚拟机在运行时有哪些优化策略

(1)针对一些热点运行的代码,虚拟机在运行过程中发现热点代码后,将其编译成本地机器码。再次运行的时候运行效率更高的本地机器码。
(2)公共子表达式消除:如果一个表达式E已经计算过了,并且从先前的计算到现在E中的所有变量的值都没有发生变化,那个E的这次出现就成为了公共子表达式。对于这样的表示式,没有必要对它再次进行计算了,直接沿用之前的结果就可以了。
(3)数组范围检查消除:例如在某些情况下,如果重复的取某个数组内固定位置的值,例如arr[2],只要在编译优化时判断2在arr的范围内即可,编译优化出来的机器码就不在判断范围了;另外咱们写的代码都是在循环内,重复获取数组内容,那么编译优化时,只要判断循环的范围不超过数据范围,那么编译优化的代码就可以不判断范围了,直接运行即可。
(4)方法内联:像下面的两个方法,内联在一起看的话,可以发现f方法没什么意义,因此编译优化时,直接将下面调用f(obj);代码编译过滤掉即可。

public static void f(Object obj){
    if)(obj != null){
        System.out.println("do something");
    }
}
public static void test(String[] args){
    Object obj = null;
    f(obj);
}

(5)逃逸分析:基本行为就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。栈上分配:如果确定不会出现方法逃逸的话,编译优化时候可以将其直接分配到栈上,这样当方法执行完毕,就自动在栈上消除了,避免了垃圾收集。同步消除:如果确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉。标量替换:如果一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替,这样成员变量会分配到栈上。

15.你知道哪些或者你们线上使⽤什么GC策略?它有什么优势,适⽤于什么场景?

(1)标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。标记和清除两个过程的效率都不高;标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(2)复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。直接分成两块太占用空间,IBM提出了另一种方式,分成1个Eden和2个Survivor空间,Eden和Survivor的大小比例是8∶1,当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。分配的空间小了,可能会导致某些对象由于没有空间直接分配到老年代中。复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。
(3)标记-整理算法:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。这样空间连续,易于管理。另外标记-整理算法主要用于老年代的回收机制,主要是由于清理次数较少,可能被清除的对象也不多。

阅读 263

程序员一枚,也爱读书,也爱理财,还喜欢把学到的读到的总结出来写点东西,写的不难,希望大家喜欢。

3 声望
0 粉丝
0 条评论

程序员一枚,也爱读书,也爱理财,还喜欢把学到的读到的总结出来写点东西,写的不难,希望大家喜欢。

3 声望
0 粉丝
文章目录
宣传栏