在浏览器输入url后发生了什么(浏览器输入url后发生了什么面试)

1.讲一下Java的虚拟机

2.说说怎么能让虚拟机中的方法区直接爆满

3.讲一下Java的垃圾回收机制

4.把Java中的容器类都讲一下

5.Java中的锁是怎么实现的?

6.引用计数法有啥缺点呢

7.说一下TCP的三次握手和四次挥手

8.为什么挥手时有个time_wait?即2MSL

9.说一下浏览器输入URL都发生了什么,到页面出来的流程

10.操作系统中的死锁怎么形成的,怎么预防死锁

11.进程和线程有什么区别

12.线程的几种状态

13.线程池用过没,怎么使用,流程是啥

14.创建线程有哪些方法,有啥区别,你一般怎么创建


1. 讲一下Java的虚拟机

6. 引用计数法有啥缺点呢

1. 引用计数法(Java没有采用)

2. 标记-清除法 (jvm老年代回收)

3. 标记-压缩法 (jvm老年代回收)

4. 复制算法 (jvm新生代回收)

引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。这也就是需要回收的对象。

引用计数算法是对象记录自己被多少程序引用,引用计数为零的对象将被清除。

计数器表示的是有多少程序引用了这个对象(被引用数)。计数器是无符号整数。

计数器的增减

引用计数法没有明确启动 GC 的语句,它与程序的执行密切相关,在程序的处理过程中通过增减计数器的值来进行内存管理。

new_obj() 函数

与GC标记-清除算法相同,程序在生成新对象的时候会调用 new_obj()函数。

这里 pickup_chunk()函数的用法与GC标记-清除算法中的用法大致相同。不同的是这里返回 NULL 时,分配就失败了。这里 ref_cnt 域代表的是 obj 的计数器。

在引用计数算法中,除了连接到空闲链表的对象,其他对象都是活跃对象。所以如果 pickup_chunk()返回 NULL,堆中也就没有其它大小合适的块了。

update_ptr() 函数

update_ptr() 函数用于更新指针 ptr,使其指向对象 obj,同时进行计数器值的增减。

这里 update_ptr 为什么需要先调用 inc_ref_cnt,再调用dec_ref_cnt呢?

是因为有可能 *ptr和 obj 可能是同一个对象,如果先调用dec_ref_cnt可能会误伤。

inc_ref_cnt()函数

这里inc_ref_cnt函数只对对象 obj 引用计数 1

func inc_ref_cnt(obj){

obj.ref_cnt

}

dec_ref_cnt() 函数

这里 dec_ref_cnt 函数会把之前引用的对象进行-1 操作,如果这时对象的计数器变为0,说明这个对象是一个垃圾对象,需要销毁,那么被它引用的对象的计数器值都需要相应的-1。

func dec_ref_cnt(obj){

obj_ref_cnt–

if(obj.ref_cnt == 0)

for(child : children(obj))

dec_ref_cnt(*child) // 递归将被需要销毁对象引用的对象计数-1

reclaim(obj)

}

上图这里开始时,A 指向 B,第二步 A 指向了 C。可以看到通过更新,B 的计数器值变为了0,因此 B 被回收(连接到空闲链表),C 的计数器值由1变成了2。

通过上边的介绍,应该可以看出引用计数垃圾回收的特点。

在变更数组元素的时候会进行指针更新

通过更新执行计数可能会产生没有被任何程序引用的垃圾对象

引用计数算法会时刻监控更新指针是否会产生垃圾对象,一旦生成会立刻被回收。

所以如果调用 pickup_chunk函数返回 NULL,说明堆中所有对象都是活跃对象。

引用计数算法的优点

1.可立即回收垃圾

每个对象都知道自己的引用计数,当变为0时可以立即回收,将自己接到空闲链表

2.最大暂停时间短

因为只要程序更新指针时程序就会执行垃圾回收,也就是每次通过执行程序生成垃圾时,这些垃圾都会被回收,内存管理的开销分布于整个应用程序运行期间,无需挂起应用程序的运行来做,因此消减了最大暂停时间(但是增多了垃圾回收的次数)

