本文转载自微信公众号“Angela的博客”,作者Angela。转载本文请联系Angela博客公众号。面试官:我们公司上次进行了专访。来了一百多名候选人。场面十分热闹。你为什么没来?安琪拉:天气太热,你们公司离地铁站远。没有本事,面试完肯定抢不到共享单车,就不凑热闹了。面试官:你是什么意思?安吉拉:没什么。..嘿,我等不及了,让我们开始吧。面试官:简历上写着你熟悉多线程。你能告诉我你为什么使用多线程吗?多线程有什么好处?最好给我一个你工作中的实际例子。Angela:比如:用户查看在支付宝上购买的电影票时,顺便在页面下半部分向用户推荐一些近期的热门电影。比如安吉拉在看她买的《寂静之地2》时,还在页面底部给我推荐了《速度与激情9》,还放了预告片、电影介绍等等。记者:这就结束了?那么,如何详细使用多线程呢?Angela:如果不使用多线程,支付宝服务器会先查询用户的购票信息,再查询热门电影推荐信息,所以串口效率很慢。改成多线程,同时进行两个query查询。查询完成后,将结果汇总并显示给用户。采访者:那么如果搜索热门电影推荐失败,或者搜索推荐信息比较慢,也会影响用户去查看自己购买的票。如果此时用户急着看电影出不来,那不就是3.25吗?Angela:我们会把查询票务信息和查询电影推荐信息的请求放在两个不同的线程池中。将查询热门电影推荐请求做成弱依赖,即查询热门电影推荐失败或超时不会影响查询电影票信息。面试官:你能写个伪代码来解释一下吗?安吉拉:对,给我拿张A4纸,顺便把你的笔借给我。(下面Future和线程池相关的代码看不懂没关系,后面介绍了并发系列之后再回头看)//查询票证信息FuturegetTicketFuture=ticketHandlePool.submit(()->{//查询票务信息doQuery();});//查询推荐电影FuturerecMovieFuture=recMovieHandlePool.submit(()->{//查询推荐电影try{doQuery();}catch(Exceptionex){//Exception捕获记录logger.warn("information",ex);}});//获取票证查询结果try{recMovieFuture.get(2,TimeUnit.SECONDS);}catch(Exceptione){//弱依赖:超时,中断等异常只是warn级别记录,任务取消,不抛异常logger.warn("information",e);recMovieFuture.cancel(true);}//获取推荐信息查询结果强依赖try{getTicketFuture.get(3,TimeUnit.SECONDS);}catch(Exceptione){logger.error("information",e);recMovieFuture.cancel(true);thrownew***Exception(e,"获取tingticketinformationisabnormal");}面试官:那你给我总结一下并发编程的优点吧。安吉拉:【看来练习赛结束了,开始八条文了。】嗯,刚才我们也看到,并发可以提高程序执行效率,充分利用CPU,尤其是多核,IO密集型(经常需要等待I/O,多线程可以充分利用CPU资源),并发也是一种设计,在一些多任务处理中,或者一个大任务需要拆分成很多子任务的场景下,并发一方面可以提高执行效率,又可以明确表达另一方面,程序员的意图。【这波方法论应该让面试官瑟瑟发抖】面试官:那么并发编程有哪些风险呢?Angela:总的来说有几个:频繁的线程上下文切换会造成性能损失;共享数据如果不控制多线程访问,可能会出现线程安全问题;面试官:关于线程安全,能不能请教几个问题。安吉拉:请问。面试官:能介绍一下JVM运行时数据区的划分吗?Angela:【来了,终于来了】这里稍微提醒一下,有时候我们会把JVM运行时数据区和Java内存模型搞混,一般都会问第二道面试题。JVM的运行时数据区就是堆、栈等,规定了运行时内存(包括寄存器)分成多少块,分别起到什么作用;Java内存模型是为了Java语言的跨平台性能一致性,屏蔽硬件和操作系统而提出的,比如规定了线程和主存之间的抽象关系。既然是规范,那么只是规定了概念,具体的实现还要看JVM虚拟机在不同平台上的实现。其实每天写代码的时候心中有一个大概的概念就好了。你不需要像做研究那样深入实施细节,除非你面试的是JVM虚拟机开发职位。如下图,是JVM运行时数据区。采访者:那我们来谈谈每个领域。先说说什么是虚拟机栈(JVMStacks)?Angela:要说虚拟机栈,首先要知道栈是什么时候创建的?因为栈是线程私有的,所以栈的生命周期和线程的生命周期是一致的,所以只要记住栈是绑定在线程上的。栈中存储的内容也是线程运行所需要的。看上面的图片。栈由栈帧组成。栈帧内部存放局部变量表、操作栈、动态链接、方法返回地址。面试官:请详细说说这四件事。Angela:我必须写一些代码来演示。publicstaticvoidmain(String[]args){Stringstr=dance("angela",3);}privatestaticStringdance(Stringname,intcount){Stringresult=name+":"+count;returnresult;}每个方法都会在执行Frame(StackFrame),栈帧是方法运行时的基本数据单元,用来存放局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程对应一个栈帧从入栈到出栈的过程。当执行局部变量表的main方法时,会启动一个线程。这时,一个栈帧会被压入主线程的虚拟栈中。dance方法调用时,会压入一个栈帧,存放name、count、name、count、result等数据,存在于局部变量表中,也就是存放方法参数和局部变量的区域。如果是非静态方法,方法所属对象实例的引用存放在局部变量表index[0]中,一个引用变量占4个字节,后面依次是方法参数和局部变量。操作数栈操作数很有意思。下面我们就来说说程序执行的原理。字符串结果=名称+“:”+计数;这行代码分为几个步骤,简单来说就是:取,执行,存。取名字压入操作数栈,取计数压入操作数栈,然后两次出栈,执行拼接动作,将结果压入栈,存入局部变量表。那么为什么说JVM的执行引擎是基于栈的执行引擎呢?这就是原因。这里的栈就是操作数栈。后面系列在讲volatile的时候会介绍load、store等相关指令。字节码指令中的STORE指令是将运算栈中的计算结果写回局部变量表的存储空间。下面说一下动态链接和方法返回地址。动态链接每个栈帧都包含对常量池中当前方法的引用,目的是支持方法调用过程的动态链接。可能有点绕。这部分将深入讨论编译、加载和链接类的过程。类文件存储了大量的符号引用。字节码中的方法调用指令使用指向常量池中方法的符号引用作为参数。.其中一些符号引用会在类加载阶段或首次使用时转换为直接引用。这种转换称为静态解析。另一部分将在每次运行期间转换为直接引用。这部分称为动态连接。比如在反射过程中调用invokedynamic时,运行时存储在常量池中的当前方法的引用是动态生成的,可以在运行时动态链接。方法返回地址方法执行后,执行出栈操作,弹出当前栈帧。方法返回地址是方法执行完后(出栈后)接下来要执行的地址。面试官:你说的native方法栈和虚拟机栈有什么区别?Angela:本地方法栈(NativeMethodStack)和虚拟机栈非常相似。它们的区别在于虚拟机栈是由虚拟机执行的。Java方法(也就是字节码)服务,native方法栈服务于虚拟机使用的Native方法。比如Thread类的start0方法就是Native方法。privatenativevoidstart0();SunHotSpot虚拟机直接将本地方法栈和虚拟机栈合二为一。面试官:那Java堆呢?Angela:Java堆是所有线程共享的内存区域。它是在虚拟机启动时创建的,几乎所有的对象实例都在这里分配内存。面试官:能谈谈Java堆的内存回收吗?Angela:堆是垃圾收集器(GC)管理的主要区域,因此常被称为“垃圾收集堆”(GarbageCollectedHeap)。从内存回收的角度来看,由于收集器基本采用分代收集算法,所以Java堆又可以细分为:新生代和老年代;更详细的包括Eden空间、FromSurvivor空间和ToSurvivor空间。空间等面试官:方法区呢?你知道JVM规范中JDK8前后方法区的变化吗?Angela:方法区是JVM规范中的语言。不同的JVM有不同的实现。最流行的Sun以Hotspot为例。在JDK8之前,Hotspot中方法区的实现是永久代(Perm)。JDK8开始使用元空间(Metaspace)。之前永久代的字符串常量被移到堆内存中,其他内容被移到元空间中。Metaspace直接在本地内存中分配。面试官:什么是方法区,或者说它的实现元空间是干什么的?Angela:和Java堆一样,方法区(MethodArea)是各个线程共享的一块内存区域。用于存放虚拟机已经加载的对象。类信息、常量、静态变量、即时编译器编译的代码等数据。其实从底层物理存储的角度来看,堆和堆都在内存中。Java虚拟机规范将方法区描述为堆的一个逻辑部分,但它有一个别名叫做Non-Heap(非堆),目的应该是为了与Java堆区分开来。面试官:为什么要用元空间来代替永久代的实现?Angela:有几个原因:字符串存在于永久代中,容易出现性能问题和内存溢出。由于PermGen(永久代)经常溢出,导致java.lang.OutOfMemoryError:PermGen问题,JVM开发者希望这块内存可以更灵活的管理,这样这样的OOM就不再频繁发生;去掉PermGen可以促进HotSpotJVM和JRockitVM的融合,因为JRockit没有永久代。面试官:我看到你在图片的元数据区画了常量池。JVM中的常量池能详细解释一下吗?安吉拉:首先,让我说清楚。JVM中的常量池分为三种:JVM常量池运行时常量池字符String常量池接下来我们会讲到这三种常量池的区别和联系。JVM常量池也称为类文件常量池。它是类文件的一部分,用于保存编译时确定的数据。最重要的是编译期三个字。让我们编写一个Java程序,将其反编译,然后查看字节码。如下图,列出了常量池的符号引用。#1引用#5.#25是什么意思?我们看到#5是java/lang/Object,而#25是#8:#9//"":()V其实是在调用初始化方法,引用方法名,返回Values和继承类(任意class继承了Object类,所以引用了java/lang/Object)常量池存放了一堆符号引用。Class编译加载完成后,Class常量池被加载到运行时常量池中,运行时常量池存放在元空间中。JVM在执行某个类时,会经历加载、连接、初始化,而连接包括验证、准备、分析三个阶段。当类被加载到内存中时,jvm会将类常量池中的内容存储在运行时常量池中。最后一个就是字符串常量池(StringConstantPool)。很多人认为上图中反编译出来的Class常量池中的字符串存放在String常量池中。网上很多博客也混淆了两者。Class常量池只在编译时起作用,在编译时确定了一堆引用关系,比如:类和方法的全限定名、字段名和描述符、方法名和描述符、文本字符串。它存储在哪里?字符串常量池存储在堆上。在JDK6.0及更早的版本中,字符串常量池是放在PermGen区(即方法区)。如何储存?StringTable类用于实现HotSpotVM中的常量池。它是一个Hash表,默认大小和长度为1009;这个StringTable只有一份,所有类共享。字符串常量由一个个字符组成,放在StringTable中。存储了什么?字符串常量池中的字符串只有一份!在JDK6.0及更早版本中,字符串常量池(StringPool)中填充的是字符串常量;常量池(StringPool)也可以存储对放置在堆中的字符串对象的引用。面试官:你说在JDK7.0之后,字符串常量池(StringPool)也可以存储对放在堆中的字符串对象的引用。你能给个例子吗?Angela:在JDK7下,执行String.intern();时,由于常量池中没有字符串"like",所以会在常量池中生成对堆中"like"的引用(注意这是一个reference,这是与JDK1.6的区别。在JDK1.6下是生成原始字符串的副本)publicvoidstringTest(){Stringstr1="follow";Stringstr2="angela";Stringstr3=newString("like");str3.intern();}如下所示,JDK1.6String.intern()的操作生成了原始字符串“like”的副本。JDK1.7如下图:在堆中生成对“like”的引用面试官:那说说前面提到的Java内存模型,最好写点实际的工程代码,解释一下应用Java内存模型在实际项目中的用处。安吉拉:下次怎么样?今天有点晚了,你们公司离地铁很远。想早点抢共享单车。这个问题留给第二位面试官吧。面试官:那好,那你先回去吧,有消息我会通知你的。安吉拉:好的,待会儿见。
