JVM本身是介于JAVA编译器和操作系统之间的程序,这个程序提供了一个无视操作系统和硬件平台的运行环境
所有线程共享的数据区:
- 方法区: 存储已被虚拟机加载的类信息、静态变量、编译后代码等数据。并使用永久代来实现方法区,1.8后被元空间替代,元空间并不在虚拟机中,而是使用本地内存,要画到上图那就是图外了。
- 堆区: 我们常说用于存放对象的区域,1.7之后字符串常量池移到这里。
每个线程私有的数据区:
- 虚拟机栈: 方法执行时创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法一个栈帧,互不干扰。
- 本地方法栈: 用于存放执行native方法的运行数据。
- 程序计数器: 当前线程所执行的字节码的指示器,通过改变计数器来选取下一条需要执行的字节码指令。
直接内存:
- 直接内存并非Java标准。
- JDK1.4 加入了新的 NIO 机制,目的是防止 Java 堆 和 Native 堆之间往复的数据复制带来的性能损耗,此后 NIO 可以使用 Native 的方式直接在 Native 堆分配内存。
- 直接内存区域是全局共享的内存区域。
废话,还有线程使用的虚拟机栈上的啊,在方法体中声明的变量以及创建的对象,将直接从该线程所使用的栈中分配空间。
逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用。正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象;故由于无法回收,即成为逃逸。
逃逸分析可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配,由于该对象一定是局部的,所以栈上分配不会有问题。
JVM在内存新生代Eden Space中开辟了一小块线程私有的区域TLAB(Thread-local allocation buffer)。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
也就是说,Java中每个线程都会有自己的缓冲区称作TLAB,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
new、静态字段或方法被使用、反射、父类、main函数调用
- 加载(获取字节流并转换成运行时数据结构,然后生成Class对象)
- 验证(验证字节流信息符合当前虚拟机的要求)
- 准备(为类变量分配内存并设置初始值)
- 解析(将常量池的符号引用替换为直接引用)
- 初始化(执行类构造器-类变量赋值和静态块的过程)
启动类加载器:是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库
扩展类加载器:它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器
系统类加载器:它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次防止恶意覆盖Java核心API。
优先选择在新生代的Eden区被分配。
- 大对象,-XX:PretenureSizeThreshold 大于这个参数的对象直接在老年代分配,来避免新生代GC以及分配担保机制和Eden与Survivor之间的复制
- 经过第一次Minor GC仍然存在,能被Survivor容纳,就会被移动到Survivor中,此时年龄为1,当年龄大于预设值就进入老年代
- 如果Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象进入老年代
- 如果Survivor空间无法容纳新生代中Minor GC之后还存活的对象
不可达对象:通过一系列的GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时则此对象是不可用的。
GC Roots包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。
彻底死亡条件:
条件1:通过GC Roots作为起点的向下搜索形成引用链,没有搜到该对象,这是第一次标记。
条件2:在finalize方法中没有逃脱回收(将自身被其他对象引用),这是第一次标记的清理。
新生代因为每次GC都有大批对象死去,只需要付出少量存活对象的复制成本且无碎片所以使用“复制算法”
老年代因为存活率高、没有分配担保空间,所以使用“标记-清理”或者“标记-整理”算法
复制算法:将可用内存按容量划分为Eden、from survivor、to survivor,分配的时候使用Eden和一个survivor,Minor GC后将存活的对象复制到另一个survivor,然后将原来已使用的内存一次清理掉。这样没有内存碎片。
标记-清除:首先标记出所有需要回收的对象,标记完成后统一回收被标记的对象。会产生大量碎片,导致无法分配大对象从而导致频繁GC。
标记-整理:首先标记出所有需要回收的对象,让所有存活的对象向一端移动。
当Eden区空间不足以继续分配对象,发起Minor GC。
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足(通过Minor GC后进入老年代的大小大于老年代的可用内存)
- 方法区空间不足
Set中的对象不按特定方式排序,并且没有重复对象。但它的有些实现类能对集合中的对象按特定方式排序,例如TreeSet类,它可以按照默认排序,也可以通过实现java.util.Comparator接口来自定义排序方式。
List中的对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,如通过list.get(i)方式来获得List集合中的元素。
Map中的每一个元素包含一个键对象和值对象,它们成对出现。键对象不能重复,值对象可以重复。
| ArrayList | LinkedList |
|---|---|
| 数组 | 双向链表 |
| 增删的时候在扩容的时候慢,通过索引查询快,通过对象查索引慢 | 增删快,通过索引查询慢,通过对象查索引慢 |
| 扩容因子1.5倍 | 无 |
(1)JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
(2)当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
(3)针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题
- Hashtable中的方法是同步的,而HashMap中的方法在缺省情况下是非同步的。
- Hashtable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null。
- 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
- Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。
http://www.jasongj.com/java/concurrenthashmap/
HashTable 在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微细粒度的。 ConcurrentHashMap 将 hash 表分为 16 个桶(默认值)
ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。
Java 8为进一步提高并发性,摒弃了分段锁的方案,而是直接使用一个大的数组。同时为了提高哈希碰撞下的寻址性能,Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N))
Java 8的ConcurrentHashMap同样是通过Key的哈希值与数组长度取模确定该Key在数组中的索引。
对于put操作,如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值。如果Key对应的数组元素(也即链表表头或者树的根元素)不为null,则对该元素使用synchronized关键字申请锁,然后进行操作。如果该put操作使得当前链表长度超过一定阈值,则将该链表转换为树,从而提高寻址效率。
- 定义:多线程是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
- 存在的原因:因为单线程处理能力低。打个比方,一个人去搬砖与几个人去搬砖,一个人只能同时搬一车,但是几个人可以同时一起搬多个车。
- 实现:在Java里如何实现线程,Thread、Runnable、Callable。
- 问题:线程可以获得更大的吞吐量,但是开销很大,线程栈空间的大小、切换线程需要的时间,所以用到线程池进行重复利用,当线程使用完毕之后就放回线程池,避免创建与销毁的开销。
新建 -- 就绪 -- 运行 -- 阻塞 -- 死亡
- 继承Thread类
- 实现Runnable接口
- 扩展一种:实现Callable接口。这个得和线程池结合
https://fangjian0423.github.io/2016/04/18/java-synchronize-way/
锁是在不同线程竞争资源的情况下来分配不同线程执行方式的同步控制工具,只有线程获取到锁之后才能访问同步代码,否则等待其他线程使用结束后释放锁
通常和wait,notify,notifyAll一块使用。
wait:释放占有的对象锁,释放CPU,进入等待队列只能通过notify/all继续该线程。
sleep:则是释放CPU,但是不释放占有的对象锁,可以在sleep结束后自动继续该线程。
notify:唤醒等待队列中的一个线程,使其获得锁进行访问。
notifyAll:唤醒等待队列中等待该对象锁的全部线程,让其竞争去获得锁。
拥有synchronize相同的语义,但是添加一些其他特性,如中断锁等候和定时锁等候,所以可以使用lock代替synchronize,但必须手动加锁释放锁
性能:资源竞争激励的情况下,lock性能会比synchronize好,竞争不激励的情况下,synchronize比lock性能好。
锁机制:synchronize是在JVM层面实现的,系统会监控锁的释放与否。lock是代码实现的,需要手动释放。
用法:synchronize可以用在代码块上,方法上。lock通过代码实现,有更精确的线程语义。
功能:
- 主内存和工作内存,直接与主内存产生交互,进行读写操作,保证可见性;
- 禁止 JVM 进行的指令重排序。
使用ThreadLocal<UserInfo> userInfo = new ThreadLocal<UserInfo>()的方式,让每个线程内部都会维护一个ThreadLocalMap,里边包含若干了 Entry(K-V 键值对),每次存取都会先的都当前线程,然后得到该线程对象中的Map,然后与Map交互。
ConcurrentHashMap ThreadPoolExecutor
https://blog.csdn.net/mzh1992/article/details/60957351
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。
CyclicBarrier可以循环使用。比如,假设我们将计数器设置为10,那么凑齐第一批10个线程之后,计数器就会归零,然后接着凑齐下一批,10个线程。司令下达命令,需要召集10个士兵,然后分别执行10个任务,需要等到士兵集合完毕,才能下达具体的任务,需要10个任务都完成,才能宣布任务结束。
一种同步非阻塞的高效IO交互模型,比同步阻塞IO区别如下:
- 通过缓冲区而非流的方式进行数据的交互,流是进行直接的传输的没有对数据操作的余地,缓冲区提供了灵活的数据处理方式。
- NIO是非阻塞的,意味着每个socket连接可以让底层操作系统帮我们完成而不需要每次开个线程去保持连接,使用的是selector监听所有channel的状态实现。
- NIO提供直接内存复制方式,消除了JVM与操作系统之间读写内存的损耗。
同步和异步在于多个任务执行过程中,后发起的任务是否必须等先发起的任务完成之后再进行,是方法被调用的顺序。
阻塞和非阻塞在于请求的方法是否立即返回,是方法被执行的方式。
- InnoDB支持事务
- InnoDB支持外键
- InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
- InnoDB有行级锁
因为MyISAM相对简单所以在效率上要优于InnoDB.如果系统读多,写少。对原子性要求低。那么MyISAM最好的选择。 如果系统读少,写多的时候,尤其是并发写入高的时候,还需要事务安全性。InnoDB就是首选了。
- 对经常查询的列建立索引,为了避免全表扫描,建多了当数据改变时修改索引浪费资源
- 使用精确列名查询而不是*
- 减少嵌套查询
- 不用NOT IN,IS NULL,NOT IS NULL,无法使用索引
- 原子性(Atomicity):事务作为一个整体被执行 ,要么全部执行,要么全部不执行;
- 一致性(Consistency):保证数据库状态从一个一致状态转变为另一个一致状态;
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行;
- 持久性(Durability):一个事务一旦提交,对数据库的修改应该永久保存。
https://www.jianshu.com/p/4e3edbedb9a8
https://segmentfault.com/a/1190000012773157
https://www.jianshu.com/p/f5ff017db62a
我们拿出一本新华字典,它的目录实际上就是一种索引:非聚集索引。我们可以通过目录迅速定位我们要查的字。而字典的内容部分一般都是按照拼音排序的,这实际上又是一种索引:聚集索引。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
主要使用B+树来构建索引,为什么不用红黑树是因为一般来说,索引本身较大,不会全部存储在内存中,会以索引文件的形式存储在磁盘上,然后是因为局部性原理,数据库系统巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入,(由于节点中有若干个数组,所以地址连续)。而红黑树这种结构,深度更深。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性
好处:
- 快速取数据
- 唯一性索引能保证数据记录的唯一性
- 实现表与表之间的参照完整性
缺点:
- 索引需要占物理空间。
- 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,降低了数据的维护速度。
如果某个字段,或一组字段会出现在一个会被频繁调用的 WHERE 子句中,那么它们应该是被索引的,这样会更快的得到结果。为了避免意外的发生,需要恰当地使用唯一索引,并且我个人不推荐使用全文索引,尤其对于汉字来说,全文索引的开销太大了,得不偿失。
工厂模式、生成器模式
桥接模式、适配器模式、外观模式、组合模式
策略模式、迭代器模式、访问者模式、观察者模式、命令模式、中介者模式、状态模式
http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
枚举实现原理:枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。每个枚举类型都继承自java.lang.Enum。每个枚举常量是一个静态常量字段,使用内部类实现,该内部类继承了枚举类。
静态代码块中初始化对象:懒汉
静态类引用:线程安全
final方法:禁止序列化、克隆
http://www.wangtianyi.top/blog/2017/10/08/shi-yao-shi-hou-neng-yong-shang-she-ji-mo-shi/
动态地将责任附加到对象上,如果要拓展功能,装饰器提供了比继承更有弹性的方式。
Java IO包中就使用了该模式,InputStream有太多的实现类如FileInputStream,如果要在每个实现类上加上几种功能如缓冲区读写功能Buffered,则会导致出现ileInputStreamBuffered, StringInputStreamBuffered等等,如果还要加个按行读写的功能,类会更多,代码重复度也太高。
所以使用FilterInputStream这个抽象装饰器来装饰InputStream,使得我们可以用BufferedInputStream来包装FileInputStream得到特定增强版InputStream,且增加装饰器种类也会更加灵活。
Spring是个包含一系列功能的合集,如快速开发的Spring Boot,支持微服务的Spring Cloud,支持认证与鉴权的Spring Security,Web框架Spring MVC。IOC与AOP依然是核心。
- 发送请求——>DispatcherServlet拦截器拿到交给HandlerMapping
- 依次调用配置的拦截器,最后找到配置好的业务代码Handler并执行业务方法
- 包装成ModelAndView返回给ViewResolver解析器渲染页面
无参数构造器、字段注入
- Spring对Bean进行实例化
- Spring将值和Bean的引用注入进Bean对应的属性中
- 容器通过Aware接口把容器信息注入Bean
- BeanPostProcessor。进行进一步的构造,会在InitialzationBean前后执行对应方法,当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理
- InitializingBean。这一阶段也可以在bean正式构造完成前增加我们自定义的逻辑,但它与前置处理不同,由于该函数并不会把当前bean对象传进来,因此在这一步没办法处理对象本身,只能增加一些额外的逻辑。
- DispostbleBean。Bean将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁,如果Bean实现了接口,Spring将调用它的destory方法
- singleton:单例模式,Spring IoC容器中只会存在一个共享的Bean实例,无论有多少个Bean引用它,始终指向同一对象。
- prototype:原型模式,每次通过Spring容器获取prototype定义的bean时,容器都将创建一个新的Bean实例,每个Bean实例都有自己的属性和状态。
- request:在一次Http请求中,容器会返回该Bean的同一实例。而对不同的Http请求则会产生新的Bean,而且该bean仅在当前Http Request内有效。
- session:在一次Http Session中,容器会返回该Bean的同一实例。而对不同的Session请求则会创建新的实例,该bean实例仅在当前Session内有效。
- global Session:在一个全局的Http Session中,容器会返回该Bean的同一个实例,仅在使用portlet context时有效。
控制反转:原来是自己主动去new一个对象去用,现在是由容器工具配置文件创建实例让自己用,以前是自己去找妹子亲近,现在是有中介帮你找妹子,让你去挑选,说白了就是用面向接口编程和配置文件减少对象间的耦合,同时解决硬编码的问题(XML)
依赖注入:在运行过程中当你需要这个对象才给你实例化并注入其中,不需要管什么时候注入的,只需要写好成员变量和set方法
面向切面的编程,是一种编程技术,是OOP(面向对象编程)的补充和完善。OOP的执行是一种从上往下的流程,并没有从左到右的关系。因此在OOP编程中,会有大量的重复代码。而AOP则是将这些与业务无关的重复代码抽取出来,然后再嵌入到业务代码当中。常见的应用有:权限管理、日志、事务管理等。
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。Spring AOP实现用的是动态代理的方式。
jdk反射:通过反射机制生成代理类的字节码文件,调用具体方法前调用InvokeHandler来处理
cglib工具:利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
- 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
https://blog.csdn.net/qq_18425655/article/details/52163228
首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。
不稳定,如果最小的值在最后面,那么在第一次排序就被放到第一位,后面有与该值相等的元素也不会交换。
最好情况:每次划分过程产生的区间大小都为n/2,一共需要划分log2n次,每次需要比较n-1次,O(nlog2n)
最坏情况:每次划分过程产生的两个区间分别包含n-1个元素和1个元素,一共需要划分n-1次,每次最多交换n-1次,这就是冒泡排序了,O(n2)
单线程的IO复用模型。便于IO操作,不便于排序、聚合。
Redis自带的PUB/SUB机制,即发布-订阅模式。这种模式生产者(producer)和消费者(consumer)是1-M的关系,即一条消息会被多个消费者消费,当只有一个消费者时即可以看做一个1-1的消息队列
或者PUSH/POP机制
但都没有可靠的重试机制,需要自己管理与备份
查询一个数据库中不存在的数据,比如商品详情,查询一个不存在的ID,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成过大地压力。
当通过某一个key去查询数据的时候,如果对应在数据库中的数据都不存在,我们将此key对应的value设置为一个默认的值。
在高并发的环境下,如果此时key对应的缓存失效,此时有多个进程就会去同时去查询DB,然后再去同时设置缓存。这个时候如果这个key是系统中的热点key或者同时失效的数量比较多时,DB访问量会瞬间增大,造成过大的压力。
- 将系统中key的缓存失效时间均匀地错开
- 当我们通过key去查询数据时,首先查询缓存,如果此时缓存中查询不到,就通过分布式锁进行加锁
缓存中的某些Key(可能对应用与某个促销商品)对应的value存储在集群中一台机器,使得所有流量涌向同一机器,成为系统的瓶颈,该问题的挑战在于它无法通过增加机器容量来解决。
- 客户端热点key缓存:将热点key对应value并缓存在客户端本地,并且设置一个失效时间。
- 将热点key分散为多个子key,然后存储到缓存集群的不同机器上,这些子key对应的value都和热点key是一样的。
https://www.ibm.com/developerworks/cn/java/j-lo-jvm-optimize-experience/index.html
问题:比较耗CPU的任务摆在这里,程序也无法提升性能了,该怎么办?
- 先判断能否使用缓存,还是重新耗费CPU资源来创建一个
- 用偏重CPU性能的机器来做这个功能的专项负载均衡
- 请求太多的话,搞个消息队列排着,慢慢消费,同时前端提示需要一会才行
问题:数据库连接池就那么几个,但是有很多来拿怎么办?加个超时限制怎么加?
- 网页采集
- 内容过滤
- 信息存储
- 信息检索与后台管理
仅仅针对网页的源代码进行下载,不包含信息结构化提取
- 下载工具:使用HttlClient下载,放入Vector,用ConcurrentHashMap做爬虫深度控制
- URL解析逻辑:将网页源代码中的URL过滤出来放入集合
首先在下载队列中,需要安排一些源URL,当下载工具把网页源码下载下来之后,从中解析出能继续当成源URL的URL放入下载集合中,直到下载工具把下载集合中的内容都下载完成
简单易用,符合需求场景,不需要更高更强的功能
HttpURLConnection:本身的 API 不够友好,所提供的功能也有限
HttpClient:功能强大
OkHttp:是一个专注于性能和易用性的 HTTP 客户端。OkHttp 会使用连接池来复用连接以提高效率。OkHttp 提供了对 GZIP 的默认支持来降低传输内容的大小。OkHttp 也提供了对 HTTP 响应的缓存机制,可以避免不必要的网络请求。当网络出现问题时,OkHttp 会自动重试一个主机的多个 IP 地址
多线程爬虫要保证线程安全,还可以控制容量扩充的大小
ArrayList,Vector主要区别为以下几点:
- Vector是线程安全的,源码中有很多的synchronized可以看出,而ArrayList不是。导致Vector效率无法和ArrayList相比;
- ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,ArrayList默认增加为原来的50%,Vector默认增加为原来的一倍;
- Vector可以设置capacityIncrement,而ArrayList不可以,从字面理解就是capacity容量,Increment增加,容量增长的参数。
因为需要保存的是一个【URL -> 深度】的键值对,在爬虫下载的过程中需要通过多个线程高频地对这个键值对集合进行操作,所以在增加元素的时候需要进行同步操作以免元素被覆盖,还需要避免多线程reset导致的循环依赖问题
在下载到网页源码之后,将其中的所有URL通过Jsoup或者对每行内容进行正则表达式提取出来,如果符合预设值的正则表达式就添加到下载集合中。
使用Executors.newFixedThreadPool(),因为我爬虫线程基本固定不会需要销毁重建,并且可以控制线程最大数量,方便日后根据机器性能调整。
如果问到线程池的原理,请参这里。
phantomjs + selenium + java
模拟登录之后将sessionId保存到request header的cookie中
买个支持ADSL的拨号服务器,便宜的一个月80,然后在上面搭建代理服务器,用爬虫连上去
寻找网站访问频率访问限制漏洞,自己探索不同网站的访问频率限制规则
图像识别
因为下载下来的HTML文件都是小文件,所以使用HDFS存储需要考虑一下,但为了分布式与扩展还是使用。
由于小文件会导致大量元数据的产生,那么变通的方法就是在文件中再创建文件,比如一个64MB的大文件,比如其中可以包含16384个4KB的小文件,但是这个64MB的大文件只占用了1个inode,而如果存放4KB的文件的话,就需要16384个inode了。
那么如何寻址这个大文件中的小文件呢?方法就是利用一个旁路数据库来记录每个小文件在这个大文件中的起始位置和长度等信息,也就是说将传统文件系统的大部分元数据剥离了开来,拿到了单独的数据库中存放,这样通过查询外部数据库先找到小文件具体对应在哪个大文件中的从哪开始的多长,然后直接发起对这个大文件的对应地址段的读写操作即可。
我们需要两种节点,namenode节点来管理元数据,datanode节点来存储数据。
- 文件被切块存储在多台服务器上
- HDFS提供一个统一的平台与客户端交互
- 每个文件都可以保存多个副本
- 每个数据的副本数量固定,直接增加一台机器就可以实现线性扩展
- 有副本让存储可靠性高
- 可以处理的吞吐量增大
- 抽取网页数据放入索引表中,插入待建立索引的信息,等待建立索引
- 剪切源文件到配置的路径,建立百度快照
所以这里涉及到MySQL的使用,建立这样的一些表:
- S_column:栏目表
- S_index:配置索引然后创建索引产生的索引表,仅用于Lucene,对Lucene的查询是通过根据columnId来查数据库得到所在索引路径来查找,所以必须要把不同的栏目放在不同的文件夹中
- S_index_column:上面两个表的多对多关联表
- S_news:新闻业务表,存放新闻信息和某栏目的外键
- S_user:用户表
- T_index:保存仅用于Solr的索引信息,便于myfullretrieve建立Solr索引,对Solr的查询是通过用columnId来过滤得到的所有相关信息
###TF-IDF
如果某个词或短语在一篇文章中出现的频率TF高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。
一个词在本文出现次数/本文总词数
(文章总数/这个词在所有文章中出现次数)取对数
相乘
去掉网页内容相同或相似的网页
用TF-IDF统计每个文章中少数重要词(排序后的前几个),用MD5算法把每个词算出一个16进制的数全部加在一起然后比较两个总数,如果相同,那么两篇文章中重要的词就相同的,去掉。
词项查询(TermQuery)
布尔查询(BooleanQuery)
短语查询(PhraseQuery)
范围查询(RangeQuery)
百搭查询(WildardQuery)
FuzzQuery(模糊)
不是由记录来确定属性值,而是由属性值来确定记录的位置。
- 分词
- Hash去重
- 根据单词生成索引表,同时得到“词典文件”(词-> 单词ID)
- 得到“频率、位置文件”(单词ID -> 包含文档ID的倒排列表)
Lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)、位置文件 (positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。