最大暂停时间,因执行 GC 而暂停执行程序的最长时间。

3.不需要沿指针查找

产生的垃圾立即就连接到了空闲链表,所以不需要查找哪些对象是需要回收的

引用计数算法的缺点

1.计数器值的增减处理频繁

因为每次对象更新都需要对计数器进行增减,特别是被引用次数多的对象。

2.计数器需要占用很多位

计数器的值最大必须要能数完堆中所有对象的引用数。比如我们用的机器是32位,那么极端情况,可能需要让2的32次方个对象同时引用一个对象。这就必须要确保各对象的计数器有32位大小。也就是对于所有对象,必须保留32位的空间。

假如对象只有两个域,那么其计数器就占用了整体的1/3。

3.循环引用无法回收

这个比较好理解,循环引用会让计数器最小值为1,不会变为0。

循环引用

像这样,两个对象相互引用,所以各个对象的计数器都为1,且这些对象没有被其他对象引用。所以计数器最小值也为1,不可能为0。

延迟引用计数法

引用计数法虽然缩小了最大暂停时间,但是计数器的增减处理特别多。为了改善这个缺点,延迟引用计数法(Deferred Reference Counting)被研究了出来。

通过上边的描述,可以知道之所以计数器增减处理特别繁重,是因为有些增减是根引用的变化,因此我们可以让根引用的指针变化不反映在计数器上。比如我们把 update_ptr($ptr, obj)改写成*$ptr = obj,这样频繁重写对重对象中引用关系时,计数器也不需要修改。但是这有一个问题,那就是计数器并不能正确反映出对象被引用的次数,就有可能会出现,对象仍在活动,却被回收。

在延迟引用计数法中使用ZCT(Zero Count Table),来修正这一错误。

ZCT 是一个表,它会事先记录下计数器在 dec_ref_cnt()函数作用下变成 0 的对象。

ZCT

dec_ref_cnt 函数

在延迟引用计数法中,引用计数为0 的对象并不一定是垃圾,会先存入到 zct 中保留。

func dec_ref_cnt(obj){

obj_ref_cnt–

if(obj.ref_cnt == 0) //引用计数为0 先存入到 $zct 中保留

if(is_full($zct) == TRUE) // 如果 $zct 表已经满了 先扫描 zct 表,清除真正的垃圾

scan_zct()

push($zct, obj)

}

scan_zct 函数

func scan_zct(){

for(r: $roots)

(*r).ref_cnt

for(obj : $zct)

if(obj.ref_cnt == 0)

remove($zct, obj)

delete(obj)

for(r: $roots)

(*).ref_cnt–

}

第二行和第三行,程序先把所有根直接引用的计数器都进行增量。这样,来修正计数器的值。

接下来检查 $zct 表中的对象,如果此时计数器还为0,则说明没有任何引用,那么将对象先从 $zct中清除,然后调用 delete()回收。

delete() 函数定义如下:

func delete(obj){

for(child : children(obj)) // 递归清理对象的子对象

(*child).ref_cnt–

if (*child).ref_cnt == 0

delete(*child)

reclaim(obj)

}

new_obj() 函数

除 dec_ref_cnt 函数需要调整,new_obj 函数也要做相应的修改。

func new_obj(size){

obj = pickup_chunk(size, $free_list)

if(obj == NULL) // 空间不足

scan_zct() // 扫描 zct 以便获取空间

obj = pickup_chunk(size, $free_list) // 再次尝试分配

if(obj == NULL)

allocation_fail() // 提示失败

obj.ref_cnt = 1

return obj

}

如果第一次分配空间不足,需要扫描 $zct,以便再次分配,如果这时空间还不足,就提示失败

在延迟引用计数法中,程序延迟了根引用的计数,通过延迟,减轻了因根引用频繁变化而导致的计数器增减所带来的额外的负担。

