问题
- JVM中的内存模型的特点、作用?(堆、栈、方法区之类的作用)
- 如何实现一个对象的深克隆?
- 多线程中,Runnable和Thread的区别?好处?.run()方法和.start()方法的区别?
- 有1G的数据,每行数据为一个单词(定长,16字节),现在只给内存1M,请问如何统计出词频前100的单词?
- Redis的集群搭建?
正确答案:
- 从知乎找的,比较详细:https://zhuanlan.zhihu.com/p/57750092
1)类加载子系统负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。
2)java堆在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的,这是一块与java应用密切相关的内存空间。
3)java的NIO库允许java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
4)垃圾回收系统是java虚拟机的重要组成部分,垃圾回收器可以对方法区、java堆和直接内存进行回收。其中,java堆是垃圾收集器的工作重点。和C/C++不同,java中所有的对象空间释放都是隐式的,也就是说,java中没有类似free()或者delete()这样的函数释放指定的内存区域。对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找、标识并释放垃圾对象,完成包括java堆、方法区和直接内存中的全自动化管理。
5)每一个java虚拟机线程都有一个私有的java栈,一个线程的java栈在线程创建的时候被创建,java栈中保存着帧信息,java栈中保存着局部变量、方法参数,同时和java方法的调用、返回密切相关。
6)本地方法栈和java栈非常类似,最大的不同在于java栈用于方法的调用,而本地方法栈则用于本地方法的调用,作为对java虚拟机的重要扩展,java虚拟机允许java直接调用本地方法(通常使用C编写)
7)PC(Program Counter)寄存器也是每一个线程私有的空间,java虚拟机会为每一个java线程创建PC寄存器。在任意时刻,一个java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined
8)执行引擎是java虚拟机的最核心组件之一,它负责执行虚拟机的字节码,现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。
JVM内存模型:
-
栈
栈也叫方法栈,是线程私有的,线程在执行每个方法时都会同时创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈。 -
本地方法栈
本地方法栈与栈类似,也是用来保存线程执行方法时的信息,不同的是,执行java方法使用栈,而执行native方法使用本地方法栈。主要为Native方法服务。 -
程序计数器
程序计数器保存着当前线程所执行的字节码位置,每个线程工作时都有一个独立的计数器。程序计数器为执行java方法服务,执行native方法时,程序计数器为空。记录当前线程执行的行号栈、本地方法栈、程序计数器这三个部分都是线程独占的。
-
堆
初始化的对象,成员变量 (那种非static的变量),所有的对象实例和数组都要在堆上分配。
堆是jvm管理的内存中最大的一块,堆被所有线程共享,目的是为了存放对象实例,几乎所有的对象实例都在这里分配。当堆内存没有可用的空间时,会抛出OOM异常。根据对象存活的周期不同,jvm把堆内存进行分代管理,由垃圾回收器来进行对象的回收管理。堆内的分区:Eden,survival (from+ to),老年代,各自的特点
堆里面分为新生代和老生代(java8取消了永久代,采用了Metaspace),新生代包含Eden+Survivor区,survivor区里面分为from和to区,内存回收时,如果用的是复制算法,从from复制到to,当经过一次或者多次GC之后,存活下来的对象会被移动到老年区,当JVM内存不够用的时候,会触发Full GC,清理JVM老年区。当新生区满了之后会触发YGC,先把存活的对象放到其中一个Survice区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎片,因此一般会把Eden 进行完全的清理,然后整理内存。那么下次GC 的时候,就会使用下一个Survive,这样循环使用。如果有特别大的对象,新生代放不下,就会使用老年代的担保,直接放到老年代里面。因为JVM 认为,一般大对象的存活时间一般比较久远。
-
方法区
方法区也是各个线程共享的内存区域,又叫非堆区。主要是存储类信息,常量池(static常量和static变量),编译后的代码(字节码)等数据。
-
如何实现一个对象的深克隆?
浅克隆:
浅度克隆步骤:-
实现java.lang.Cloneable接口
要clone的类为什么还要实现Cloneable接口呢?Cloneable接口是一个标识接口,不包含任何方法的!这个标识仅仅是针对Object类中clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的 clone()方法(也就是调用了super.Clone()方法),那么Object的clone()方法就会抛出 CloneNotSupportedException异常。 -
重写java.lang.Object.clone()方法
JDK API的说明文档解释这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用。二是拷贝对象与用new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。观察一下Object类的clone()方法是一个native方法,native方法的效率一般来说都是远高于java中的非native方法。这也解释了为什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息赋到新对象中,虽然这也实现了clone功能。Object类中的clone()还是一个protected属性的方法,重写之后要把clone()方法的属性设置为public。
Object类中clone()方法产生的效果是:先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容。对基本数据类型,这样的操作是没有问题的,
但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。
深克隆:
- 利用原型模式实现深克隆:
要克隆的类和类中所有非基本数据类型的属性对应的类
1、都实现java.lang.Cloneable接口
2、都重写java.lang.Object.clone()方法(因为需要克隆的类,如果包含了其他属性的类,那么这些其他的类也需要实现,重现克隆方法,才能实现深克隆)
Card类
public class Card implements Cloneable{ private int cardId; private String name; public int getCardId() { return cardId; } public void setCardId(int cardId) { this.cardId = cardId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Card(int cardId, String name) { super(); this.cardId = cardId; this.name = name; } public Object colne() { Card c=null; try { c=(Card) super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(); } return c; } }
Student类
public class Student implements Cloneable { private String name; private String sex; private String major; private String phoneNumber; private Card card; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String getMajor() { return major; } public void setMajor(String major) { this.major = major; } public String getPhoneNumber() { return phoneNumber; } public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } public Object clone() { Student s=null; try { s=(Student) super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(); } s.setCard((Card) card.colne()); return s; } public Card getCard() { return card; } public void setCard(Card card) { this.card = card; } }
-
利用序列化、反序列化实现深克隆:
Card类(只需实现Serializable接口,并定义序列号)public class Card implements Serializable{ private static final long serialVersionUID = 872390113109L; }
Student类
public class Student implements Serializable{ private static final long serialVersionUID = 369285298572941L; private String name; private String sex; private String major; private String phoneNumber; private Card card; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String getMajor() { return major; } public void setMajor(String major) { this.major = major; } public String getPhoneNumber() { return phoneNumber; } public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } public Object clone() { Student s=null; try { //字节数组输出流在内存中创建一个字节数组缓冲区,所有发送到输出流的数据保存在该字节数组缓冲区中。创建字节数组缓冲区 无参数传入时为32字节 ByteArrayOutputStream baos=new ByteArrayOutputStream(); //创建对象输出流 ObjectOutputStream oo=new ObjectOutputStream(baos); //将对象写入内存 oo.writeObject(this); //字节数组输入流在内存中创建一个字节数组缓冲区,所有发送到输入流中的数据保存再该字节数组缓冲区 ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray()); //创建对象输入流 ObjectInputStream oi=new ObjectInputStream(bais); //将对象从内存的输出流中读回对象,完成深克隆 反序列化 s=(Student) oi.readObject(); oi.close(); oo.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } return s; } public Card getCard() { return card; } public void setCard(Card card) { this.card = card; } }
-
-
对于多线程的实现方式主要有两种:实现Runnable接口、继承Thread类
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。main函数,实例化线程对象也有所不同,
extends Thread :t.start();
implements Runnable : new Thread(t).start();
总结:
实现implements Runnable接口比继承 extends Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制(不能访问父类的私有成员?)
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
.run()和.start()的区别
Start:
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到spu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
Run:
run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。 -
分而治之:
-
由于内存的限制,所以不能同时将1G的文件进行分析计算,可以采用分治思想,将文件分为多个,可以分为每一个只有1M的,这样对小文件的计算就不会出现超出内存的问题。
-
分割的方法是将每一个单词进行hash后,hash%5000这样将单词分割到5000个小文件中,1G/5000 大约一个文件200k,重复单词一定被分割到同一个文件中。
-
由于每一项是一个单词,可以采用字典树Trie进行统计/hashmap,统计每一个文件中出现的次以及频率。字典树的时间复杂度为单词最长的数值+遍历一遍n*O(k),hash为遍历一遍+产生hash+冲突解决。
-
对每一个小文件取出其中频率最大的前100个单词,然后进行合并,或者直接进行归并排序/堆排序,nlog(k),如果遇到相同的单词,直接累加上去就行,再重新计算该单词的排序位置。
-