但是,延迟引用计数却不能马上将垃圾进行回收,可立即回收垃圾这一优点也就不存在了。scan_zct函数也会增加程序的最大暂停时间。

Sticky 引用计数法

对于引用计数法,有一个不能忽略的部分是计数器位宽的设置。假设为了反映所有引用,计数器需要1个字(32位机器就是32位)的空间。但是这会大量的消耗内存空间。比如,2个字的对象就需要一个字的计数器。也就是计数器会使对象所占的空间增大1.5倍。

sticky 引用计数法就是用来减少位宽的。

如果我们为计数器的位数设为5,那么计数器最大的引用数为31,如果有超过31个对象引用,就会爆表。对于爆表,我们怎么处理呢?

1. 什么都不做

这种处理方式对于计数器爆表的对象,再有新的引用也不在增加,当然,当计数器为0 的时候,也不能直接回收(因为可能还有对象在引用)。这样其实是会产生残留的对象占用内存。

不过,研究表明,大部分对象其实只被引用了一次就被回收了,出现5位计数器溢出的情况少之又少。

爆表的对象大部分也都是重要的对象,不会轻易回收。

所以,什么都不做也是一个不错的办法。

2. 使用GC 标记-清除算法进行管理

这种方法是,对于爆表的对象,使用 GC 标记-清除算法来管理。

func mark_sweep_for_counter_overflow(){

reset_all_ref_cnt()

mark_phase()

sweep_phase()

}

首先,把所有对象的计数器都设为0,然后进行标记和清除阶段。

标记阶段代码为:

func mark_phase(){

for (r: $roots) // 先把根引用的对象推到标记栈中

push(*r, $mark_stack)

while(is_empty($mark_stack) == False) // 如果堆不为空

obj = pop($mark_stack)

obj.ref_cnt

if(obj.ref_cnt == 1) // 这里必须把各个对象及其子对象堆进行标记一次

for(child : children(obj))

push(*child, $mark_stack)

}

在标记阶段,先把根引用的对象推到标记栈中

然后按顺序从标记栈中取出对象,对计数器进行增量操作。

对于循环引用的对象来说,obj.ref_cnt > 1,为了避免无谓的 push 这里需要进行 if(obj.ref_cnt == 1) 的判断

清除阶段代码为:

func sweep_phase(){

sweeping = $heap_top

while(sweeping < $heap_end) // 因为循环引用的所有对象都会被 push 到 head_end 所以也能被回收

if(sweeping.ref_cnt == 0)

reclaim(sweeping)

sweeping = sweeping.size

}

在清除阶段,程序会搜索整个堆,回收计数器仍为0的对象。

这里的 GC 标记-清除算法和GC 标记-清除算法 主要不同点如下:

1.开始时将所有对象的计数器值设为0

2.不标记对象,而是对计数器进行增量操作

3.为了对计数器进行增量操作,算法对活动对象进行了不止一次的搜索。

这里将 GC 标记-清除算法和引用计数法结合起来,在计数器溢出后,对象称为垃圾也不会漏掉清除。并且也能回收循环引用的垃圾。

因为在查找对象时不是设置标志位而是把计数器进行增量,所以需要多次查找活动对象,所以这里的标记处理比以往的标记清除花的时间更长,吞吐量会相应的降低。

7. 为什么挥手时有个time_wait?即2MSL

TIME_WAIT主要是用来解决以下几个问题:

1)上面解释为什么主动关闭方需要进入TIME_WAIT状态中提到的: 主动关闭方需要进入TIME_WAIT以便能够重发丢掉的被动关闭方FIN包的ACK。如果主动关闭方不进入TIME_WAIT,那么在主动关闭方对被动关闭方FIN包的ACK丢失了的时候,被动关闭方由于没收到自己FIN的ACK,会进行重传FIN包,这个FIN包到主动关闭方后,由于这个连接已经不存在于主动关闭方了,这个时候主动关闭方无法识别这个FIN包,协议栈会认为对方疯了,都还没建立连接你给我来个FIN包?

于是回复一个RST包给被动关闭方,被动关闭方就会收到一个错误(我们见的比较多的:connect reset by peer,这里顺便说下 Broken pipe,在收到RST包的时候,还往这个连接写数据,就会收到 Broken pipe错误了),原本应该正常关闭的连接,给我来个错误,很难让人接受;

2)防止已经断开的连接1中在链路中残留的FIN包终止掉新的连接2(重用了连接1的所有的5元素(源IP,目的IP,TCP,源端口,目的端口)),这个概率比较低,因为涉及到一个匹配问题,迟到的FIN分段的序列号必须落在连接2的一方的期望序列号范围之内,虽然概率低,但是确实可能发生,因为初始序列号都是随机产生的,并且这个序列号是32位的,会回绕;

3)防止链路上已经关闭的连接的残余数据包(a lost duplicate packet or a wandering duplicate packet) 干扰正常的数据包,造成数据流的不正常。这个问题和2)类似。

MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为tcp报文(segment)是ip数据报(datagram)的数据部分,具体称谓请参见《数据在网络各层中的称呼》一文,

而ip头中有一个TTL域,TTL是time to live的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。

2MSL即两倍的MSL,TCP的TIME_WAIT状态也称为2MSL等待状态,当TCP的一端发起主动关闭,在发出最后一个ACK包后,即第3次握手完成后发送了第四次握手的ACK包后就进入了TIME_WAIT状态,必须在此状态上停留两倍的MSL时间,等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。

在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

TTL与MSL是有关系的但不是简单的相等的关系,MSL要大于等于TTL。

9.说一下浏览器输入URL都发生了什么,到页面出来的流程

1、首先,在浏览器地址栏中输入url

2、浏览器先查看浏览器缓存-系统缓存-路由器缓存,如果缓存中有,会直接在屏幕中显示页面内容。若没有,则跳到第三步操作。

3、在发送http请求前,需要域名解析(DNS解析),解析获取相应的IP地址。

4、浏览器向服务器发起tcp连接,与浏览器建立tcp三次握手。

5、握手成功后,浏览器向服务器发送http请求,请求数据包。

6、服务器处理收到的请求,将数据返回至浏览器

7、浏览器收到HTTP响应

8、读取页面内容,浏览器渲染,解析html源码

9、生成Dom树、解析css样式、js交互

10、客户端和服务器交互

11、ajax查询

其中,步骤2的具体过程是:

浏览器缓存:浏览器会记录DNS一段时间,因此,只是第一个地方解析DNS请求;

操作系统缓存:如果在浏览器缓存中不包含这个记录,则会使系统调用操作系统,获取操作系统的记录(保存最近的DNS查询缓存);

路由器缓存:如果上述两个步骤均不能成功获取DNS记录,继续搜索路由器缓存;

ISP缓存:若上述均失败,继续向ISP搜索。

手撕代码:

思路:(可能不是很正规)

要使用尽可能少的硬币兑换,肯定是尽可能使用大额的硬币,但是有个问题是大额的硬币可能找不开啊,举个简单的例子:现在有100,30,1共三种硬币,我要凑120,尽可能使用大额硬币的话方案应该是一个100加上二十个1,很明显不对,没有四个30使用硬币少。 所以直接找还是不行,还是得递归或者回溯,我用的是直接递归,递归的过程就在代码里面体现了就不说了 ,说一下可取的点,虽然尽可能使用大额的硬币不能直接得到答案,但是我们可以得到一点有用的东西,我使用100的硬币得到的答案虽然不是最小的,但是也算是比较小的了,在这里是21枚硬币,我们可以借助这个21排除一些方案,比如我全选1的硬币肯定不行,120/1==120远大于21 直接排除。

代码:

iphone锁屏密码忘记了怎么重置(iphone锁屏密码忘了如何解锁)
« 上一篇 2024-11-09 18:08
支付宝花呗怎么快速提额(花呗可以申请提额吗)
下一篇 » 2024-11-09 18:08